Part 1 - Overview and Process
December 2003
Introduction
In the previous article of this series, we examined the fundamentals of aspect-oriented
refactoring (AO refactoring), the general schemes and considerations involved,
and the process through a simple example. By now, you should have a clear idea
about AO refactoring. In this second and concluding part of the series, I will
present several AO refactoring techniques.
We will examine AO techniques to refactor exception handling, concurrency control,
argument trickle, worker object creation, interface implementation, overridden
methods, lazy initialization, and contract enforcement. For the sake of brevity,
we will focus on before-and-after code affected by refactoring and not so much
on the steps leading to it. The process for each of the techniques is similar
to the approach we presented in the first article. Further, I assume that all
relevant conventional refactoring has been applied before we start AO refactoring.
This avoids repeating information covered elsewhere (see “Resources”)
and shows that AO refactoring indeed augments, and not replaces, conventional
refactoring.
Many of the AO refactoring techniques presented in this article are useful
in implementing crosscutting concerns at a wider scope. The difference, however,
is in the emphasis placed on certain principles as outlined in the “Peculiarities
of AO refactoring” section in part 1 of this series. In practice, it is
very common to start by writing an aspect to refactor a class, then using the
aspect to refactor multiple classes, and eventually modifying it to implement
crosscutting in a system-wide fashion.
There are many techniques to cover. Let’s dive right in!
Extract exception handling
Exception handling is a crosscutting concern that affects most nontrivial classes.
Due to the structure of exception handling code (try/catch blocks), conventional
refactoring cannot perform further extraction of common code. Each class (or
a set of classes) may have its own way to handle exceptions encountered during
execution of its logic. With AO refactoring, you can extract exception handling
code in a separate aspect.
We will illustrate the technique through an exception handling scheme, where
handlers throw a new exception converting the caught exception to another type.
It is possible to extend this example to other exception handling techniques
such as logging and rethrowing, aborting transactions, and reattempting the
operation.
Consider the Business Delegate J2EE pattern (see “Resources”).
Almost every method in a business delegate class catches exceptions thrown by
the underlying implementation and rethrows an application-specific exception.
Implementing this exception handling scheme requires a try/catch block in each
method. In each of the catch blocks, you create and throw a new exception that
wraps the caught exception, after performing other tasks such as logging and
rolling back the current transaction. The same situation occurs in many other
design patterns such as Data Access Object and Service Locator.
Consider Listing 1 where class LibraryDelegate
is using the Business Delegate pattern. There is duplicated logic present
in almost all methods.
Listing 1: LibraryDelegate.java before refactoring
package
library;
... imports
public class
LibraryDelegate {
private LibrarySession
_session;
public LibraryDelegate()
throws LibraryException {
init();
}
private void
init() throws LibraryException
{
try
{
LibrarySessionHome
home
=
(LibrarySessionHome)ServiceLocator.getInstance().
getRemoteHome("Library", LibrarySessionHome.class);
_session
= home.create();
} catch
(ServiceLocatorException ex) {
throw
new LibraryException(ex);
} catch
(CreateException ex) {
throw
new LibraryException(ex);
} catch
(RemoteException ex) {
throw new LibraryException(ex);
}
}
... session management: reconnection,
get/set current library etc with
... identical exception handling code
public LibraryTO
getLibraryDetails() throws LibraryException
{
try
{
return
_session.getLibraryDetails();
} catch
(RemoteException ex) {
throw
new LibraryException(ex);
}
}
public void
setLibraryDetails(LibraryTO to) throws
LibraryException {
try
{
_session.setLibraryDetails(to);
} catch
(RemoteException ex) {
throw
new LibraryException(ex);
}
}
public void
addBook(BookTO book) throws LibraryException
{
try
{
_session.addBook(book);
} catch
(RemoteException ex) {
throw
new LibraryException(ex);
}
}
public void
removeBook(BookTO book) throws
LibraryException {
try
{
_session.
removeBook(book);
} catch
(RemoteException ex) {
throw
new LibraryException(ex);
}
}
... other methods for adding/removing
patrons, checkin/out books,
... get all books etc. with identical exception handling
code
}
There isn’t any conventional
refactoring technique to extract repeated try/catch blocks and the code associated
with each. To refactor out all the exception handling logic, we write the following
aspect. (Due to a bug in AspectJ 1.1.1, the current implementation, we cannot
make this aspect a nested aspect. Therefore, we do the next best thing –
make it a peer aspect):
aspect
LibaryExceptionHandling {
declare soft
: RemoteException
: call(*
*.*(..) throws RemoteException)
&& within(LibraryDelegate);
declare soft
: ServiceLocatorException
: call(*
*.*(..) throws ServiceLocatorException)
&& within(LibraryDelegate);
declare soft
: CreateException
: call(*
*.*(..) throws CreateException)
&& within(LibraryDelegate);
after() throwing(SoftException
ex) throws LibraryException
: execution(*
LibraryDelegate.*(..) throws
LibraryException)
&& within(LibraryDelegate)
{
throw
new LibraryException(ex.getWrappedThrowable());
}
}
In the aspect, each declare soft statement in LibaryExceptionHandlingAspect
causes any exception of the specified types (RemoteException,
ServiceLocatorException,
CreateException) thrown
during a call matching the specified pointcut to be treated as a runtime exception.
When such an exception is thrown, it is wrapped in a SoftException,
which is a runtime exception. The after throwing advice catches any SoftException
thrown and throws a new LibraryException
wrapping the original exception obtained by calling getWrappedThrowable()
on the caught exception. Now we can take out all the exception handling from
the core implementation. Listing 2 shows the LibraryDelegate.java after applying
the “Extract exception handling” refactoring. Note that while the
pointcuts in the above aspects heavily use wildcards, you would arrive at such
a definition by following the process described in the first article of the
series.
Listing 2: LibraryDelegate.java after refactoring
package library;
... imports
public class
LibraryDelegate {
private LibrarySession _session;
public LibraryDelegate()
throws LibraryException {
init();
}
private void
init() throws LibraryException
{
LibrarySessionHome home
= (LibrarySessionHome)ServiceLocator.getInstance().
getRemoteHome("Library", LibrarySessionHome.class);
_session = home.create();
}
... session management: reconnection,
get/set current library etc.
public LibraryTO
getLibraryDetails() throws LibraryException
{
return
_session.getLibraryDetails();
}
public void
setLibraryDetails(LibraryTO to) throws
LibraryException {
_session.setLibraryDetails(to);
}
public void addBook(BookTO
book) throws LibraryException
{
_session.addBook(book);
}
public void
removeBook(BookTO book) throws
LibraryException {
_session. removeBook(book);
}
... other methods for adding/removing
patrons, checkin/out books,
... get books etc.
}
aspect LibaryExceptionHandling
{
declare soft
: RemoteException
: call(*
*.*(..) throws RemoteException)
&& within(LibraryDelegate);
declare soft
: ServiceLocatorException
: call(*
*.*(..) throws ServiceLocatorException)
&& within(LibraryDelegate);
declare soft
: CreateException
: call(*
*.*(..) throws CreateException)
&& within(LibraryDelegate);
after() throwing(SoftException
ex) throws LibraryException
: execution(*
LibraryDelegate.*(..) throws
LibraryException)
&& within(LibraryDelegate)
{
throw
new LibraryException(ex.getWrappedThrowable());
}
}
This is clearly a substantial saving in code and the core code is cleaner.
The aspect has localized exception handling, thus simplifying modification to
exception handling policy. For example, it is easy to add logging by modifying
the advice.
One refactoring technique often leads to another. The refactoring described
next often starts as a combination of “extract method calls” and
“extract exception handing”.
Extract concurrency control
Concurrency control is often a critically important, but equally hard to implement,
crosscutting concern. Besides being tough to understand, a concurrency control
implementation requires the code to be scattered over many methods. There are
a few concurrency control patterns available, and although the concepts are
reusable, their implementations aren’t. AOP offers reusable implementations
of those patterns, thus taking some pain out of concurrency control implementation.
Here is a simple example in Listing 3 that uses the read-write lock pattern
(adapted from “AspectJ in Action”). The concurrency pattern requires
managing two kinds of lock: read lock and write lock. Upon entering each method
that only reads data, the read lock is acquired and that lock is released before
leaving the method. The write lock is acquired and released in a similar manner
for each method that modifies data.
Listing 3: Account.java before refactoring
import
EDU.oswego.cs.dl.util.concurrent.*;
public class Account {
...
private ReadWriteLock
_lock = new ReentrantWriterPreferenceReadWriteLock();
... constructors etc.
public void
credit(float amount) {
try
{
_lock.writeLock().acquire();
... business
logic for credit operation ...
} catch
(InterruptedException ex) {
throw
new InterruptedRuntimeException(ex);
} finally
{
_lock.writeLock().release();
}
}
public void
debit(float amount) throws
InsufficientBalanceException {
try
{
_lock.writeLock().acquire();
... business
logic for debit operation ...
} catch
(InterruptedException ex) {
throw
new InterruptedRuntimeException(ex);
} finally
{
_lock.writeLock().release();
}
}
public float
getBalance() {
try
{
_lock.readLock().acquire();
... business
logic for getting the current balance ...
} catch
(InterruptedException ex) {
throw new InterruptedRuntimeException(ex);
} finally
{
_lock.readLock().release();
}
}
public void
setBalance(float balance) {
try
{
_lock.writeLock().acquire();
... business
logic for setting the current balance ...
} catch
(InterruptedException ex) {
throw new InterruptedRuntimeException(ex);
} finally
{
_lock.writeLock().release();
}
}
... more methods
public String
toString() {
try
{
_lock.readLock().acquire();
... form
the description string ...
} catch
(InterruptedException ex) {
throw new InterruptedRuntimeException(ex);
} finally
{
_lock.readLock().release();
}
}
}
The solution uses a reusable ReadWriteLockSynchronizationAspect
aspect (see source). The aspect declares two abstract pointcuts: readOperations()and
writeOperations(), and advises
them to manage read and write lock. The aspect uses a perthis
association to maximize code reuse. However, the details of the perthis
aspect association are beyond the scope of this article. Note that the reusable
base aspect may be a result of following the “Refactor the refactoring
aspect” step as outlined in the part 1 of this series.
We refactor the concurrency control code for the Account class in a nested
aspect. This aspect is a concrete subaspect of ReadWriteLockSynchronizationAspect.
We provide the definition for abstract pointcuts: readOperations()and
writeOperations(). In
the Account class’ case (after going through the process described in
part 1 of this series), we define read operations as all methods with their
name starting in “get” as well as the toString()
method. We consider every other method as a write operation.
static
aspect
ConcurrencyControlAspect
extends ReadWriteLockSynchronizationAspect
{
public pointcut
readOperations()
: (execution(*
Account.get*(..)) || execution(*
Account.toString(..)))
&& within(Account);
public pointcut
writeOperations()
: (execution(*
Account.*(..)) && !readOperations())
&& within(Account);
}
}
Listing 4 shows the Account.java file after applying refactoring to encapsulate
concurrency control in the nested aspect. The Account class looks like the following:
Listing 4: Account class after refactoring
public
abstract class
Account {
...
... no _lock variable here (compared to Listing 3)
... constructors etc.
public void
credit(float amount) {
... business logic for credit operation
...
}
public void
debit(float amount) throws InsufficientBalanceException
{
... business logic for debit operation
...
}
public float
getBalance() {
... business logic for setting the
current balance ...
}
public void
setBalance(float balance) {
... business logic for getting the
current balance ...
}
... more methods
static aspect
ConcurrencyControlAspect
extends
ReadWriteLockSynchronizationAspect {
public
pointcut readOperations()
: (execution(*
Account.get*(..)) || execution(*
Account.toString(..)))
&&
within(Account);
public
pointcut writeOperations()
: (execution(*
Account.*(..)) && !readOperations())
&& within(Account);
}
}
The refactoring saves a good amount of code, ensures consistently acquiring
and releasing the right locks, and isolates details of the read-write lock pattern.
As a bonus, you can easily change implementations. For example you can use a
simpler scheme that surrounds each read and write operation in a synchronized
block. All you need to do is change the base aspect. The change is highlighted
in the code.
public
abstract class
Account {
... core implementation unchanged from Listing 4 ...
static aspect
ConcurrencyControlAspect
extends
SimpleSynchronizationAspect
{
public
pointcut readOperations()
: (execution(*
Account.get*(..)) || execution(*
Account.toString(..)))
&&
within(Account);
public
pointcut writeOperations()
: (execution(*
Account.*(..)) && !readOperations())
&& within(Account);
}
}
The reusable base aspect SimpleSynchronizationAspect
(see source) simply surrounds each join point captured by either of the pointcuts
in a synchronized block. The use of AOP created separation of concerns between
the Account implementation and concurrency control implementation, which allowed
such an easy modification.
The techniques described so far use the simple crosscutting constructs. The
use of AOP design patterns help in creating powerful refactoring techniques.
The two techniques make the use of AOP design patterns to extract common code
that crosscuts many methods.
Extract worker object creation
A worker object is an instance of a class that encapsulates a method (called
a worker method) so that the method can be treated like an object. You need
to create worker objects in many situations: executing methods asynchronously,
performing authorization using Java Authentication and Authorization Service
(JAAS) API, implementing thread safety in Swing/SWT applications, and so on.
On each occasion, a lot of extra code is required to create such worker objects.
For each worker method, you need to create a named or anonymous class that encapsulates
the required method call and create an instance of such a class. If you use
named classes, you end up with many classes that just execute a worker method
and if you use anonymous class, the code to create such a class overwhelms the
core logic. The result is hard to understand and maintain code.
We can devise an AO refactoring that uses the worker object creation pattern
to help keep the core implementation simple and to localize the worker creation
and usage logic. While the details of the worker object creation pattern are
beyond the scope of this article, here is a quick summary of the pattern: AspectJ’s
around advice allows proceed()
(which executes the captured join point) to be called from anywhere in the advice
body. The pattern utilizes this fact and calls proceed()
from an anonymous class in the advice. See “Resources” for more
information about this pattern.
Listing 5 shows the ATM class
before refactoring. The use of the JAAS authorization scheme requires executing
the method by passing a worker object to Subject.doAsPrivileged().
Therefore, we use an anonymous class that executes the business logic in its
run() method. We pass
an instance of the anonymous class to Subject.doAsPrivileged()
(in the overall implementation, you must call checkPermission()
in the methods that need an authorization check). See the example in the first
part of the article.
Listing 5: ATM.java before refactoring
... imports
public class ATM {
private Subject _atmSubject;
...
public float
getBalance(final Account account)
throws
BankingException {
PrivilegedAction worker
= new
PrivilegedAction() {
public Object run() {
BankLiaison bl = ...
return new Float(bl.getBalance(account));
}};
Float balance
= (Float)Subject.doAsPrivileged(_atmSubject,
worker, null);
return
balance.floatValue();
}
public void
credit(final Account account,
final float amount)
throws
BankingException {
PrivilegedExceptionAction worker
= new
PrivilegedExceptionAction() {
public
Object run() throws Exception
{
BankLiaison bl = ...
bl.credit(account, amount);
return null;
}};
try
{
Subject.doAsPrivileged(_atmSubject,
worker, null);
} catch
(PrivilegedActionException ex) {
Throwable
cause = ex.getCause();
throw
new BankingException(ex.getCause());
}
}
... identical JAAS authorization for other methods accessing
BankingLiaison
}
Clearly, the code is hard to understand and takes a while to figure out the
business logic buried inside the overwhelming amount of code for the anonymous
classes. We can refactor the authorization logic by using the worker object
creation pattern as shown in the following aspect.
private
static aspect
AuthorizationRouterAspect {
pointcut
authOperations(ATM atm)
: execution(public
* ATM.*(..)) && this(atm)
&& within(ATM);
Object around(final
ATM atm) throws BankingException
: authOperations(atm) {
PrivilegedExceptionAction worker
= new
PrivilegedExceptionAction() {
public
Object run() throws Exception
{
return
proceed(atm);
}
};
try
{
return
Subject.doAsPrivileged(atm._atmSubject, worker, null);
} catch
(PrivilegedActionException ex) {
return
new BankingException(ex);
}
}
}
Listing 6 shows the code for the ATM
class after applying the refactoring.
Listing 6: ATM.java after refactoring
... imports
public class ATM {
private Subject _atmSubject;
...
public float
getBalance(Account account) throws
BankingException {
BankLiaison bl = ...
return
bl.getBalance(account);
}
public void
credit(Account account, float
amount) throws BankingException
{
BankLiaison bl = ...
bl.credit(account, amount);
}
...
private static aspect
AuthorizationRouterAspect {
pointcut
authOperations(ATM atm)
: execution(public
* ATM.*(..)) && this(atm)
&& within(ATM);
Object around(final
ATM atm) throws BankingException
: authOperations(atm)
{
PrivilegedExceptionAction
action
=
new PrivilegedExceptionAction()
{
public
Object run() throws Exception
{
return
proceed(atm);
}};
try
{
return Subject.doAsPrivileged(atm._atmSubject,
action, null);
} catch
(PrivilegedActionException ex) {
return new BankingException(ex);
}
}
}
}
Note that if you route methods that throw a business-specific exception, you
will need additional logic in the aspect to deal with it. For example, from the preceding
aspect, when applied to the debit()
method that throws InsufficientBalanceException,
the client will receive a generic BankingException. However, we will not consider
that issue in this article; for more information, review the exception introduction
pattern (as described in “AspectJ in Action” listed under “Resources”).
So far, we have focused on refactoring common functionality in one class at
a time. The next two refactoring techniques consider a set of related classes
together as the refactoring target.
Replace argument trickle by wormhole
Often, there is a need to pass a part of the current context (current execution/target
object, method arguments etc.) to invoked methods, which in turn pass it along,
so that a called method down the call chain eventually may use the context to
perform its task. The result is API pollution due to a crosscutting concern
and increased cognitive burden for the developer of the classes in the middle
of the chain. This also causes significant problems for reusing components.
With AO refactoring, you can avoid passing the parameters to methods in the
call chain. This is one of the more invasive AO refactoring techniques as it
typically cuts across two or more classes.
Consider fragments of a few classes (Listing 7) in a banking system. The ATM
class utilizes a BankingLiaison
instance (that represents the bank corresponding to the ATM card) which, in
turn, invokes operation on the Account instance. Account statement generation
needs information about the accessed ATM in addition to the account, the amount,
and the operation involved. To facilitate statement generation, ATM’s
methods pass the accessed ATM to the methods of BankLiaison,
which propagate it to the Account class’ methods. The Account class’
methods invoke appendAccountActivities()
which appends activities, most likely, to a database table.
Listing 7: Banking classes before refactoring
public
class ATM {
...
public void
credit(Account account, float
amount) {
BankLiaison bl = ... // agent for
the bank based on the card info
bl.credit(this, account, amount);
}
public void
debit(Account account, float amount)
throws
InsufficientBalanceException {
BankLiaison bl = ... // agent for
the bank based on the card info
bl.debit(this, account, amount);
}
... more operations
}
public class BankLiaison {
...
public void
credit(ATM atm, Account account, float
amount) {
account.credit(atm, amount);
}
public void
debit(ATM atm, Account account, float
amount)
throws
InsufficientBalanceException {
account.debit(atm, amount);
}
... more operations
}
public class Account {
...
public void
credit(ATM atm, float
amount) {
... credit business logic
appendAccountActivities(atm,
this, amount, "credit");
}
public void
debit(ATM atm, float
amount) {
... debit business logic
appendAccountActivities(atm,
this, amount, "debit");
}
... more operations
private static void
appendAccountActivities(ATM atm,
Account account,
float
amount, String operation) {
... update database
}
}
A problem with the above code is that the ATM parameter is trickled through
layers of calls (highlighted in the code). Note that the example uses only three
class levels and one parameter. The problem becomes more acute as the number
of level and call parameters increase.
We will devise an AO refactoring technique based on the Wormhole pattern to help
in this situation. While the details of the pattern are beyond the scope of
this article, essentially it uses two pointcuts – one for the caller that
has the context and one for the callee that needs the context. Then the pattern
creates a wormhole using a cflow()
pointcut and transfers the caller context to the call join point site. See “Resources”
for more information about the wormhole pattern. Note that there is an alternative
using a ThreadLocal
variable, to hold the ATM variable, but the solution that uses the pattern is
cleaner because it modularizes access to the information, rather than using
a global variable. As an aside, even the scheme using a ThreadLocal
variable is more effective when used with aspects because it is modularized!
We refactor the logic of updating the account activities table in a nested aspect
of the Account class. While we show directly the final result of the refactoring
effort, applying the “Extract method calls” prior to utilizing the
wormhole pattern may be a good idea. Here is the aspect that performs the refactoring:
static
aspect UpdateAccountActivitiesTable
{
pointcut atmRequest(ATM
atm)
: execution(*
ATM.*(..)) && this(atm)
&& within(ATM);
pointcut accountOperation(Account
account, float amount)
: (execution(void
Account.credit(float))
|| execution(void
Account.debit(float)))
&& this(account)
&& args(amount) &&
within(Account);
// wormhole
pointcut accountOperationsInAtm(ATM
atm, Account account, float amount)
: accountOperation(account, amount)
&& cflow(atmRequest(atm));
after(ATM atm,
Account account, float amount)
returning
: accountOperationsInAtm(atm, account,
amount) {
appendAccountActivities(atm, account,
amount,
thisJoinPointStaticPart.getSignature().getName());
}
private static void
appendAccountActivities(ATM atm,
Account account,
float
amount, String operation) {
... update database
}
}
The reason we have chosen to implement the aspect as a nested aspect of the
Account class, as opposed to, say, the ATM class, is to avoid an additional
dependency compared to the conventional implementation. A separate top-level
aspect would be a good choice, too. Note that we have moved the appendAccountActivities()
method from Account to the aspect, since only the aspect uses it. Listing 8
shows the banking classes after refactoring.
Listing 8: Banking classes after refactoring
public
class ATM {
...
public void
credit(Account account,
float amount)
{
BankLiaison
bl = ...
// agent for the bank based on the card
info
bl.credit(account, amount);
}
public void
debit(Account account,
float amount)
throws InsufficientBalanceException
{
BankLiaison
bl = ...
// agent for the bank based on the card
info
bl.debit(account, amount);
}
... more operations: debit, getBalance, transfer
}
public
class BankLiaison {
...
public void
credit(Account account,
float amount)
{
account.credit(amount);
}
public void
debit(Account account,
float amount)
throws InsufficientBalanceException
{
account.debit(amount);
}
...
}
public
class Account {
...
public void
credit(float
amount) {
... credit business
logic
}
public void
debit(float
amount) throws
InsufficientBalanceException {
... debit business
logic
}
...
static aspect
UpdateAccountActivitiesTable {
pointcut atmRequest(ATM
atm)
: execution(*
ATM.*(..)) &&
this(atm)
&& within(ATM);
pointcut accountOperation(Account
account, float
amount)
: (execution(void
Account.credit(float))
|| execution(void
Account.debit(float)))
&&
this(account)
&& args(amount)
&& within(Account);
// wormhole
pointcut accountOperationsInAtm(ATM
atm, Account account,
float amount)
: accountOperation(account,
amount) &&
cflow(atmRequest(atm));
after(ATM
atm, Account account,
float amount)
returning
: accountOperationsInAtm(atm,
account, amount)
{
appendAccountActivities(atm,
account, amount,
thisJoinPointStaticPart.getSignature().getName());
}
private static
void
appendAccountActivities(ATM
atm, Account account,
float
amount, String
operation) {
... update database
}
}
}
The ATM, BankingLiaison, and
Account classes no longer have an additional ATM parameter that is used for the
sole purpose of updating the account activity database tables.
So far, we have mostly relied on dynamic crosscutting techniques for AO refactoring.
The next technique illustrates the use of static crosscutting offered by AspectJ
to refactor the existing code.
Extract interface implementation
The conventional refactoring technique of “Extract interface” allows
improved decoupling of clients from implementations. If more than one class
implements that interface, you may end up duplicating code required to implement
the interface. With AOP, you can take the idea further and avoid any duplication.
AO refactoring uses AspectJ’s inter-type declaration mechanism (also known
as introduction). You can write an aspect that introduces the default implementation
into the extracted interface. In essence, AspectJ allows implementing mixins.
This kind of refactoring is especially useful when the implementation of an
interface is mostly boilerplate.
Let’s consider the ServiceCenter interface in Listing 9:
Listing 9: ServiceCenter.java before refactoring
public
interface ServiceCenter
{
public String
getId();
public void
setId(String id);
public String
getAddress();
public void
setAddress(String address);
}
The ATM class implements the ServiceCenter
interface. Listing 10 shows the ATM class before any AO refactoring:
Listing 10: ATM.java before refactoring
public
class ATM extends
Teller implements ServiceCenter
{
private String
_id;
private String
_address;
...
public String
getId() {
return
_id;
}
public void
setId(String id) {
_id = id;
}
public String
getAddress() {
return
_address;
}
public void
setAddress(String address) {
_address = address;
}
...
}
The ATM class contains a very boilerplate implementation of ServiceCenter.
Other classes such as BrickAndMortarBank,
SuperStoreServiceCenter
will be similar – each one repeating the code to implement the ServiceCenter
interface. While you could avoid duplicated code by creating the default implementation
of the interface and make the classes extend the default implementation, the
technique fails for the classes that are already extending another class.
With AO refactoring, we introduce the default implementation into the ServiceCenter
interface using a nested aspect as shown in Listing 11:
Listing 11: ServiceCenter.java after refactoring
public
interface ServiceCenter
{
public String
getId();
public void
setId(String id);
public String
getAddress();
public void
setAddress(String address);
static abstract aspect
IMPL {
private
String ServiceCenter._id;
private
String ServiceCenter._address;
public
String ServiceCenter.getId() {
return
_id;
}
public
void ServiceCenter.setId(String id) {
_id = id;
}
public
String ServiceCenter.getAddress() {
return
_address;
}
public
void ServiceCenter.setAddress(String address) {
_address
= address;
}
}
}
In listing 11, the nested IMPL aspect introduces data members as well as methods
with the default implementation of the ServiceCenter
interface. With this aspect present, any class that implements the interface
automatically inherits the default implementation.
Now we can take out the implementation of the methods declared in ServiceCenter
from the ATM class as shown in Listing 12.
Listing 12: ATM.java after refactoring
public
class ATM extends Teller implements
ServiceCenter {
...
}
The other classes implementing the interface such as BrickAndMortarBank,
SuperStoreBranch can remove the
implementation of the methods declared in the interface, too. The implementing classes
no longer include boilerplate code to implement the interfaces. The implementing
classes, however, can still override the default implementation provided by
the aspects.
There are a few variations of this refactoring technique. First, you may let
the implementing classes choose whether to inherit the default implementation
provided by the aspect. One way to implement this scheme is to create a subinterface
of the original interface, say, ServiceCenterDefaultAspectImpl,
and let the aspect introduce the default implementation to this interface. The implementing
classes will inherit the default implementation only if they declare themselves
to implement ServiceCenterDefaultAspectImpl.
Another variation is to provide only the default implementation for a part of
the interface. While in some cases, such a choice may be a sheer necessity (due
to lack of information needed for implementation), in other cases, this may
be a deliberate design decision to force developers of the classes to think
about the right implementation semantics.
The rest in short
In this article, we have examined a few AO refactoring techniques. We will
conclude with a brief discussion of a few other techniques.
Replace override with advice
It is often required to augment additional common behavior to many methods
of a class. A typical solution is to create a subclass and override methods
to perform some additional logic. For example, you may have a model class without
any support for observer notification. You can add such a support by creating
a subclass and overriding each state-modifying method to notify the observers.
With AO refactoring, you can use an aspect to advise the needed method with
additional logic. For example, instead of overriding, you may simply advise
methods of the subclass that notifies the observers. The implementation is much
like the “Extract method calls” technique presented in the first
part of the article. The difference is that once you extract method calls, the
methods in the subclass simply call the corresponding base class method, and
therefore need not exist in the derived class.
Extract Lazy initialization
Lazy initialization of expensive resources is a common optimization technique.
The conventional solution requires checks for uninitialized resources in every
place that uses those resources and causes code-scattering and code-tangling.
It may seem that you can solve the code-scattering and code-tangling problem
by simply accessing the instance variable through its getter method; however,
there is a caveat: if a portion of the class holding the resource reference
accesses it directly, initialization will not occur and you will experience
undesired consequences. Testing could reveal such a bug, but only if the errant
method is called before another method that leads to resource initialization.
Note that setting private access to a class member will prevent direct access
from other classes, but not from within the class itself. With AO refactoring,
you can advise read access to the resource (using a get() pointcut) to initialize
it.
Extract contract enforcement
Contract enforcements often require duplicated code in many methods of a
class. This is especially true for implementing class invariants and pre- and
post-conditions that are common to many methods. Conventional implementations
require adding identical code – conditional checks and assert statements
– into many methods. With AO refactoring, you can refactor such contract
checks into a separate aspect. This kind of refactoring is much like “Extract
method calls” that we discussed in the first part of this series, except
you typically use assert statements to perform the additional logic.
Conclusion
Aspects that come from refactoring typically build up from affecting a small
portion of a system, often starting with just a single class. While limiting
the scope reduces benefits of those aspects, it also reduces risk of unwanted
changes in system behavior. Over time, refactoring techniques can build up more
broadly scoped aspects. In this article, we examined several techniques with
prototypical examples that a Java developer faces. Initially, you may use the
technique in the scenarios described. Overtime, you will develop an eye for
applying these techniques to different scenarios and you may even uncover newer
techniques.
Refactoring techniques are useful to understand by themselves. However, it
will be much better when IDEs help AO refactoring as they do conventional refactoring.
A simple support for AO refactoring would let programmers choose multiple blocks
of code and the kind of refactoring desired. The IDE could then create a simple
version of an aspect encapsulating the common elements from the selected code.
If appropriate, the programmers could improve the pointcut definitions in the
aspect created by the IDE. An advanced feature might provide hints on capturing
commonalities between the pointcuts. IDEs could also support aspect mining,
using hints supplied by programmer, to find refactoring opportunities (see “Resources”
for existing tools offering such functionality).
Aspect-oriented programming significantly benefits real-world programming.
However, understandably, there is a cautionary approach towards adopting it. A
safe adaptation path for any technology is the one that allows for gradual use.
For AOP, such a path seems to be using development aspects initially, followed
by refactoring using AO techniques, then implementing complex crosscutting concerns,
and finally designing systems with AOP right from the project’s inception.
Such a path minimizes the associated risk, improves the understanding of AOP
fundamentals, creates awareness of issues surrounding it, gives a realization
of its appropriate and inappropriate usages, uncovers recurring design patterns,
and all the while, boosts confidence in AOP.
The benefits of AOP are too real to ignore, and the cautionary approach towards
it is too valid to dismiss. Utilizing a safe path of incorporating development
and AO refactoring aspects help in overcoming this dilemma. First, make yourself
comfortable with AOP using development aspects. Then when you see duplicated
code that cannot be refactored using conventional techniques (you won’t
have to look that hard!), take that opportunity to use AO refactoring. You will
have gained immediate benefits and taken a step towards tapping full power of
AOP.
Acknowledgements
Thanks to Ron Bodkin, Mik Kersten, Gregor Kiczales for reviewing the series
and providing useful feedback.
Author’s bio
Ramnivas Laddad is the author of several articles, papers, and books. His most
recent book, "AspectJ in Action: Practical aspect-oriented programming"
(Manning, 2003), has been labeled as the most useful guide to AOP/AspectJ. Ramnivas
has been developing complex software systems using technologies such as Java,
J2EE, AspectJ, UML, networking, real-time systems, and XML for over a decade.
He is an active member of the AspectJ user community and has been involved with
aspect-oriented programming from its early form. He is also a mentor at AspectMentor,
a consortium of AOP experts who provide assistance with training, consulting,
and mentoring. You can reach him at ramnivas@yahoo.com
References
- Understand conventional refactoring techniques:
Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts, Refactoring:
Improving the Design of Existing Code, Addison Wesley, 1999.
- Know all about AOP and AspectJ, with many examples ranging from simple
logging and policy enforcement to authorization and transaction management.
You can also get detailed information about the AOP patterns used in this
article such as the wormhole pattern, the worker object creation pattern,
and the exception introduction pattern. The examples presented in this book
should provide ideas for AO refactoring:
Ramnivas Laddad, AspectJ
in Action: Practical Aspect-Oriented Programming, Manning, 2003.
TheServerSide.com features two sample chapters from “AspectJ in Action”:
/articles/AspectJreview.tss
- Find more about the fundamental of AOP and AspectJ (based on AspectJ 1.0,
but most information is still applicable):
Ramnivas Laddad, I
want my AOP (part 1-3), JavaWorld, January 2002.
- Understand whys and hows behind AOP:
Gregor Kiczales, Crosscut (monthly column) Software Development magazine.
- Read more about the AspectJ programming language:
Joseph D. Gradecki, Nicholas Lesiecki, Mastering AspectJ: Aspect-Oriented
Programming in Java, John Wiley & Sons, 2003.
- Find more information on J2EE design patterns discussed in this article:
Dan Malks, Deepak Alur and John Crupi, Core J2EE Patterns: Best Practices
and Design Strategies, 2nd edition. Prentice Hall, 2003.
- Read another good source for enterprise design patterns:
Martin Fowler, Patterns of Enterprise Application Architecture, Addison-Wesley,
2002.
- Read more about Doug Lea’s concurrency library. The read-write lock
pattern used on “Extract concurrency refactoring” uses this library:
http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html
- Download AspectJ tools, documentation, and links to plugins for various
IDEs:
http://www.eclipse.org/aspectj
- Download tool that helps to find and manage crosscutting concerns, Aspect
Browser:
http://www.cs.ucsd.edu/users/wgg/Software/AB
- Download Eclipse plugin that helps to explore crosscutting concerns in
an existing system, Feature Exploration and Analysis Tool (FEAT):
http://www.cs.ubc.ca/labs/spl/projects/feat
- Download tool to mine aspects in an existing system, Aspect Mining Tool
(AMT):
http://www.cs.ubc.ca/~jan/amt
- Download AspectJRB, an aspect-aware refactoring tool:
http://dawis.informatik.uni-essen.de/site/research/aop/aspectjrb/index.html
- A tool in making that will support Dialogue-Based Aspect-Oriented Refactoring:
http://www.cs.ubc.ca/labs/spl/projects/ao-refactoring.html
PRINTER FRIENDLY VERSION
|