 | Level: Intermediate Nicholas Lesiecki (ndlesiecki@apache.org), Software engineer/Programming instructor, Google
17 May 2005 Design patterns have long been part of the experienced developer's tool chest. Unfortunately, because patterns can affect multiple classes, they can also be invasive and hard to (re)use. In this two-part article, the third in the AOP@Work series, Nicholas Lesiecki shows you how AOP solves this problem by fundamentally transforming pattern implementation. He examines three classic Gang of Four (GoF) design patterns (Adapter, Decorator, and Observer) and discusses the practical and design benefits of implementing them with aspect-oriented techniques. 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.
 |
About this series
The AOP@Work series is intended for developers who
have some background in aspect-oriented programming and want to
expand or deepen what they know. As with most developerWorks
articles, the series is highly practical: you can expect to come away
from every article with new knowledge that you can put immediately to
use.
Each of the authors contributing to the series has been selected for
his leadership or expertise in aspect-oriented programming. Many of the
authors are contributors to the projects or tools covered in the series.
Each article is subjected to a peer review to ensure the fairness and
accuracy of the views expressed.
Please contact the authors individually with comments or questions
about their articles. To comment on the series as a whole, you may
contact series lead Nicholas
Lesiecki. See Resources
for more background on 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.
 |
What is crosscutting?
Programs often exhibit behavior that does not fit naturally into a
single program module, or even several closely related program
modules. The aspect community describes this type of behavior as
crosscutting because it cuts across the typical divisions of
responsibility in a given programming model. In OO
programming, for instance, the natural unit of modularity is the
class, and a crosscutting concern is a concern that spans multiple
classes. Thus, if a design pattern contributes behavior to three
otherwise distinct classes, that design pattern can be said to
crosscut those classes. Crosscutting leads to code scattering (related
code does not localize with other related code) and code tangling
(related code sits next to unrelated code). This scattering and
tangling makes it difficult to reason about the system. For a
refresher on AOP concepts like crosscutting, see the Resources section.
|
|
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.
A framework for analysis
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 Adapter pattern
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.
A Java-language Adapter
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?
 |
An AspectJ Adapter
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.
 |
Modifying third party code
Astute readers probably recall that in the setup for the example, I
mentioned that one of the sensors came from a third-party vendor and
that this would prevent you from easily adding a method to the class
in the Java-language implementation. In contrast, the AspectJ compiler
provides the ability to affect compiled bytecode in order to allow
developers to apply aspects to a wider range of classes. Adding a
compilation step in which the SensorAdapter
aspect was woven into the third-party jar would let you use the
adapted version of the class seamlessly.
Some developers may worry about the ramifications of modifying
bytecode without possessing the corresponding source code. In some
cases, this sort of modification violates license terms (for
example, it's widely understood that modifying classes provided by
Sun Microsystems violates the corresponding license). In other cases,
creating invasive modifications to a class without a solid
understanding of its internals could introduce subtle bugs. In this
case, however, the modifications serve only to supplement the
functionality of the target class and interact only with its public
interface. Assuming that the provider of RadiationDetector does not forbid modification to
its classes, there's no more reason to be concerned about the AOP
version of Adapter than there is about the OOP version.
|
|
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.
The Decorator pattern
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.
A Java language Decorator
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.io and
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.
 |
An AspectJ Decorator
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).
Exposing relevant context
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);
}
|
Maintaining state
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
ProgressMonitorInputStream and 40 for FilterInputStream (I'll leave out InputStream since it could be a legitimate
superclass absent the Decorator pattern). In contrast, the MonitorFileReads aspect consumes 53 lines, and
the IncrementMonitor helper 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 (the FileInputStream) 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 line while (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
read method), 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 a ProgressMonitorInputStream. In contrast, the
MonitorFileReads aspect will monitor reads
on any input stream as long as they occur from within the control flow
of a JComponent. Because ProgressMonitor only 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.
Conclusion to Part 1
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.
Download | Name | Size | Download method |
|---|
| j-aopwork56code.zip | 142 KB | HTTP |
Resources - 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.
About the author  | |  | 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. |
Rate this page
|  |