|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Build highly decoupled systems with the power of static
crosscutting
Andrew
Glover (mailto:aglover@vanwardtechnologies.com?cc=&subject=AOP
banishes the tight-coupling blues) CTO, Vanward Technologies 18
February 2004
Many Java developers have embraced the non-intrusive style
and flexibility of aspect-oriented programming (AOP), particularly when
it comes to building highly decoupled and extensible enterprise systems.
In this article, you'll see for yourself how one of AOP's functional
design concepts -- static crosscutting -- can turn what might be a
tangled mass of tightly coupled code into a powerful, extensible
enterprise application.
Aspect orientation is a powerful principle to draw upon during the
fast-paced cycle of transforming business requirements into software
features. By shifting the primary design focus away from the traditional
hierarchical nature of object-oriented programming (OOP), AOP and design
principles allow software architects to contemplate design in a
horizontal, yet complementary, manner to object orientation.
In this article, you'll learn how to implement one of the most
underused features of AOP. Crosscutting is a relatively simple design and
programming technique that packs a hearty punch, particularly when it
comes to building loosely coupled, extensible enterprise systems. While
dynamic crosscutting -- in which the runtime behavior of objects can be
altered -- is considered one of the foundations of AOP, static
crosscutting is a far less known technique. In this article, I'll attempt
to remedy that. I'll start with an overview of both dynamic and static
crosscutting, and then move quickly into an implementation scenario that
demonstrates the latter technique. You'll see for yourself how handily
static crosscutting meets one of the most common enterprise challenges:
how to keep an application's codebase flexible while leveraging
third-party code.
Please note that although I will begin with a brief, conceptual
overview of aspect-oriented programming, this article is not intended as
an introduction to AOP. See the Resources section for a listing of introductory
articles on this topic.
Overview of AOP The
fundamental beauty of object-oriented design lies in its ability to model
real-world domain entities and their respective behavior as abstract
objects. Systems designed in an object-oriented manner yield effective
business objects, such as Person s, Account s,
Order s, and Event s. The downside of
object-oriented design is that such business objects can become muddied
with mixed properties and operations that are incongruent with the
object's original intent.
Aspect-oriented programming effectively addresses this problem by
allowing designers to add behavior to objects in a non-obtrusive, clean,
modularized manner through the use of dynamic and static crosscutting.
What is
crosscutting? Crosscutting is a term
specific to aspect-oriented programming. It refers to the act of cutting
across the established divisions of responsibility (such as logging and
performance optimization) in a given programming model. Within the world
of crosscutting, there are two types: dynamic crosscutting and static
crosscutting. In this article, I am concerned with static crosscutting,
although I will briefly discuss both.
Dynamic
crosscutting Dynamic crosscutting is
the process of creating behavior in an aspect via pointcuts and join points, which can
then be applied laterally, at execution time, to existing objects. Dynamic
crosscutting is commonly used to facilitate the addition of logging or
authentication to various methods in an object hierarchy. Let's take a
minute to learn about some of the concepts at work in dynamic
crosscutting:
- An aspect is analogous to a class in the Java programming
language. Aspects define pointcuts and advices and are compiled by
aspect compilers, such as AspectJ, to interweave crosscuts (both dynamic
and static) into existing objects.
- A join point is a precise point of execution in a program,
such as a method found in a class. For instance, the method
bar() in object Foo could be a join point.
Join point is an abstract notion; one does not actively define a
join point.
- A pointcut is in essence a construct to capture join points.
For instance, one could define a pointcut to capture any call to method
bar() on object Foo . In contrast to join
points, pointcuts are defined in aspects.
- An advice is executable code for a pointcut. A commonly
defined advice is the addition of logging facilities, where a pointcut
captures every call to
bar() on object Foo and
the advice dynamically inserts some logging functionality, such as
capturing bar() 's parameters.
These concepts are central to dynamic crosscutting, although as we'll
see they are not all required for static crosscutting. See Resources to learn more about dynamic
crosscutting.
Static
crosscutting Static crosscutting
differs from dynamic crosscutting in that it does not modify the execution
behavior of a given object. Rather, static crosscutting allows you to
alter the structure of an object
by introducing additional methods, fields, and properties. Moreover,
static crosscutting lets you affix extensions and implementations to the
fundamental structure of an object.
While there are currently no common uses of static crosscutting to
speak of -- it appears to be a relatively unexplored (although powerfully
compelling) feature of AOP -- the implications of the technique are
enormous. With static crosscutting, architects and designers can
effectively model complex systems in a true object oriented manner. Static
crosscutting lets you plug in common behaviors across a system without
having to create deep hierarchies, in essence rendering object models more
elegant and true to their real-life structures.
For the remainder of the article I will focus on the technique and
application of static crosscutting.
Creating static
crosscuts The syntax for creating static crosscuts is quite
different from that of dynamic crosscutting, in that there are no
pointcuts or advices. Given an object (such as Foo , defined
below), static crosscutting makes it quite simple to create new methods,
add additional constructors, and even alter the inheritance hierarchy.
We'll use an example to better see how static crosscutting can be
implemented in an existing class. Listing 1 shows a simple, unaspected
Foo . Listing 1. An unaspected
Foo
public class Foo {
public Foo() {
super();
}
}
|
Adding a new method to an object is as simple as defining one in an
aspect, as shown in Listing 2. Listing 2. Adding a
new method to Foo
public aspect FooBar {
void Foo.bar() {
System.out.println("in Foo.bar()");
}
}
|
Constructors are slightly different in that the new
keyword is required, as shown in Listing 3. Listing 3. Adding a new constructor to Foo
public aspect FooNew {
public Foo.new(String parm1){
super();
System.out.println("in Foo(string parm1)");
}
}
|
Changing the inheritance hierarchy of an object requires a
declare parents tag. For example, in order to become
multi-threaded, Foo would need to implement
Runnable , or extend Thread . Listing 4 shows how
you might use the declare parents tag to change
Foo 's inheritance hierarchy. Listing
4. Changing Foo's inheritance hierarchy
public aspect FooRunnable {
declare parents: Foo implements Runnable;
public void Foo.run() {
System.out.println("in Foo.run()");
}
}
|
At this point, you can probably begin to imagine for yourself the
implications of static crosscutting, particularly with regard to creating
loosely coupled, highly extensible systems. In the sections that follow,
I'll walk you through a real-world design and implementation scenario that
demonstrates the ease with which static crosscutting can be used to extend
the flexibility of your enterprise applications.
The implementation
scenario Enterprise systems are often designed to take
advantage of third party products and libraries. So as not to couple the
entire architecture to the desired product, it is common to include an
abstraction layer in applications designed to interface with outside
vendor code. This abstraction layer furnishes the architecture with a high
degree of flexibility to plug in another vendor's implementation or even
homegrown code with minimal disruption to the system's contiguity.
For this implementation example, let's imagine a system that, upon some
action taken, notifies customers via disparate communication channels. The
example system employs an Email object to represent an
instance of a direct email communication. The Email object
contains properties such as the sender's address, the recipient's address,
a subject heading, and a message body, as shown in Listing 5. Listing 5. The example Email object
public class Email implements Sendable {
private String body;
private String toAddress;
private String fromAddress;
private String subject;
public String getBody() {
return body;
}
public String getFromAddress() {
return fromAddress;
}
public String getSubject() {
return subject;
}
public String getToAddress() {
return toAddress;
}
public void setBody(String string) {
body = string;
}
public void setFromAddress(String string) {
fromAddress = string;
}
public void setSubject(String string) {
subject = string;
}
public void setToAddress(String string) {
toAddress = string;
}
}
|
Incorporating third-party
code Rather than build a custom communication system that
sends e-mails, faxes, SMS messages, etc., the architecture team decides to
incorporate a vendor product that is able to send messages based on
arbitrary objects following a specific convention. The vendor product is
quite flexible and provides a mapping mechanism, via XML, that permits
custom client objects to be mapped to the vendor's specific channel
implementations. The vendor system relies heavily on this mapping file and
the Java platform's reflection abilities to work with normal Java objects.
Embracing flexibility, the architecture team models a
Sendable interface, as shown in Listing 6. Listing 6. The example Sendable interface
public interface Sendable {
String getBody();
String getToAddress();
}
|
Figure 1 shows a class diagram of the Email object and the
Sendable interface.
Figure 1. Class diagram of Email and
Sendable
The design
challenge In addition to the ability to send various message
formats on disparate channels, the vendor offers a hook to allow for
recipient address validation via a provided interface. The vendor's
documentation indicates that any object found to implement this interface
will follow a predefined life cycle, which guarantees the
validateAddress() method will be called and handled correctly
corresponding to the resulting behavior. If validateAddress()
returns false , the vendor's communication system will not
attempt to send the corresponding communication. Listing 7 shows the
vendor's Validatable interface. Listing 7. Address validation for the Sendable
interface
package com.acme.validate;
public interface Validatable {
boolean validateAddress();
}
|
Using fundamental object-oriented design principles, the architecture
team may decide to update the Sendable interface to extend
the vendor's Validatable interface. This decision, however,
would lead to a direct dependency and coupling to the vendor's code. If
the team decides, down the road, to go with another vendor's tool, the
code base would have to be refactored to remove the extends
clause in the Sendable interface and the implemented behavior
in the object hierarchy.
A more elegant and ultimately more flexible solution is to employ
static crosscutting to add the behavior to desired objects.
Static crosscutting to the
rescue Using aspect-oriented principles, the team could
create an aspect that declares the Email object implements
the vendor's Validatable interface; moreover, in the aspect,
the team would then code the desired behavior in the
validateAddress() method. This leads to a nice decoupling of
the code as the Email object does not contain any imports of
the vendor packages, nor does it define a validateAddress()
method. The Email object has no idea it is of type
validatable ! Listing 8 shows the resulting aspect where the
Email object is statically enhanced to implement the vendor's
Validatable interface. Listing 8.
Email validatable aspect
import com.acme.validate.Validatable;
public aspect EmailValidateAspect {
declare parents: Email implements Validatable;
public boolean Email.validateAddress(){
if(this.getToAddress() != null){
return true;
}else{
return false;
}
}
}
|
Let's test it! You
can utilize JUnit to demonstrate that the EmailValidateAspect
actually altered the Email object. In a JUnit test suite, an
Email object can be created with default values and a series
of test cases can verify that Email is indeed an instance of
Validatable ; moreover, a test case can assert that if the
toAddress is null , a call to
validateAddress() will return false .
Additionally, a test case can verify that a non
null toAddress will cause
validateAddress() to return true .
1-2-3, testing with
JUnit You can start by creating a fixture that constructs an
instance of an Email object with simple values. Notice in
Listing 9 that the instance does have a valid (meaning it is not
null ) toAddress value. Listing 9. JUnit setUp()
import com.acme.validate.Validatable;
public class EmailTest extends TestCase {
private Email email;
protected void setUp() throws Exception {
//set up an email instance
this.email = new Email();
this.email.setBody("body");
this.email.setFromAddress("dev@dev.com");
this.email.setSubject("validate me");
this.email.setToAddress("ag@ag.com");
}
protected void tearDown() throws Exception {
this.email = null;
}
//EmailTest continued...
}
|
With a valid Email object,
testEmailValidateInstanceof() ensures the instance is of type
Validatable , as shown in Listing 10. Listing 10. JUnit instanceof check
public void testEmailValidateInstanceof() throws Exception{
TestCase.assertEquals("Email object should be of type Validatable",
true, this.email instanceof Validatable);
}
|
The next test case, shown in Listing 11, purposely sets the
toAddress field to null and then verifies that
the call to validateAddress() returns false .
Listing 11. JUnit null toAddress check
public void testEmailAddressValidateNull() throws Exception{
//force a false
this.email.setToAddress(null);
Validatable validtr = (Validatable)this.email;
TestCase.assertEquals("validateAddress should return false",
false, validtr.validateAddress());
}
|
The last step is for sanity's sake: the
testEmailAddressValidateTrue() test case calls
validateAddress() with the Email instance's
initial values where the toAddress field was set to
ag@ag.com. Listing 12. JUnit non null toAddress
check
public void testEmailAddressValidateTrue() throws Exception{
Validatable validtr = (Validatable)this.email;
TestCase.assertEquals("validateAddress should return true",
true, validtr.validateAddress());
}
|
Refactoring the
example The architecture team worked hard to abstract the
communication implementations with the Sendable interface;
however, the team's first attempt seemingly ignored this interface. Taking
the lessons learned with static crosscutting the Email
object, the team further refines its strategy by moving the contractual
behavior up the hierarchy into the Sendable base interface.
The new aspect creates an extension of the Sendable
interface with the vendor's Validatable interface.
Furthermore, the implemented behavior is then created in the aspect. This
time, the validateAddress() method is defined for another
communication object: Fax , as shown in Listing 13. Listing 13. A better aspect
import com.acme.validate.Validatable;
public aspect SendableValidateAspect {
declare parents: Sendable extends Validatable;
public boolean Email.validateAddress(){
if(this.getToAddress() != null){
return true;
}else{
return false;
}
}
public boolean Fax.validateAddress(){
if(this.getToAddress() != null
&& this.getToAddress().length() >= 11){
return true;
}else{
return false;
}
}
}
|
Never stop refactoring You may
note that the aspect in Listing 13 suffers slightly in that all
implementers of Sendable have their
validateAddress() method defined in a single aspect.
This could easily lead to code bloat. Additionally, altering the
static structure of an interface can have undesirable side effects
if not approached with caution: One must ensure that all
implementers of the target interface are found. So the lesson here
is simple: Never stop refactoring. |
Conclusion While the
API example is contrived, it has hopefully demonstrated how simple it can
be to apply static crosscutting in an enterprise architecture. Static
crosscutting is particularly effective when it comes to the type of
scenario described here (where it can be used to unobtrusively alter an
object's behavior, and even its definition), but it has many other uses.
For example, you might use static crosscutting to "EJB-ify" POJOs (plain
old Java objects) upon deployment; or you might use it in business objects
to leverage the lifecycle interfaces of persistence frameworks such as
Hibernate (see Resources).
Whatever the application, static crosscutting provides an elegant
solution to many of the ailments that can render enterprise code
ineffective. With this article you have learned the basics of the
technique and one of its most fundamental uses. See Resources to learn more about aspect-oriented
programming and different crosscutting techniques.
Resources
- Download the source
code used in this article.
- You can download AspectJ and its associated tools from eclipse.org/aspectj. The site also hosts an FAQ,
mailing lists, excellent documentation, and links to other resources on
AOP. It's a good place to begin further research.
- AspectWerkz is dynamic, lightweight and
high-performant AOP/AOSD framework for the Java platform.
- The Eclipse IDE features an AspectJ plugin.
- For a comprehensive source of information about aspect-oriented
software development, try AOSD.net.
- The JBoss
team has created an interesting AOP framework.
- Hibernate is a powerful, ultra-high-performance
object/relational persistence and query service for the Java
platform.
- Codehaus is a great repository of interesting open
source projects, including AspectWerkz and Nanning (another Aspect implementation for the Java
platform).
- Visit the Developer
Bookstore for a comprehensive listing of technical books, including
Aspect-Oriented Programming with AspectJ by
Ivan Kiselev (Sams Publishing, 2002) and AspectJ
in Action by Ramnivas Laddad (Manning Publishing, 2003), as well
hundreds of other Java-related titles.
- You'll find articles about every aspect of Java programming in the
developerWorks Java
technology zone.
- Also see the Java
technology zone tutorials page for a complete listing of free
Java-focused tutorials from developerWorks.
About the
author Andrew Glover is the CTO of Vanward Technologies, a Washington, DC, metro
area company specializing in the construction of automated testing
frameworks, which lower software bug counts, reduce integration and
testing times, and improve overall code
stability. |
|
|