At my currrent project I ended up generating run-time proxies for my Business Objects with CGLIB. As this is somewhat unusual for a straightforward enterprise application, I still wonder about the pros and cons of this approach.
The main thing the application does is product configuration - given a definition of some tree of products, it lets the user create and edit product attributes, add and remove subproducts according to that definition. It is a requirement that the definition is external and it is expected to change regularly, so as much as possible of the product logic should be defined there.
A fragment of some imaginary domain objects definition to illustrate the case:
<productdefinition id="someProduct"> <date id="startingDate" nullable="false"> <enumeration id="area"> <enum default="true" value="World"> <enum value="Europe"> </enumeration> <boolean id="allInclusive" defaultvalue="false"> <range id="limit" min="10" max="5000" step="10" defaultvalue="1250"> <subproduct type="someSubproduct"> <multiplicity mincount="0" maxcount="1" defaultcount="1"/> </subproduct> <productdefinition> <productdefinition id="someSubproduct"> <text id="reason" pattern="\a{1,30}"/> </productdefinition>
Whatever our Java classes implementation:
- when creating SomeProduct some values have to be set initially: setArea('World'), setLimit(1250) etc. A properly initialized instance of SomeOtherProduct should be added as a subproduct.
- someProduct.setStartDate(null) or someProduct.setArea("Japan"), someProduct.setLimit(10000) should all cause an error because the provided value is not acceptable according to the definition.
- Rendering a GUI for editing a product attribute and processing the user input is uniform for all product attributes
- etc. etc.
An intuitive approach was to try something PropertyBean-like with named properties. The implementation is straightforward, but the interface is somewhat clumsy:
interface Product { Object getPropertyValue( String propertyName); void setPropertyValue( String propertyName, Object propertyValue); . . . } class ProductImpl implementing Product { . . . }
This works, but the use cases coed using this interface code is clumsy and prone to errors:
someOtherProduct.setPropertyValue("someProduct.sumeSubproduct[0].reason", "Lousy service");
An example without nested properties is too awful to reproduce here, though not difficult to imagine. Forget auto code completion,
type checking, refactoring facilities. The natural Java way of doing this is:
someOtherProduct.getSomeProduct().get(0).setArea("Europe");
And you need to invent some trics to write the equivalent of:
<form:input path="someProduct.someSubproduct[0].reason"/>
The interface that I actually would like for the use case implementations is:
interface SomeProduct { Date getStartingDate(); void setStartingDate( Date date) throws BadValueException; // Null not accepted ListgetSomesubproduct(); SomeSubproduct addSubproduct(); void deleteSubproduct( Subproduct p); . . . } interface SomeSubproduct() { String getReason(); void setReason( String reason); }
Given existing ProductImpl instances, coding the implementations of those interfaces is a repetitive task, all getter/setters/addXXX/deleteXXX etc. will look alike. A code generation library could generate this trivial code for us. CGLib is a code generation library (a lot of tautology here) and I know it is good enough for Hibernate, so I thought it should do the job for me too. With a productImpl instance and a desired interface, the CGLib code needed to produce the proxy is surprisingly little:
class ProductProxyFactory { public static Object createProxy(ProductImpl productImpl, Class clazz) { Enhancer e = new Enhancer(); e.setSuperclass(clazz); Callback callback = new ProductProxyCallback(clazz.getSimpleName(), productImpl, productMapping); e.setCallback(callback); return e.create(); } } public class ProductProxyCallback implements MethodInterceptor { private ProductImpl productImpl; public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable { String methodName = method.getName(); if ( methodName.startsWith("get")) { String propertyName = findMappedPropertyName(methodName); // find mapped property from method name Object propertyValue = productImpl.getPropertyValue(propertyName ); Class subProductClass = findSubProductClass(propertyName ); // property value might be a product too if (subProductClass != null) { return createSubproductProxy(result, subProductClassName, mapped); } else { return result; } else { // Process setXXX, addXXX, deleteXXX methods and methods inherited from Object . . .
Let's see an example:
// There is some old code that still creates product using the clumsy Product interface ProductImpl productImpl = new ProductImpl( someProductDefinition); // and sets some data productImpl.setProperty( "startDate", today); // Let's create a proxy implementing SomeProduct interface: SomeProduct someProduct = (SomeProduct)createProxy( productImpl, SomeProduct.class); Date date = someProduct.getStartingDate()
The code above is schematic, of course, in reality the application does not have to create any proxies explicitly. Not bad, the old classes have received a new face - another interface to the same data, and the only code that has to be written for each new interface is the interface definition itself.
Pros & Cons
- This approach adds complexity. This part of the application has confused more than one project members joining the team - 'Hey, why can't I find the implementation of SomeProduct?". Is it worth maintaining this extra complixity for the needs af smaal audience - the developpers of one application?
- The code above is schematic, the real implementation has to take care of more details - external mapping file, caching proxy instances etc. The real code is already more complicated than the example and could potentially become too complicated, as business analysts add new requirements to the application.
- Object identity issues pop up if you use proxies and this case is not different - it is possible to create many proxies working on the same underlying productImpl.
- Some OOP has gone here. Try to override SomeProduct.getStartDate() or to set a breakpoint on it. I guess it happens when you mess up with AOP.
Let's not forget the pros:
- No typing, retyping and copy-pasting of trivial code or introducing errors there. Changing a product is limitied to editing tha java interface and a mapping file.
- There is one well-defined place where all products are defined. There is one well-defined class where the behaviour of all products is defined, and no lazy or evil programmer can override it somewhere.
Is that DSL?
I wonder how this approach relates to the DSL concept of trading generality for expressiveness. The starting point is undoubtedly a domain-specific XML. While the DSL examples I have seen have a code-generation phase, here code is generated at run time vs. source generation phase. An obvious difference is that one can not inspect/change/override the generated code. While this is a DSL according to scholars (see http://ftp.cwi.nl/CWIreports/SEN/SEN-E0309.pdf) is this the sort of the DSL that is hot right now? I am curious on your thoughts on this.
No comments:
Post a Comment