What is a design pattern? According to Design Patterns: Elements of Reusable Object-Oriented Software (commonly referred to as GoF; see Resources for details):
A design pattern systematically names, motivates, and explains a general design that addresses a recurring design problem in object-oriented systems. It describes the problem, the solution, when to apply the solution, and its consequences. It also gives implementation hints and examples. The solution is a general arrangement of objects and classes that solve the problem. The solution is customized and implemented to solve the problem in a particular context.
After years of successfully applying patterns to solve problems in OO systems, I found myself nodding along with this definition. Patterns were a great way to talk to my fellow programmers about design, and they represented best practices that addressed recurring design problems. So it was a bit of a shock to me when I attended a talk by Stuart Halloway where he suggested an alternate title for the GoF: "Workarounds for things that are broken in C++." His point was that what exists as a "pattern" in one language can be subsumed into the language itself in a different paradigm. He went on to give the example of Factories -- useful in the Java™ language, but less useful in Objective-C, which lets you return subtypes from a constructor.
I had to think about it for a while before I realized that the two sides were saying the same thing: design patterns give us a vocabulary for expressing concepts that can't be said directly in the programming language.
So where does AOP come in? For OOP, we have the GoF patterns, which give us a consistent, though sometimes cumbersome, way of working with common concepts like observers and decorators. AOP builds on OOP to give us a direct way of expressing crosscutting concerns. It turns out that some of the GoF patterns are about crosscutting and can be expressed directly in AOP. So what you'll notice is that some of the patterns that involve many classes can be expressed with a single aspect. Some patterns become easier to use because they involve less code. Some are so well supported that they almost disappear. Others are strictly tied to OOP (for example, the patterns dealing with class structures) and remain unchanged when used in conjunction with AOP.
This article explores pattern implementation with AOP (specifically AspectJ). I chose to tackle GoF patterns because of their widespread popularity and general utility. In this part of the article, I set up some criteria for analyzing the impact of patterns and then go on to investigate the Adapter and Decorator patterns. Adapter demonstrates the benefits of static crosscutting, while Decorator reveals itself as a disappearing pattern. In Part 2, I offer a more in-depth study of the Observer pattern, which does not disappear, but sees major benefits when implemented in AspectJ. Part 2 illustrates how AspectJ allows patterns to be converted into reusable base aspects, thus allowing you to download prebuilt pattern libraries -- an exciting prospect for the pattern enthusiast.
Why apply AOP to design patterns?
I previously claimed that many patterns are crosscutting, but of course I wasn't the first one to think of this. A recent research paper analyzed the GoF patterns and found that 17 out of the 23 patterns exhibited some degree of crosscutting. (The paper is Jan Hannemann and Gregor Kiczales's "Design Pattern Implementation in Java AspectJ," see the Resources section for more details.) If AOP promises to help with crosscutting, what kind of benefits can you expect to see from using it on design patterns? I'll start by answering that question in general terms, and then set up a framework through which I'll consider each design pattern.
Benefits of using AOP on design patterns
The first key benefit of AOP is the ability to localize the
code for a given design pattern. This means that the pattern can often
be realized in a single aspect or a pair of closely related aspects.
(This stands in contrast to a Java-language implementation, where a
pattern application can spread across several classes.) Being able to
see all of the code in a single location leads to several practical
benefits. First, readers of the code can understand the pattern more
easily if all of its interactions are visible in one place. Second, if
developers need to alter a pattern implementation, they can
make that change in one place, instead of chasing the pattern
fragments throughout the system. Third, developers can use meaningful
names to describe the aspect that encapsulates the pattern, giving
textual clues as to the intent of the pattern to future maintainers.
For example you could name an aspect SensorAdapter, indicating that you had applied
the Adapter pattern to sensors.
The other key benefit of an AspectJ pattern implementation is obliviousness. In other words, classes that play a role in the pattern do not necessarily have to be aware of that role. This benefit results directly from localization -- because the pattern is localized in an aspect, it does not invade its participants. Obliviousness makes the code for pattern participants easier to understand. Not only that, but obliviousness makes pattern composition a great deal easier. In a Java-language implementation, if a class participates in multiple patterns, the pattern machinery can quickly obscure its core meaning. Classes that do not know about their participation in a pattern can also be reused in other contexts. You'll see a concrete example of this in Part 2 of the article.
These benefits allow for code-level reuse of some patterns. The
concepts and structure of design patterns have always been reusable.
Anyone who wanted to implement the Observer pattern could pull out the
GoF book and meld the pattern into their code. But with
aspect-orientation, you can spare yourself the trouble by downloading
the ObserverProtocol aspect (it's available
at the Design Patterns project; check the Resources section). Beyond the saved
implementation effort, code-level reuse also allows tighter coupling
of pattern code and documentation. For example, I can browse the
javadocs for ObserverProtocol and
understand its intent and structure without having to pick up a
separate textbook.
Each pattern description follows a common structure. I'll begin with an example problem and provide a general description of the pattern. Then I will describe how to implement the pattern, first in the Java language and then in the AspectJ language. After each implementation I will describe what makes the pattern crosscutting and what effects that version of the pattern has on understanding, maintaining, reusing, and composing the code.
Click the Code icon at the top or bottom of this article (or see Download) to download the complete source for upcoming examples.
The first pattern I'll consider in detail is Adapter. Adapter is all about compatibility. The pattern allows classes to interact that otherwise couldn't because of incompatible interfaces. To implement Adapter in Java code, you wrap a class (the target class) with a special Adapter class that translates the API of the target class into one that clients expect or can more easily make use of.
The setup: Providing an aggregated sensor readout
Let's say you're designing a space vehicle, and you've been asked to provide a readout of all of the critical sensors in the spacecraft. Because you're extending an existing system, convenient Java classes already exist for each sensor you need to access. For example, you can access a temperature gauge with the following class:
public class TemperatureGauge {
public int readTemperature(){
//accesses sensor internals
}
}
|
You can access a radiation sensor with:
public class RadiationDetector {
public double getCurrentRadiationLevel(){
//read radiation somehow
}
}
|
Some of these sensor classes were written by other team members, some by third-party vendors. What you would like to do is to provide a display of each sensor's status, so that a mission commander can see at a glance whether his craft is in trouble. Here's an example of what you're looking for. (The real display would probably involve flashing red lights and klaxons, but I'll stick to text for now.)
Readout: Sensor 1 status is OK Sensor 2 status is OK Sensor 3 status is BORDERLINE Sensor 4 status is DANGER! |
You could accomplish a display like this using the following method:
public static void main(String[] args){
RadiationDetector radiationDetector = //find critical detector
TemperatureGauge gauge = //get exhaust nozzle gauge
List allSensors = new ArrayList();
allSensors.add(radiationDetector);
allSensors.add(gauge);
int count = 1;
for (Sensor sensor : allSensors) {
//How to read each type of sensor...?
}
}
|
So far, so good, but how do you read each sensor without resorting
to ugly if(sensor instanceof XXX) checks?
One option is to modify each sensor class to have a getStatus() method that interprets the sensor's
readings and returns a String, as shown here:
if(this.readTemperature() > 160){
return "DANGER";
}
return "OK"
|
Doing this introduces unrelated status-display code into multiple classes, adding to their complexity. Further, there might be practical limitations (such as having to recompile a third-party class). That's where the Adapter pattern steps in.
The traditional implementation of the Adapter pattern works
by wrapping each target class with a class that implements a convenient API. In this
case, you would create a common interface called, say, StatusSensor, shown here:
public interface StatusSensor {
String getStatus();
}
|
With this common interface in place, you could implement the readout method like this:
for (StatusSensor sensor : allSensors) {
System.out.print("Sensor " + count++);
System.out.println(" status is " + sensor.getStatus());
}
|
The only remaining challenge is to make each sensor conform to
the interface. The Adapter classes accomplish this. As you can see in
Listing 1, each Adapter stores the sensor it wraps in a member
variable and uses that underlying sensor to implement the getStatus method:
Listing 1. Adapter classes and client code
//Adapter classes
public class RadiationAdapter implements StatusSensor {
private final RadiationDetector underlying;
public RadiationAdapter(RadiationDetector radiationDetector) {
this.underlying = radiationDetector;
}
public String getStatus() {
if(underlying.getCurrentRadiationLevel() > 1.5){
return "DANGER";
}
return "OK";
}
}
public class TemperatureAdapter implements StatusSensor {
//...similar
}
//glue code to wrap each sensor with its adapter...
allSensors.add(new RadiationAdapter(radiationDetector));
allSensors.add(new TemperatureAdapter(gauge));
|
The listing also shows "glue code" that wraps each sensor with the appropriate Adapter before the readout. The pattern does not dictate that this glue code appear in any particular place. Likely locations include "just after creation" and "just before use." The sample code places it just before it adds the sensor to the readout collection.
Analysis of the Java language Adapter
The Adapter pattern fares reasonably well in its traditional implementation. What makes this crosscutting? Well, the "status" concern cuts across several different sensor classes. If you were to co-locate the Adapter classes in a package, the OO implementation of this pattern would modularize fairly well. The package would become an "Adapter Module." The wrapping idiom prevents the sensors from knowing about the pattern, which leads to looser coupling. Unfortunately, the part of the application that did the wrapping would need to know about both the Adapters and the sensors they applied to. So the glue code location would also be crosscut by the pattern.
Now, here's how the Java language Adapter stacks up against my evaluation criteria:
- Understanding: Evocatively named
SensorAdapters co-located in a package make the intent of this pattern clear. Unfortunately, the glue code's location might be far from the Adapter package. Since the glue code area is unstructured, you might overlook it while attempting to understand the pattern or trip over it while trying to understand the code it tangles with.
You must also take care to deal with issues of object identity. That is, if wrapped and unwrapped versions of the same object coexist in a system, you will have to sort out whether they should be regarded as equal. - Reusing: To reuse this pattern, you must reimplement it
from scratch.
- Maintaining: As you add new sensors to the readout, you
must add new adapter classes and update the glue code to wrap them.
- Composing: Imagine you want to involve the sensors in another pattern. The sensors are oblivious to the Adapter, so they would be unaffected. However, this a two-edged sword. Should the new pattern consider the adapted or the unadapted version of the sensor to be its object?
As with other design patterns, the AspectJ implementation of Adapter preserves the intent and concepts of its cousin. The implementation uses intertype declarations, an important type of crosscutting support that gets less airtime than pointcuts and advice. If you need a refresher on static crosscutting, check out the Resources section for appropriate pointers.
As with the pure OOP version, the AOP version of Adapter requires the StatusSensor interface. However, instead of using
separate wrapper classes, the AspectJ version uses the declare parents
form to make the various sensors implement StatusSensor directly, as you see here:
public aspect SensorAdapter {
declare parents :
(TemperatureGauge || RadiationDetector)
implements StatusSensor;
}
|
Now the sensors should conform to the interface. But they do not yet
implement the interface (a fact that the AspectJ compiler will
happily point out to you). To complete the pattern implementation, you
must add intertype method declarations to the aspect to make each
sensor conform. The code below adds the getStatus() method to the TemperatureGauge class:
public String TemperatureGauge.getStatus(){
if(this.readTemperature() > 160){
return "DANGER";
}
return "OK";
}
|
The AspectJ version of the readout class looks the same as the version implemented in the Java language, except that no glue code is necessary to wrap the sensors. Each sensor is its own wrapper.
Analysis of the AspectJ Adapter
The key benefit of implementing Adapter with AspectJ is localization. The entire pattern is contained within a single aspect, rather than in two (or more) separate adapter classes and an unstructured "wrapping" location. Here's how the AspectJ implementation fares according to my criteria:
- Understanding: The lack of wrapping removes the need to
look anywhere other than the Adapter aspect to understand the
pattern. No wrapping also removes the need to deal with object
identity issues.
- Reusing: The AspectJ version isn't any more or less
reusable than the other version.
- Maintaining: Because each new sensor involves only writing a
method (rather than a full class), extending the AspectJ
implementation should be slightly easier. As the number of adaptees
grows or the logic required for each adaptation becomes complex, you
may find that a single aspect becomes unreasonably long. In this case,
you can split the aspect into several. Splitting the aspect loses some
of the localization benefits, but preserves the other benefits.
- Composing: You can compose multiple patterns easily because there are no wrapping-coordination issues to deal with.
The upshot is that both the Java and AspectJ implementations do a good job of staying out of the way of the sensor classes. However, only the AspectJ version stays out of the rest of your application. Is this a major advantage? It probably depends on whether your application exhibits any of the complicating properties described in the analyses. If I were using AspectJ on a project, I would definitely use it to implement Adapter, although I wouldn't introduce AspectJ just to solve this problem. The next pattern, Decorator, provides some more compelling advantages.
Decorator is an interesting pattern to consider from an aspect-oriented perspective because it's one of the patterns that comes closest to "disappearing" with the capabilities introduced by an aspect-oriented language such as AspectJ. Why is this? A closer look at how Decorator evolves in both aspect- and object-oriented implementations should make it clear.
The Decorator pattern aims to add capabilities dynamically to an
existing object. The canonical example given in the GoF book involves
literal decoration. In their example, a GUI component class is wrapped
in a decorator class that adds a border, or perhaps scrollbars, to the
display of the component. The Java Class Libraries feature Decorator
prominently, with the methods on java.util.Collections that decorate a Collection so that it becomes unmodifiable or
synchronized, as well as a rich variety of IO streams that can buffer,
inflate, or monitor other streams.
The setup: Monitoring file reads
To add spice to this example, I chose to stick with the Decorators
in the Java distribution and see what it would take to
replicate one of them in AspectJ. One of the decorators I found
intriguing was ProgressMonitorInputStream
from javax.swing. According to the
documentation, ProgressMonitorInputStream
monitors the progress of reading an underlying input stream.
To demonstrate decoration in action, I've written a simple GUI that reads a file. You can view the code for reading the input stream in Listing 2 and see screenshots of the running app in Figure 1. (You can also play with the full source of the example by clicking on the Code icon at the top or bottom of this page.)
Figure 1. The ProgressMonitor for the stream
You may wish to have the javadocs or indeed the source code for
java.io and ProgressMonitorInputStream open beside you as you
consider the next section; see Resources for
further reference.
In the Java language, you realize the Decorator pattern by first
creating an AbstractComponent interface (or
class) that both base implementations (sometimes referred to as ConcreteComponents) and decorators can conform
to. In this example, the AbstractComponent
is java.io.InputStream, which defines the
interface both for FileInputStream (the
ConcreteComponent) and BufferedInputStream (a ConcreteDecorator).
Although it is not strictly necessary, Decorator implementations
often feature an AbstractDecorator that
maintains a reference to the decorated component and provides the
basic decoration mechanics without adding any additional behavior. In
java.io, FilterInputStream provides this functionality.
Finally, a ConcreteDecorator extends the
AbstractDecorator, overrides methods
requiring decoration, and adds behavior before or after invoking the
same method on the decorated component. In
this case, ProgressMonitorInputStream plays
the ConcreteDecorator.
Glancing at Sun's implementation of ProgressMonitorInputStream (which I won't reprint
here because of licensing concerns), you can see that it instantiates a
javax.swing.ProgressMonitor upon creation.
After every read method, it updates the monitor with a count
of how many bytes have been read from the underlying stream. The
separate ProgressMonitor class determines
when to pop up a monitoring dialog and updates the visual display.
To use ProgressMonitorInputStream, you
simply need to wrap another input stream (as in Listing 2) and be sure
to refer to the wrapped instance when doing reads. Notice the
similarity here between the Adapter and Decorator patterns: both
require programmatic application of the additional behavior to the
target class.
Listing 2. Monitoring an InputStream
private void actuallyReadFile() {
try {
InputStream in = createInputStream();
byte[] b =new byte[1000];
while (in.read(b) != -1) {
//do whatever here
bytesRead+=1000;
}
bytesReadLabel.setText("Read " + (bytesRead/1000) + "k");
bytesRead = 0;
in.close();
} catch (Exception e) {
//handle...
}
}
private InputStream createInputStream() throws FileNotFoundException
{
InputStream stream = new FileInputStream(name.getText());
stream = new BufferedInputStream(stream);
//_this_ is a JPanel GUI component
stream = new ProgressMonitorInputStream(
this, "This is gonna take a while", stream);
return stream;
}
|
Analysis of the Java language Decorator
From looking at the example, it may seem that nothing could be
easier than using the Decorator pattern. However, don't forget that
to make the example work, Sun implemented InputStream, FilterInputStream, and ProgressMonitorInputStream -- a non-trivial
amount of code.
In the example, the concern of monitoring crosscuts the InputStream. More generally, the decoration
crosscuts the decoration target. Further, the decoration concern may
cut across the application. For example, users could demand a
ProgressMonitor for all file inputs. (Lest you
think this is an artificial example, ask yourself how many times
you've used an input stream without buffering it.)
Now let's look at the remaining criteria:
- Understanding: Once you know Decorator is at work, it's
fairly easy to comprehend. But I'll never forget the confusion I felt
the first time I cracked open
java.ioand tried to make sense of the wealth of machinery classes that made up Decorator as applied to streams. Although a quick tutorial could have easily set me straight, there was no easy way to arrive at a comprehension of the pattern just from looking at the code. A more concrete measure of comprehension burden is lines of code. I'll take a look at line counts after examining the AspectJ implementation. It's also worth noting that, because it uses wrapping, Decorator suffers from the same object identity issues that affect Adapter. - Reusing: To reuse this pattern implementation, you must
reimplement it.
- Maintaining: There are a couple of key maintenance
scenarios. In the first, you add a new decorator to an existing
implementation. Depending on the number of methods in the Decorator,
this could be tedious, but it's certainly not hard. In the second
scenario, you add a new operation to the
AbstractComponent(that is,InputStream). This means updating all of the existing decorators to reflect the new operation and making a decision at each one whether the decoration should apply to the new method. - Composing: Because the decorators and the component share a common interface, Decorator allows for transparent composition of decorators on a given instance. (Just look at Listing 2, where the code buffers and monitors the input stream.) This is great, especially because the decoration targets do not have to be aware of being decorated.
In their paper, Hanneman and Kiczales state that
Using AspectJ, the implementation of some patterns completely disappears, because AspectJ language constructs implement them directly. This applies to [Decorator].
Taking a look at the Motivation section for Decorator in the GoF book, it becomes obvious why this would be the case:
The decorator forwards requests to the component and may perform additional actions (such as drawing a border) before or after forwarding. Transparency lets you nest decorators recursively, thereby allowing an unlimited number of additional responsibilities.
What is advice after all, but the ability to transparently add additional "actions" to any operation? In a sense, AspectJ lets you decorate any method. To see how this plays out in a real system, you can examine the monitoring of input stream reads in AspectJ.
Identifying decorated operations
To correctly implement monitoring, the aspect must identify read operations on the stream. You do this by writing a pointcut that picks out read operations:
pointcut arrayRead() :
call(public int InputStream+.read(..));
|
Now you can apply some advice of the following general form:
after() returning (int bytesRead) :
arrayRead()
{
updateMonitor(bytesRead);
}
|
This advice uses the returning form to
expose the return value of the method call. The number of bytes read
is then fed to a private method on the aspect: updateMonitor(). This method takes care of the
details of updating the actual ProgressMonitor (more on this later).
The solution so far is simple. However, it turns out that the ProgressMonitor class requires a few more things
to do its job. Specifically, it needs a GUI component to tie
the monitoring dialog to. You saw this requirement in the traditional
implementation:
//this is a JPanel GUI component stream = new ProgressMonitorInputStream(this, "This is gonna take a while", stream); |
To obtain the GUI component it needs, the aspect must bind it in
the pointcut so that it can be used by the advice. Listing 3 contains
the revised pointcut and advice. Notice that the fromAComponent() pointcut makes use of the
primitive cflow() pointcut. In essence, the pointcut is
saying "select all join points that occur as a result of
the execution of a method on a JComponent
and expose that component for use in advice."
Listing 3. Ferrying context to the monitor using cflow
pointcut arrayRead(JComponent component, InputStream is) :
call(public int InputStream+.read(..)) && target(is)
&& fromAComponent(component);
pointcut fromAComponent(JComponent component) :
cflow(execution(* javax.swing.JComponent+.*(..))
&& this(component));
after(JComponent component, InputStream is) returning (int bytesRead) :
arrayRead(component, is)
{
updateMonitor(component, is, bytesRead);
}
|
To make the aspect widely applicable (and to accurately
mimic the other implementation), the aspect must maintain state. That
is, it should pop up a unique progress monitor per monitored stream.
AspectJ offers several options for dealing with this. The best choice
for this aspect is probably to maintain per-object state storage using
a Map. This technique will reappear in my
implementation of the Observer pattern, so take note! (Other ways to
store state specific to an object include intertype declarations
and pertarget/perthis aspects, but considering these concepts is
beyond the scope of this article.)
To implement state storage, you first declare a WeakHashMap that takes a stream as a key and
stores a monitor as a value. You want to use a WeakHashMap because WeakHashMaps will not prevent their keys from
being garbage collected if the keys are no longer in ordinary use.
This best-practice prevents the aspect from holding references to
defunct objects and thereby creating a memory-leak.
The updateMonitor() method then uses the
map to lazily initialize a new IncrementMonitor. Once the method is sure the
monitor exists, it updates it with the latest progress (indicated by
the return value of read()). Listing 4 shows the code for the lazy initialization and progress updates, as well as
the full code for IncrementMonitor:
Listing 4. Lazy initialization of per-stream monitor
//From the aspect...
private void updateMonitor(JComponent component, InputStream is,
int amount){
IncrementMonitor monitor =
(IncrementMonitor)perStreamMonitor.get(is);
if(monitor == null){
monitor = initMonitor(is, component);
}
monitor.increment(amount);
}
private IncrementMonitor initMonitor(InputStream is,
JComponent component){
try {
int size = is.available();
IncrementMonitor monitor =
new IncrementMonitor(component, size);
perStreamMonitor.put(is, monitor);
return monitor;
} catch (Exception e) {
//...handle
}
}
}//...end aspect
public class IncrementMonitor extends ProgressMonitor{
private int counter;
public IncrementMonitor(Component component, int size){
super(component, "Some Title", null, 0, size);
}
public void increment(int amount){
counter += amount;
setProgress(counter);
}
}
|
Finally, the aspect needs to discard the monitor when the stream
has been fully read. If you're thinking in aspects at the moment,
you'll recognize the opportunity. InputStream
conveniently defines a close() method for the aspect to advise, as shown here:
before(InputStream is):
call(public void InputStream+.close())
&& target(is)
{
System.out.println("Discarding monitor.");
perStreamMonitor.remove(is);
}
|
At this point, you've completed the exercise. If you're familiar
with the implementation of InputStream,
however, you'll have spotted something I deliberately left out. The
read() method (no parameters) must be
handled differently from the other readmethods because its return value
is not the number of bytes read, but rather the next byte in the
stream. The example code that accompanies this article expands the
aspect to deal with this constraint, but I urge you to visualize how
you would address this concern if you were writing the aspect before
referring to the code.
Analysis of the AspectJ Decorator
Like Adapter, the two implementations of Decorator differ in
localization. In the AspectJ version, the entire pattern sits neatly
in a single aspect. (I've excluded the IncrementMonitor helper class because it does not
play a structural role in the pattern.) In the Java language version,
the base pattern implementation (leaving aside the client code)
requires three classes. What effects does this have?
- Understanding: Because of the power of AspectJ's pointcut
language, an aspect can affect multiple operations with the same
advice. In contrast, a decorator class must repeat the behavior at
each operation. In part because of this, Sun's implementation shows
more than twice as many lines of code as the aspect implementation. I
counted approximately 110 lines for the
ProgressMonitorInputStreamand 40 forFilterInputStream(I'll leave outInputStreamsince it could be a legitimate superclass absent the Decorator pattern). In contrast, theMonitorFileReadsaspect consumes 53 lines, and theIncrementMonitorhelper class consumes 12. The line ratio stands at 160 to 65, or about 2.4 to 1. Although lines-of-code (LOC) is a crude measure, in general shorter code is clearer code.
Furthermore, if you are familiar with AOP, the AspectJ solution does not give you the sense that something special is going on. The Java language solution requires several classes working carefully in concert, while the AspectJ version looks as if it's doing what most aspects do: adding behavior to a set of join points through advice.
Finally, it's worth remembering that a frequent criticism of AOP is that you can "no longer tell what a module is doing by reading the source." If you apply decorators to objects without the help of aspects, there's no source-based clue in either the client code (other than the wrapping location) or the decoration target (theFileInputStream) that the object displays additional behavior. In contrast, if you examine the GUI from Listing 2 in AJDT, you will see a friendly annotation on the linewhile (in.read(b) != -1)that indicates that the monitoring aspect affects the read call. The combination of AspectJ and its development environment provides better information in this case than the original implementation. - Reusing: Because decoration is built into the language,
almost all aspects reuse this pattern. A more specific reuse would
be to make the monitoring aspect abstract and allow subaspects to
specify a pointcut for monitored operations. In this way, nearly any
object could be decorated with monitoring -- without the preparation
required by the traditional implementation. (If you're wondering about
abstract aspects, Part 2 of the article explains their use in more
detail.)
- Maintaining: Adding a new decoration to an object requires
no special effort. If the decoration target changes (imagine a new
type of
readmethod), then you must (possibly) update the pointcuts to account for this. Having to update a pointcut is burdensome, but the burden can be reduced by writing robust pointcuts that are likely to catch new operations. (See the Resources for a link to a great blog entry on robust pointcuts.) In any case, updating a pointcut seems less trouble than updating all the decorators as would be required for a similar change in the Java-language implementation.
Here's another interesting scenario (mentioned earlier in the Java language analysis): monitoring all file reads. With an OO decorator, this means that every class that reads a stream must remember to wrap it in aProgressMonitorInputStream. In contrast, theMonitorFileReadsaspect will monitor reads on any input stream as long as they occur from within the control flow of aJComponent. BecauseProgressMonitoronly ever pops up when the operation is taking longer than a preset threshold, this aspect could transparently ensure that users never get annoyed at having to wait for a file read -- without the need for programmer vigilance. - Composing: Like the competing implementation, the AspectJ version allows for transparent composition of multiple decorators with low effort.
As I've mentioned before, Decorator's chief trick (transparently adding behavior to an operation) is subsumed by the AspectJ language. The only challenge for the AspectJ implementation is how to associate aspectual state (the updated progress monitor) with a specific instance -- the example used a map to make this association. This need to handle the association preserves Decorator as a pattern in AspectJ. Sometimes, when the decoration machinery already exists, it seems easier to use a traditional Decorator -- especially because the pattern does not invade the decorated class. However, if the decoration machinery does not exist, the flexibility and simplicity of the AspectJ implementation make a better choice.
I hope that this tour of two familiar patterns has helped illustrate aspect-oriented mechanisms in practice. As the development community grapples with the ramifications of an emerging paradigm, it can be useful to apply the new technology to old problems -- problems for which good solutions already exist. The exercise can provide a familiar vantage from which to asses the new approach.
So how has it fared so far? While it's not a golden hammer, AspectJ has managed to secure some solid advantages when used to implement traditional OO patterns. These advantages stem from AspectJ's ability to better handle crosscutting concerns. By gathering the code for a pattern into a single aspect, AspectJ makes it easier to understand the pattern from reading the code. Because pattern code does not show up in non-pattern classes (such as the wrapping locations required by Adapter and Decorator) these other classes are also easier to understand. The combination also makes it easier to extend and maintain the system, and even to reuse the patterns elsewhere.
Adapter and Decorator represent medium-complexity patterns. In Part 2 of this article, I'll examine whether aspect-orientation scales to more complex patterns. Specifically, Part 2 tackles Observer, a pattern that involves multiple roles and dynamic relationships. Part 2 also explores aspect-oriented reuse -- the ability to define a pattern or protocol as an abstract aspect and to apply it with an application-specific aspect.
| Name | Size | Download method |
|---|---|---|
| j-aopwork56code.zip | 142 KB | HTTP |
Information about download methods
- Click the Code icon at the top or bottom of this article (or see Download) to download the source code discussed in this article.
- AOP@Work is a year-long series dedicated to helping you
incorporate AOP into your day-to-day Java programming. Don't miss a
single article in the series. See the complete series listing.
- If you need an introduction to AspectJ and AOP, check out the
introductory article "Improve modularity with aspect-oriented programming"
(developerWorks, January 2002).
- For more information on the AspectJ development environment
(AJDT), take a look at and this offering from members of the AJDT
team: " Develop
aspect-oriented Java applications with Eclipse and AJDT"
(developerWorks, September 2004).
- The section on Decorator suggests that you take a look at the
sources for
java.io. Sun provides an implementation: J2SE 1.4.2. - Learn more about the Gang of Four and their book at Hillside.net.
- The Aspect-Oriented
Design Pattern Implementations project has so far refactored 23
GoF patterns using AspectJ; three of them are studied in this article.
- Project authors Jan Hannemann and Gregor Kiczales have made
reusable abstract aspects from 13 of the studied patterns freely
available under the Mozilla
Public License.
- For a more in-depth study, see Hannemann and Kiczales's "Design Pattern Implementation in Java AspectJ" (OOPSLA,
November 2002), which proposes that AspectJ implementations of the GoF
design patterns show modularity improvements in 17 of 23 cases.
- Adrian Colyer's The Aspects Blog brings you up to the
minute news from the AspectJ project combined with penetrating design
insights and practical applications of aspect technology. This entry
discusses how to write robust pointcut expressions .
- The Portland Pattern
Repository is an excellent resource for learning about patterns,
as well as a great introduction to the patterns community. It includes
sections on the Adapter, Decorator, and Observer patterns.
- You'll find articles about every aspect of Java programming in
the developerWorks Java technology
zone.
- Browse for books on these and other technical topics.
- Also see the Java technology zone tutorials page for a complete Listing of
free Java-focused tutorials from developerWorks.
Nicholas Lesiecki is a recognized expert on AOP in the Java language. In addition to coauthoring Mastering AspectJ (Wiley, 2003), Nick is a member of AspectMentor, a consortium of experts in aspect-oriented software development. He has spoken about applying AspectJ to testing, design patterns, and real-world business problems in such venues as SD West, OOPSLA, AOSD, and the No Fluff Just Stuff symposium series. He currently serves Google as a Software Engineer and Programming Instructor.



