Being able to "separate concerns" is a fundamental tenet of
object-oriented programming. Give an object
a small, well-defined set of responsibilities; and then assemble many
of those small objects to provide a larger set of responsibilities.
Following this pattern of keeping concerns separate enables our
code to be 1) modular in the case that it needs to be
reused, 2) testable because the code is separated into
isolated units, and 3) agile in the case that requirements
change. It is the challenge of assembling those objects that we'll
explore in this article.
Let's look at an example of a PersonService
interface that has a save() method. Clients will use
this interface to save a Person object. The core
implementation of this method persists a given Person
to the underlying database. The implementation is tested and it
works.
/**
* Person Service Interface
*/
public interface PersonService {
public void save(Person person);
}
/**
* Person Service Implementation
*/
public class PersonServiceImpl implements PersonService{
public void save(Person person){
doPersonSaveStuff(person);
}
private void doPersonSaveStuff(Person person){
// implementation details of saving a person
}
}
When clients call the PersonService.save() method,
a security check is needed to enforce that the client is indeed
allowed to save that Person. So a
SecurityService is introduced with its
isSecure() method.
SecurityService.isSecure() is tested to ensure that it
properly enforces security.
/**
* Security Service Interface
*/
public interface SecurityService{
public boolean isSecure();
}
/**
* Security Service Implementation
*/
public class SecurityServiceImpl implements SecurityService{
public boolean isSecure(){
// returns true...this is just an illustration
return true;
}
}
There are now two distinct classes,
PersonServiceImpl and
SecurityServiceImpl, that both need to be called at
runtime when a client calls the PersonService
interface's save() method. The simplest way to do this
is to, within the save() method of the
PersonService implementation, make a call out to
SecurityService.isSecure().
/**
* Person Service Interface
*/
public class PersonServiceImpl implements PersonService{
public void save(Person person){
if (securityService.isSecure()){
// if security check passes, do person save
doPersonSaveStuff(person);
}else{
throw new IllegalAccessException("Could not save "
+ " person: " + person);
}
}
private void doPersonSaveStuff(Person person){
// implementation details of saving a person
}
private SecurityService securityService;
public void setSecurityService(SecurityService securityService){
this.securityService = securityService;
}
}
This approach, however, is at odds with object-orientation and
the goal of separating concerns. By combining the code in this way,
the save() method is now less modular because it
cannot be used in the absence of the security check. Additionally,
the save() method is less testable because the
Person-saving cannot be tested, in isolation,
separate from the security-checking. The problem to be solved here
is how to keep the person-saving concern separate from the
security-checking concern, but combine them at runtime when
appropriate.
The theory of Aspect-Oriented Programming (AOP)
formalized the problem definition laid out in the example above, and
there have since been many implementations to help developers
assemble their objects at runtime. A good starting point to learn
more about AOP and various implementations is Wikipedia's AOP article. In AOP
terms for the example above, we needed to "advise" the
PersonService's save() method with the
security "advice" by "weaving" it in when a
client calls save(). These AOP terms will be used
going forward in this article.
There are many factors that go into choosing an approach for
assembling concerns at runtime. Perhaps the biggest factor, though,
is whether or not the object being advised is a singleton. In my opinion, there exist
well-documented and well-implemented approaches for advising
singletons. I personally enjoy Spring AOP when I need to advise
singletons. Recently however, I came across the need to advise a
domain object, an object that would be created frequently. I searched online for the best approach and I was
pointed to AspectJ. This was unsatisfactory
for me because AspectJ's learning curve and different compiler were
too invasive for my need to advise just one domain object. In the
remainder of this article, we will explore 1) why advising
singletons is so different from advising domain objects, and 2) some
alternatives to AspectJ for advising domain objects.
Advising Singletons Versus Domain Objects
A singleton, a class instantiated once, and a domain class, a
class instantiated frequently, illustrate the opposing extremes of
object-creation frequency. I have read and seen for myself that
common techniques to advising singletons do not perform as well
when applied to domain objects. The problem arises for domain
objects due to the frequency with which they get created. The approach
that Spring takes for advising singletons is to create a proxy for
the singleton at runtime that will intercept its method calls. When
clients make a call to the method, this proxy will configurably
coordinate calls to the necessary advisors. Spring takes one of two
approaches to create a proxy at runtime: either a Java dynamic proxy or a
CGLIB code-generated proxy. Either way the proxy
is created, the creation time is significantly slower than creating
a regular object using Java's new keyword. For a
singleton, one instance of the class will be created, so the time it
takes to create the proxy for that object is not a large concern.
For domain objects that would be created frequently, the
performance of creating a runtime-generated proxy along with each
object being advised may not be suitable. The following timings
show the difference between creating a dynamic proxy as opposed to
creating a regular object using the new keyword.
1000000 iteration test for 'Create dynamic proxy using Proxy.newProxyInstance()'
WARMED UP in 16 ms
Iterations ,Total Time(ms) ,Avg(ms)/Iteration ,Transactions/sec
1000000 ,7978 ,0.007978 ,125344.71
1000000 iteration test for 'Create regular object using new'
WARMED UP in 0 ms
Iterations ,Total Time(ms) ,Avg(ms)/Iteration ,Transactions/sec
1000000 ,63 ,0.000063 ,15873015
Note that creating a CGLIB proxy is even slower than creating a
Java dynamic proxy, but method invocations through the CGLIB proxy
will perform better than the Java dynamic proxy.
Advising Domain Objects
What we need are some techniques to advise domain objects that
will not significantly slow the object creation times of those
advised objects and its advisors.