Decorators, by design, should be lightweight objects which can be attached to the skin of other domain objects to add to the roles and responsibilities of the latter. They are added and/or removed dynamically from original objects - hence we say that decorators offer an added level of flexibility over static inheritance. Consider the following aggregate, which is modeled as a Composite :
Here is the base abstraction for the Composite, which models the Salary components of an employee :
public abstract class Salary {
public Salary add(final Salary component) {
throw new UnsupportedOperationException("not supported at this level");
}
public Salary remove(final Salary component) {
throw new UnsupportedOperationException("not supported at this level");
}
public Salary getChild(final int i) {
throw new UnsupportedOperationException("not supported at this level");
}
public abstract double calculate();
}
and here is the composite ..
public class SalaryComposite extends Salary {
private List<Salary> components = new ArrayList<Salary>();
@Override
public Salary add(Salary component) {
components.add(component);
return this;
}
@Override
public double calculate() {
double total = 0;
for(Salary salary : components) {
total += salary.calculate();
}
return total;
}
@Override
public Salary getChild(int i) {
return components.get(i);
}
@Override
public Salary remove(Salary component) {
components.remove(component);
return this;
}
}
and a couple of leaf level components ..
public class BasicPay extends Salary {
private double basicPay;
public BasicPay(double basicPay) {
this.basicPay = basicPay;
}
@Override
public double calculate() {
return basicPay;
}
}
public class HouseRentAllowance extends Salary {
private double amount;
public HouseRentAllowance(double amount) {
this.amount = amount;
}
@Override
public double calculate() {
return amount;
}
}
Decorators and Composites - a Marriage
Decorators work nicely with Composites, and I have some decorators to allow users add behaviors to the Composite elements dynamically. In order to have decorators for the Composite, I need to have the Decorator share the base class with the Composite.
public abstract class SalaryDecorator extends Salary {
private Salary component;
public SalaryDecorator(final Salary component) {
this.component = component;
}
@Override
public double calculate() {
return component.calculate();
}
}
and a couple of concrete decorators ..
a senior citizen adjustment which can be applied to some of the components of the salary ..
public class SeniorCitizenAdjustment extends SalaryDecorator {
protected double adjustmentFactor;
public SeniorCitizenAdjustment(final Salary component, double adjustmentFactor) {
super(component);
this.adjustmentFactor = adjustmentFactor;
}
@Override
public double calculate() {
//.. calculation of absolute amount based on adjustmentFactor
//.. complex logic
adjustment = ...
return super.calculate() + adjustment;
}
}
and a city compensatory allowance which varies based on the city where the employee leaves ..
public class CityCompensatoryAdjustment extends SalaryDecorator {
private double adjustmentFactor;
public CityCompensatoryAdjustment(final Salary component, double adjustmentFactor) {
super(component);
this.adjustmentFactor = adjustmentFactor;
}
@Override
public double calculate() {
//.. calculation of absolute amount based on adjustmentFactor
//.. complex logic
adjustment = ...
return super.calculate() + adjustment;
}
}
Now my clients can make use of the Composite and decorate them using the decorators designed to compose the individual salary components ..
//..
Salary s = new SalaryComposite();
s.add(new SeniorCitizenAdjustment(new BasicPay(1000), 100))
.add(new CityCompensatoryAdjustment(new HouseRentAllowance(300), 150));
//..
In the above example, the use of decorators provide the flexibility that the particular design pattern promises and allows instance level customization of individual components of the composite.
Decorating the Decorators
How do you handle fine grained variations within decorators ? In our case many of the decorators had fine grained variations within themselves, which calls for either inheritance hierarchies between them or some other way to decorate the decorators (super-decorators ?). The problem with static inheritance hierarchies is well-known. They have to be defined during compile time and the codebase for each deployment need to have the explosion of all possible variations modeled as subclasses. Looks like the problem that decorators are supposed to solve and that which we have already solved for the Composite Salary aggregate. Now we have the same problem for the decorators themselves.
We rejected the idea of static inheritance hierarchies and decided to model the variations as yet another level of decorators. We did not want to make the original decorators too complex and focused on making small composable, additive abstractions that can be tagged on transparently onto the domain objects and decorators alike.
e.g. in an implementation we found that the
CityCompensatoryAdjustment
has another fine grained variation. When CityCompensatoryAdjustment
is applied, we have the additional variation that states that if the employee's residence is in one of a group of premium cities, then he is eligible for an additional premium allowance on top of the normal CityCompensatoryAdjustment
. We rejected the idea of changing CityCompensatoryAdjustment
to incorporate the new variations for 2 reasons :- Trying to incorporate too much logic into decorators will make them complicated, which defeats the basic design motivation for the decorators
- These variations change across deployments - hence it will be a bloat trying to incorporate everything into a single class
Modeling the new requirement as another decorator (
PremiumCityAdjustment
), we have the following usage of the above snippet .. //..
Salary s = new SalaryComposite();
s.add(new SeniorCitizenAdjustment(new BasicPay(1000), 100))
.add(new PremiumCityAdjustment(
new CityCompensatoryAdjustment(
new HouseRentAllowance(300), 150),
adjustmentFactor));
//..
But the Variations are Deployment specific!
We cannot change the main codebase with the above fine-grained variations, since they are deployment specific. We need to find out ways to externalize the invocation of these decorators. We found out that these finer variations are policies that need to change the behavior of the original decorators whenever they are applied. In the above example, every invocation of
CityCompensatoryAdjustment
needs to be decorated with PremiumCityAdjustment
.public class PremiumCityAdjustment extends SalaryDecorator {
public PremiumCityAdjustment(final Salary component) {
super(component);
}
@Override
public double calculate() {
//.. calculation of adjustment amount
adjustment = ..
return super.calculate() + adjustment;
}
}
Aspects again !
We decided to implement the finer grained variations as decorators which would be applied through aspects to the original decorators. This is the base aspect for all such SuperDecorators ..
public abstract aspect SalarySuperDecoratorBase<T extends SalaryDecorator> {
pointcut decoratorCalculate(T s) : target(s)
&& execution(double T.calculate());
abstract pointcut myAdvice();
}
and here is the implementation for adding
PremiumCityAdjustment
as a decorator for CityCompensatoryAdjustment
..public aspect SuperDecoratorCityCompensatoryAdjustment
extends SalarySuperDecoratorBase<CityCompensatoryAdjustment> {
pointcut myAdvice(): adviceexecution()
&& within(SuperDecoratorCityCompensatoryAdjustment);
Object around(CityCompensatoryAdjustment s) : decoratorCalculate(s) && !cflow(myAdvice()) {
return new PremiumCityAdjustment(s).calculate();
}
}
This way we had a set of core decorators for the main domain model and another set of deployment specific decorators which decorated the core ones through aspect weaving. By adopting this strategy we were able to keep the decorators lightweight, avoided deep inheritance hierarchies and managed to keep customizable code base completely separate from the core one.
No comments:
Post a Comment