I want my AOP!, Part 3

Use AspectJ to modularize crosscutting concerns in real-world problems

A spect-oriented programming (AOP) -- a new programming paradigm -- allows modular implementation of crosscutting concerns. In Part 1 of this three-part series covering AOP, I defined a concern as:

A concern is a particular goal, concept, or area of interest. In technology terms, a typical software system comprises several core and system-level concerns. For example, a credit card processing system's core concern would process payments, while its system-level concerns would handle logging, transaction integrity, authentication, security, performance, and so on. Many such concerns -- known as crosscutting concerns -- tend to affect multiple implementation modules. Using current programming methodologies, crosscutting concerns span over multiple modules, resulting in systems that are harder to design, understand, implement, and evolve.

AOP works in addition to, not as a replacement for, object-oriented programming (OOP). AspectJ, a free AOP implementation for Java, helps create software systems that are easier to implement, understand, and maintain.

Read the whole "I Want My AOP" series:

In Part 1, I introduced basic AOP concepts, while in Part 2 I presented an AspectJ tutorial. Armed with your new AOP and AspectJ knowledge, in this article you'll learn how to modularize crosscutting-concern implementations with real-world examples.

Although I discuss several AspectJ examples, by no means do I imply that such implementations can only work using AspectJ. In general, AOP (and for that matter, any other programming methodology) is not about what but about how. This article's examples demonstrate that implementing software systems with AOP offers crosscutting-concern modularity advantages, including more focused code and maintenance ease.

I keep the examples intentionally simple to focus on AOP's and AspectJ's usages. For example, in the thread-pooling example, I do not implement the pool management's full functionality. However, I do concentrate on how to apply the AOP techniques. Also note, for logging I deliberately use System.out.println() instead of the logging aspect to keep the focus from the main problem to the logging concern.

This article looks at examples from several categories: resource-pool management, policy enforcement, characteristics-based behavior, and flexible access control. I wrap up the article (and the series) by presenting several other ways to use AspectJ in your work.

Note: You can download this article's source code from Resources.

Example 1: Resource-pool management modularization

One common way to optimize resource usage recycles previously created resources such as threads and database connections.

A typical OOP approach to introducing resource pooling requires you to make a few up-front decisions. To wit, the design must specify when and how to obtain resources from the pool and how to put them back. Since profiling the system necessitates many optimizations, designing up-front resource pooling proves difficult. However, introducing such support afterwards requires you to modify many source files. (See Part 1's "Architecture's Dilemma" section for more details.) Further, the conventional approach lacks agility; it is not easy to turn on, off, or tune such optimizations.

Let's see how AOP and AspectJ help implement resource-pool management in a modularized fashion.

First, let's implement a simple TCP/IP service for converting requested strings to uppercase. The server creates a new thread each time a new connection request arrives. Once a thread completes serving a connection, it terminates naturally. The implementation below is simple Java without any AspectJ constructs:

// UppercaseServer.java
import java.io.*;
import java.net.*;
public class UppercaseServer {
    public static void main(String[] args) throws Exception {
      if (args.length != 1) {
          System.out.println("Usage: java UppercaseServer <portNum>");
          System.exit(1);
      }
      int portNum = Integer.parseInt(args[0]);
      ServerSocket serverSocket = new ServerSocket(portNum);
        
        while(true) {
            Socket requestSocket = serverSocket.accept();
            Thread serverThread 
                = new Thread(new UppercaseWorker(requestSocket));
            serverThread.start();
        }
    }
}
class UppercaseWorker implements Runnable {
    private Socket _requestSocket;
    public UppercaseWorker(Socket requestSocket) throws IOException {
        System.out.println("Creating new worker");
        _requestSocket = requestSocket;
    }
    public void run() {
        BufferedReader requestReader = null;
        Writer responseWriter = null;
        try {
            requestReader
                = new BufferedReader(
                      new InputStreamReader(_requestSocket.getInputStream()));
            responseWriter
                = new OutputStreamWriter(_requestSocket.getOutputStream());
            while(true) {
                String requestString = requestReader.readLine();
                if (requestString == null) {
                    break;
                }
                System.out.println("Got request: " + requestString);
                responseWriter.write(requestString.toUpperCase() + "\n");
                responseWriter.flush();
            }
        } catch(IOException ex) {
        } finally {
            try {
                if (responseWriter != null) {
                    responseWriter.close();
                }
                if (requestReader != null) {
                    requestReader.close();
                }
                _requestSocket.close();
            } catch (IOException ex2) {
            }
        }
        System.out.println("Ending the session");
    }
}

Next, let's see how AspectJ can add a thread-pooling crosscutting concern. First, write a simple ThreadPool class that acts as a stack for available threads. The get() method extracts a thread from the stack, whereas the put() method pushes one in. The put() method also contains the DelegatingThread class that delegates the run() method to the _delegatee worker object:

// ThreadPool.java
import java.util.*;
public class ThreadPool {
    List _waitingThread = new Vector();
    public void put(DelegatingThread thread) {
        System.out.println("Putting back: " + thread);
        _waitingThread.add(thread);
    }
    public DelegatingThread get() {
        if (_waitingThread.size() != 0) {
            DelegatingThread availableThread 
                = (DelegatingThread)_waitingThread.remove(0);
            System.out.println("Providing for work: " + availableThread);
            return availableThread;
        }
        return null;
    }
    static class DelegatingThread extends Thread {
        private Runnable _delegatee;
        
        public void setDelegatee(Runnable delegatee) {
            _delegatee = delegatee;
        }
        
        public void run() {
            _delegatee.run();
        }
    }
}

You now possess the classes needed to add thread pooling. Next, write an aspect that uses the ThreadPool class to add thread pooling to the server:

// ThreadPooling.java
public aspect ThreadPooling {
    ThreadPool pool = new ThreadPool();
    //=====================================================================
    // Thread creation
    //=====================================================================
    pointcut threadCreation(Runnable runnable) 
        : call(Thread.new(Runnable)) && args(runnable);
    Thread around(Runnable runnable) : threadCreation(runnable) {
        ThreadPool.DelegatingThread availableThread = pool.get();
        if (availableThread == null) {
            availableThread = new ThreadPool.DelegatingThread();
        }
        availableThread.setDelegatee(runnable);
        return availableThread;
    }
    //=====================================================================
    // Session   
    //=====================================================================
    pointcut session(ThreadPool.DelegatingThread thread)
        : execution(void ThreadPool.DelegatingThread.run()) && this(thread);
    void around(ThreadPool.DelegatingThread thread) : session(thread) {
        while(true) {
            proceed(thread);
            pool.put(thread);
            synchronized(thread) {
                try {
                    thread.wait();
                } catch(InterruptedException ex) {
                }
            }
        }
    }
    //=====================================================================
    // Thread start    
    //=====================================================================
    pointcut threadStart(ThreadPool.DelegatingThread thread) 
        : call(void Thread.start()) && target(thread);
    void around(Thread thread) : threadStart(thread) {
        if (thread.isAlive()) {
            // wake it up
            synchronized(thread) {
                thread.notifyAll();
            }
        } else {
            proceed(thread);
        }
    }
}

Let's examine the implementation in detail:

  • Pointcut threadCreation() captures joinpoints, thus creating a new thread object taking a Runnable object as the argument.
  • Advise the threadCreation() pointcut to first check the thread pool for available threads. If no thread is available, create a new one. In either case, set the delegatee to the Runnable object passed in and return that object instead. Note, you do not call proceed() in this advice, so therefore you never execute the captured operation.
  • Pointcut session() captures the run() method's execution of any ThreadPool.DelegatingThread objects.
  • By putting session() inside a while(true) loop, you advise session() to never finish the servicing. That ensures a thread, once created, never dies. Once a request is processed, you put the thread back into thread pool and put the thread into waiting state.
  • Pointcut threadStart() captures a call to the Thread.start() method. It uses isAlive() to check if the thread previously started. That would happen for a thread obtained from a pool and now in a waiting state. Wake up the thread by notifying it. If the thread had not started yet, as for a freshly created thread, proceed with starting the thread.

Note that you made no changes to UppercaseServer.java. The code for a simple test client is in UppercaseClient.java.

You can use the same technique for pooling database-connection objects. Simply capture joinpoints that create new connections and advise them to use one from a connection pool instead, if an appropriate connection is available. You also need to capture joinpoints that close connection objects and advise them to instead put those objects back in the resource pool.

Example 2: Policy-enforcement modularization

To implement policy enforcement using OOP, you must use a combination of documentation, code reviews, and so on. Since these techniques are manual, it is easy to miss policy violations.

In this section, you'll implement policy enforcement to ensure no duplicate listener objects are added to the models, and listeners do not loiter around when the view they represent becomes usable.

Swing's MVC (Model, View, Controller) pattern includes a few unenforced assumptions. First, models let you add a listener object more than once, which leads to duplicate work if an event-notification method carries an expensive operation. Second, you can easily forget to remove listeners before destroying a view. Consider a view onto a model. You add a listener to the model. The listener typically keeps a reference to a view (as a direct field, as a side effect of the listener being the view class's inner class, or as the view class itself being the listener). Now if a programmer forgets to remove its listener from the model before destroying it, the model still holds onto the listener that holds the view object. Since a garbage collector can trace such a view object to a live model object, it would not collect it. Such a view, then, would simply loiter around consuming memory. For more details and a solution, read Java Tip 79.

You can enforce a policy to avoid both situations. In this section, I present an AOP way to handle such policy enforcement. Simply compiling your sources along with these aspects will handle these policies -- no other changes are needed. Cool, right?

I also show how to use UML (Unified Modeling Language) to describe the aspect structural view. But let's first look at the overall structure. Figure 1 shows the aspect hierarchy for event management. I use an <<aspect>> stereotype to denote aspects and a pointcut qualifier to mark pointcuts.

Figure 1. The core listener management structure. Click on thumbnail to view full-size image.

Base aspect: EventListenerManagement

Since implementing both concerns requires capturing joinpoints that add listeners to models, you share the code by creating a base abstract EventListenerManagement aspect. This aspect contains an addListenerCall() pointcut that captures calls to methods adding a listener. It uses the modelAndListenerTypeMatch() pointcut to let derived aspects restrict methods captured by addListenerCall():

// EventListenerManagement.java
import java.util.*;
public abstract aspect EventListenerManagement {
    pointcut addListenerCall(Object model, EventListener listener) 
        : call(void *.add*Listener(EventListener+)) 
        && target(model) && args(listener) && modelAndListenerTypeMatch();
    abstract pointcut modelAndListenerTypeMatch();
}

Implement the uniqueness concern

At its core, the uniqueness concern, before adding any listener, checks whether that listener was previously added. If that listener is already present, the operation does not proceed; otherwise, it adds the listener.

The EventListenerUniqueness aspect implements the concern's core functionality, thus ensuring no duplicate listener objects in a model. That abstract aspect declares EventListenerManagement as its parent. It advises the addListenerCall() pointcut to check for the listener's uniqueness by looking in a list obtained by invoking getCurrentListeners(). It proceeds with adding the listener only if the list doesn't include a listener:

// EventListenerUniqueness.java
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
public abstract aspect EventListenerUniqueness 
    extends EventListenerManagement {
    void around(Object model, EventListener listener) 
        : addListenerCall(model, listener) {
        EventListener[] listeners = getCurrentListeners(model);
        if (!Utils.isInArray(listeners, listener)) {
            System.out.println("Accepting " + listener);
            proceed(model, listener);
        } else {
            System.out.println("Already listening " + listener);
        }
    }
    public abstract EventListener[] getCurrentListeners(Object model);
}

As Figure 2 shows, the concrete aspect TableModelListenerUniqueness extends EventListenerUniqueness to apply the aspect to TableModel and related classes. It provides an implementation for the modelAndListenerTypeMatch() pointcut to restrict the model type to AbstractTableModel and the listener type to TableModelListener. Another concrete aspect, ListDataListenerUniqueness, does the same for list-related classes. For ListDataListenerUniqueness's source code, see Resources.

Figure 2. Structure for implementing uniqueness concern for tables and lists. Click on thumbnail to view full-size image.
// TableModelListenerUniqueness.java
import java.util.EventListener;
import javax.swing.event.TableModelListener;
import javax.swing.table.*;
aspect TableModelListenerUniqueness extends EventListenerUniqueness {
    pointcut modelAndListenerTypeMatch() 
        : target(AbstractTableModel) && args(TableModelListener);
    public EventListener[] getCurrentListeners(Object model) {
        return ((AbstractTableModel)model)
            .getListeners(TableModelListener.class);
    }
}

Each concrete aspect, TableModelListenerUniqueness, for example, handles the listener associated with a model type. You can create a simple aspect such as this for other Swing models, as well as your models. Compiling your source code along with these aspects will ensure no duplicate listener is added.

Implement a no loitering-views concern

Next, you implement a no loitering-views concern, as Figure 3 shows. Instead of adding listeners to a model directly, you can wrap it as a referent in a WeakReference object and add it. Since the added object must be of the correct listener type, use the Decorator pattern in a class extending WeakReference and implementing the required listener interface. The decorator implements the listener interface by delegating each method to the referent object.

Figure 3. The structure for implementing a no loitering-views concern. Click on thumbnail to view full-size image.

The EventListenerWeakening aspect implements the concern's core functionality, thus ensuring that no view loiters after its destruction. That abstract aspect also declares EventListenerManagement as its parent. It advises addListenerCall() to proceed with the listener obtained by calling the getWeakListener() method:

// EventListenerWeakening.java
import java.lang.ref.*;
import java.util.*;
import javax.swing.event.*;
public abstract aspect EventListenerWeakening 
    extends EventListenerManagement dominates EventListenerUniqueness {
    void around(Object model, EventListener listener) 
        : addListenerCall(model, listener) {
        proceed(model, getWeakListener(listener));
    }
    public abstract EventListener getWeakListener(EventListener listener);
}

The WeakEventListener decorates the EventListener in addition to extending WeakReference. The nested RemoveGarbageCollectedListeners aspect removes WeakEventListener from the model when it detects that the referent is garbage collected. Here you check the collected referent in an event notification method. If you don't wish to wait for the event to happen after the view disappears, you could use an implementation using ReferenceQueues and a reaper thread:

// EventListenerWeakening.java (contd...)
public abstract class WeakEventListener extends WeakReference 
    implements EventListener {
    public WeakEventListener(EventListener delegatee) {
        super(delegatee);
    }
    public EventListener getDelegatee() {
        return (EventListener)get();
    }
    public boolean equals(Object other) {
        if (getClass() != other.getClass()) {
            return false;
        }
        return getDelegatee() == ((WeakEventListener)other).getDelegatee();
    }
    
    public String toString() {
        return "WeakReference(" + get() + ")";
    }
    
    abstract static aspect RemoveGarbageCollectedListeners {
        pointcut eventNotification(WeakEventListener weakListener,
                                   EventObject event) 
            : execution(void WeakEventListener+.*(EventObject+))
            && this(weakListener) && args(event) 
            && lexicalScopeMatch();
        abstract pointcut lexicalScopeMatch();
        
        public abstract void removeListener(EventObject event, 
                                            EventListener listener);
        void around(WeakEventListener weakListener, EventObject event) 
            : eventNotification(weakListener, event) {
            if (weakListener.getDelegatee() != null) {
                proceed(weakListener, event);
            } else {
                System.out.println("Removing listener: " + weakListener);
                removeListener(event, weakListener);
            }
        }
    }
}

The abstract lexicalScopeMatch() pointcut ensures only methods from specific WeakListener types are captured. Without this pointcut, execution of all WeakListeners notification would be caught -- for example, table as well as lists. Since removeListener() would cast arguments to specific types, a ClassCastException would throw.

The EventListenerWeakening aspect dominates EventListenerUniqueness, which causes EventListenerWeakening's advice to addListenerCall() to be applied before that of EventListenerUniqueness. That way, you create the wrapped listener first, and the uniqueness check using WeakEventListener.equals() works correctly. Since listeners added in a model are WeakReference wrapped, comparing bare listeners would cause a comparison using equals() to not match. You could avoid the problem by modifying equals(), but that could cause the equals() method's symmetric nature to break.

The TableModelListenerWeakening aspect handles table-related listeners. It uses a specialized WeakEventListener that implements TableModelListener by delegating to the referent object. You'll find the code for the aspect that handles the list-related listener in ListModelListenerWeakening.java:

// TableModelListenerWeakening.java
import java.util.*;
import javax.swing.event.*;
import javax.swing.table.*;
public aspect TableModelListenerWeakening extends EventListenerWeakening {
    pointcut modelAndListenerTypeMatch() 
        : target(AbstractTableModel) && args(TableModelListener);
    public EventListener getWeakListener(EventListener listener) {
        System.out.println("Weakening " + listener);
        return new WeakTableModelListener((TableModelListener)listener);
    }
}
public class WeakTableModelListener extends WeakEventListener 
    implements TableModelListener {
    public WeakTableModelListener(TableModelListener delegatee) {
        super(delegatee);
    }
    
    public void tableChanged(TableModelEvent e) {
        TableModelListener listener = (TableModelListener)getDelegatee();
        listener.tableChanged(e);
    }
    
    static aspect TableRemoveGarbageCollectedListeners 
        extends WeakEventListener.RemoveGarbageCollectedListeners {
        
        pointcut lexicalScopeMatch() : within(WeakTableModelListener);
        public void removeListener(EventObject event, EventListener listener) {
            ((TableModel)event.getSource())
                .removeTableModelListener((TableModelListener)listener);
        }
    }
}

With the aforementioned aspects, you've created a crosscutting concern to avoid multiple notifications to the same model listener, as well as avoided loitering view objects. The simple Test.java tests the functionality. The aspects use the utility Utils.java class.

Example 3: Characteristic-based implementation modularization

Operations with the same characteristics should typically implement common behaviors. For example, a wait cursor should be put before any slow method executes. Similarly, you may need to authenticate access to all security-critical data.

Since such concerns possess a crosscutting nature, AOP and AspectJ offer mechanisms to modularize them. Because a method's name might not indicate its characteristics, you need a different mechanism to capture such methods. To create such a mechanism, declare the aspect adding characteristic-based crosscutting behavior as an abstract aspect. In that aspect, declare an abstract pointcut for methods with characteristics under consideration. Finally, write an advice performing the required implementation.

Consider SlowMethodAspect's implementation. It declares the abstract slowMethods() pointcut and advises it to first put a wait cursor, proceed with the original operation, and finally restore the original cursor, as Figure 4 shows.

Figure 4. The aspect structure for crosscutting characteristics-based concerns. Click on thumbnail to view full-size image.

Here's the code:

// SlowMethodAspect.java
import java.util.*;
import java.awt.*;
import java.awt.event.*;
public abstract aspect SlowMethodAspect {
    abstract pointcut slowMethods(Component uiComp);
    void around(Component uiComp) : slowMethods(uiComp) {
        Cursor originalCursor = uiComp.getCursor();
        Cursor waitCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
        uiComp.setCursor(waitCursor);
        try {
            proceed(uiComp);
        } finally {
            uiComp.setCursor(originalCursor);
        }
    }
}

Two test components, GUIComp1 and GUIComp2, nest a concrete implementation of the aspect. For example, GUIComp1 contains following aspect:

public static aspect SlowMethodsParticipant extends SlowMethodAspect {
pointcut slowMethods(Component uiComp) 
            : execution(void GUIComp1.performOperation1())
            && this(uiComp);
}

Whereas, GUIComp2 contains following aspect:

public static aspect SlowMethodsParticipant extends SlowMethodAspect {
        pointcut slowMethods(Component uiComp) 
            : (execution(void GUIComp2.performOperation1())
               || execution(void GUIComp2.performOperation2()))
            && this(uiComp);
}

Compared to this article's other examples, in this example the aspected classes now no longer remain oblivious to other aspects; they include code to participate in the collaboration. However, such knowledge is limited -- it merely declares a method with certain known characteristics. Also note, you need not implement SlowMethodsParticipant as a nested aspect. However, since these aspects completely depend on the surrounding classes, making them nested aspects helps keep the pointcut coordinated with implementation changes.

That usage pattern lets you provide aspectual class interfaces and lets you capture semantics- and characteristics-based pointcuts not otherwise possible by property-based pointcuts.

You'll find the complete code for both test components in GUIComp1.java and GUIComp2.java, and a simple test application in Test.java (see Resources).

Example 4: Implement flexible access control

With AspectJ you can declare compile-time warnings and errors, a mechanism with which you can enforce static crosscutting concerns. For example, such a mechanism can enforce an access-control crosscutting concern. Java's access controls -- public, private, package, and protected -- do not offer enough control in many cases. Consider, for example, a typical Factory design-pattern implementation. Oftentimes, you want only the factory to create the product objects. In other words, classes other than the factory should be prohibited from accessing constructors of any product class. While marking constructors with package access offers the best choice, that works only when the factory resides in the same package as the manufactured classes -- quite an inflexible solution.

C++'s friend mechanism controls access to a class from other specified classes and methods. Similarly, with AspectJ you can implement such functionality in Java, as well as a much more fine-grained and expressive access control.

The following code declares a simple Product class with a constructor and a configure() method. It also declares a nested FlagAccessViolation aspect, which in turn declares one pointcut to detect constructor calls from classes other than ProductFactory or its subclasses, and another pointcut to detect configure() method calls from classes other than ProductConfigurator or its subclasses. It finally declares either such violations as compile-time errors. ProductFactory, ProductConfigurator, or its subclasses could reside in any package, and both pointcuts could specify packages if necessary. You can further restrict access to, say, the createProduct() method, using the withincode() pointcut instead of within:

// Product.java
public class Product {
    public Product() {
        // constructor implementation
    }
    public void configure() {
        // configuration implementation
    }
    static aspect FlagAccessViolation {
        pointcut factoryAccessViolation() 
            : call(Product.new(..)) && !within(ProductFactory+);
        
        pointcut configuratorAccessViolation() 
            : call(* Product.configure(..)) && !within(ProductConfigurator+);
        declare error 
            :  factoryAccessViolation() || configuratorAccessViolation() 
            : "Access control violation";
    }
}

The ProductFactory class calls the Product.configure() method, thus causing a compile-time error with a specified "Access control violation" message:

// ProductFactory.java
public class ProductFactory {
    public Product createProduct() {
        return new Product();
    }
    
    public void configureProduct(Product product) {
        product.configure();
    }
}

The rest in short

In this section, I outline a few simple, yet powerful AspectJ usages. You may implement these examples as part of your AspectJ learning process.

Logging and debugging

You can easily try a logging and debugging project in AspectJ. Using AspectJ for logging and debugging also lets you log different contextual information without touching the main code.

Design by contract

Design by contract (DBC) requires explicit contracts that must hold true at various execution points, such as before and after each operation. You can use AspectJ to enforce DBC by creating aspects containing advices to check contracts at required execution points. If you wish to not enforce contracts in a production environment, simply exclude DBC aspects from your production build.

Realize design patterns

AOP allows interesting twists when you implement certain design patterns. For example, AOP lets you look at MVC in a new way. Without AOP, MVC models are responsible for three concerns: state management, listener management, and state update notification. With AOP and AspectJ, you modularize each concern, which also lets you use models originally written only for state management, for example, Date or Point2D in MVC.

Lazy creation/initialization

It's common to optimize memory-intensive objects by lazily creating them. As a result, they exist just in time to serve for the first time. As with other optimizations, you'd use lazy instantiation because of profiling. However, it's invasive to change your implementation everywhere to lazily create certain fields. At the most, OOP only can require programmers to always use a get<Field>() method and modularize logic for "create-if-needed" in that method. With that approach, if a programmer forgets to use this method and does a direct access, a crash awaits. Marking such fields with private access does not help for in-class field access. Even if you discover the need earlier during the design phase and the problem mentioned above is unimportant, it is nice to separate optimization concerns to avoid overloading the main implementation logic.

AspectJ's get() pointcut lets you capture access to a field. You can advise such a pointcut to create an object if the field was null. With this, the logic of checking for null and creating an object if needed is modularized.

Caching

High performance applications need to cache expensive-to-create objects. For example, a server serving images, say stock charts, may need to cache byte-stream image representations. AspectJ can help separate caching concern implementations. With such a scheme, you capture image creation joinpoints, looking into the cache for an available image for some given data, then using the cached image instead. If no such image is available, the advice to such joinpoints creates an image and stores it in a hash table. The caching implementation could also employ a soft reference or some other scheme to ensure cached images do not cause the application to starve for memory. By using AspectJ, you can make noninvasive changes if the caching logic needs to change.

Parting thoughts

Be warned before reading this section as it represents my opinionated view of the world!

AOP and AspectJ solve real problems, and early adopters are increasingly trying AOP concepts with AspectJ. Like any other new programming paradigm, it will take time before both technologies reach their full potential. As more developers use AspectJ, the experience gained will guide the language's evolution. Simultaneously, design patterns and anti-patterns will appear. AOP's adaptation as a mainstream programming methodology will be a fun thing to watch and, better still, participate in.

I want to thank Kavita Laddad and Rick Warren for reviewing this series.

Ramnivas Laddad is a Sun Certified Architect of Java Technology. He holds a master's degree in electrical engineering with a specialization in communication engineering. He has been architecting, designing, and developing software projects involving GUIs, networking, distributed systems, real-time systems, and modeling for more than eight years. Ramnivas began working with object-oriented Java systems five years ago. As a principal software engineer at Real-Time Innovations, Inc., Ramnivas leads the development of the next generation of ControlShell, a component-based programming framework for building complex real-time systems.

Learn more about this topic