Option C is by far the most ideal from a design standpoint, but it requires a lot of work to create and maintain the proxy classes. In a loose sense, dynaop uses the same approach as Option C, but it does all of the hard work for us. dynaop creates these proxy classes on the fly and provides a generic hook into object creation. Using dynaop to make a second class or even all of the classes in a package observable consists of a small configuration tweak rather than hand coding a horde of proxy classes. As with the proxy pattern implementation, dynaop gives you the freedom to use your target class with and without the proxy, or even with multiple different proxy configurations, all at the same time depending on your needs.
First, we must add the Subject implementation to our target class. In the OO implementations, we accomplished this using inheritance or delegation. In dynaop, we use mixin introduction. We can take the same Subject mixin implementation and later attach it to any target class we like:
Example 2.5. subject mixin
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import dynaop.Proxy; import dynaop.ProxyAware; public class SubjectMixin implements Subject, ProxyAware { Collection observers = new ArrayList(); Subject subject; public void addObserver(Observer observer) { observers.add(observer); } public void notifyObservers() { for (Iterator i = observers.iterator(); i.hasNext();) ((Observer) i.next()).notify(this.subject); } public void removeObserver(Observer observer) { this.observers.remove(observer); } // this is called by dynaop. public void setProxy(Proxy proxy) { this.subject = (Subject) proxy; } }
As you can see, the SubjectMixin implementation contains all the logic necessary to notify the observers. It has no dependency on any particular target class. Note also that it passes the proxy to the observers rather than a "this" reference; the observers want to see our proxy as a whole, not just this particular mixin instance. An awareness that your class is a small part of a bigger proxy is essential.
Next, we must call the Subject.notifyObservers() method at the appropriate time. Doing so from our target class would create a dependency on the subject/observer API, something we're trying to avoid. Using dynaop we can intercept the invocations on our state-changing methods using a generic Interceptor class:
Example 2.6. subject interceptor
import dynaop.Interceptor; import dynaop.Invocation; public class SubjectInterceptor implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { // proceed to the target method implementation. Object result = invocation.proceed(); // invoke our SubjectMixin... Subject subject = (Subject) invocation.getProxy(); subject.notifyObservers(); return result; } }
This Interceptor class can be applied to any method. The passed Invocation instance provides:
We apply our SubjectInterceptor implementation to state-changing methods and the interceptor notifies the observers after the invocation completes.
Third, we must configure dynaop to apply our mixin and interception advice to our target class. The default configuration file is a BeanShell script located in the class path and named "dynaop.bsh". To apply our mixin and interceptor to setter methods in MyClass, we simply add these lines to the configuration file:
Example 2.7. subject configuration, "dynaop.bsh" listing
// attach SubjectMixin to instances of MyClass. mixin( mypackage.MyClass.class, SubjectMixin.class ); // apply interceptor to set methods. interceptor( mypackage.MyClass.class, SET_METHODS, new SubjectInterceptor() );
This is a BeanShell script; these are simply method calls. If MyClass has a method named settle() that we do not want to be intercepted, we simply tweak our configuration:
Example 2.8. method pointcut that omits "settle()"
// apply interceptor to set methods, but not settle().
interceptor(
mypackage.MyClass.class,
intersection(SET_METHODS, not(signature("settle"))),
new SubjectInterceptor()
);
The highlighted portion in the preceding example is what's known as a method pointcut. This basically says pick the intersection (as in intersection of two sets) of the SET_METHODS pointcut (one of the implicit pointcuts in dynaop) and the pointcut that picks all methods except for those containing the word "settle". This essentially picks all setters, but not methods named "settle".
The signature() method used in the method pointcut creates another method pointcut that picks method signatures that match the given regular expression ("settle"). The not() method negates the pointcut essentially picking the inverse of the method set.
In the future, we simply tweak our pointcuts to make more classes "observable."
Last but not least, we must hook MyClassImpl so that dynaop can wrap it with a proxy and add the mixins and interceptors:
Example 2.9. making a class observable and registering an observer
import dynaop.*; import mypackage.*; ... MyClass myClass = new MyClassImpl(); // wrap myClass using the default proxy factory (configured with // "/dynaop.bsh"). myClass = (MyClass) ProxyFactory.getInstance().wrap(myClass); // cast myClass to Subject (implemented by SubjectMixin) and add an observer. ((Subject) myClass).addObserver(new MyObserver()); // call a method that will result in the observer being notified. myClass.setSomeField();
In the previous example, dynaop dynamically creates a proxy class that implements the MyClass and Subject interfaces. Invocations on those interfaces delegate to the target or mixin instances. This is known as a dynamic proxy. One limitation of this approach is that we are now working with a proxy instance, not an instance of MyClassImpl directly. Because of this, we only have access to methods in interfaces.
If MyClassImpl doesn't implement an interface or we need methods not present in the interface, dynaop provides a second method of proxy generation: class proxy. Whereas with dynamic proxies dynaop creates an entirely new proxy class, class proxies extend the target class to create a proxy class. The class proxy adds mixins and interceptors just like the dynamic proxy, but it calls super.someMethod() in the parent class rather than delegating to a wrapped instance. To use a class proxy, we modify the way we hook our object's creation:
Example 2.10. using a class proxy
import dynaop.*;
import mypackage.*;
...
// extend MyClassImpl using the default proxy factory (configured with
// "/dynaop.bsh").
myClass = (MyClass) ProxyFactory.getInstance().extend(MyClassImpl.class);
// cast myClass to Subject (implemented by SubjectMixin) and add an observer.
((Subject) myClass).addObserver(new MyObserver());
// call a method that will result in the observer being notified.
myClass.setSomeField();
Because of the nature of class proxies, dynaop must create our object for us. Because of this, dynaop requires that the class it's extending have a default, public, no-argument constructor.
That's it! We only need to provide this hook into the proxy factory once at creation time. Now, with a simple tweak of our configuration, we can apply advice that makes MyClass transactional, that logs method invocations, etc. Using dynaop we've improved our implementation of a well-established design pattern with very little effort, something not possible with only OO tools at our disposal.