Martin Fowler blogged about “minimal” vs. “humane” interface. A minimal interface provides only the basic methods enabling, but not providing, clients to write convenience methods. A humane interface, on the other hand, considers typical uses of the interface and provides convenience methods as a part of the interface itself. Providing convenience methods through a wrapper (similar to the java.util.Collections class) isn’t as natural as providing them as a part of the interface itself. Further, using a wrapper for accessing minor convenience methods (such as List.first() and List.last(), see below) is difficult to justify given the extra code you need to write.
Martin asks “what’s the basis for deciding what should be added to a humane interface?”. This is a really important question. I have seen fat “humane” interfaces, where many methods had dubious value and only increased the complexity of those interfaces. I believe creating good humane interfaces requires that
- Use cases form the basis for deciding what should be added to a humane interface. This means each project may need different humane interfaces for the same underlying concept.
- Convenience methods for each use case (or coherent set of use cases) be contributed by separate humanizing modules. This allows keeping the core interfaces clean, while adapting them to project-specific needs by including appropriate modules.
Languages such as Java make the separation hard to achieve, since a type needs to define everything in one place and interfaces cannot contain method implementations. We can overcome these limitations using AspectJ. Here core interfaces follow the minimalist approach and aspects introduce convenience methods using inter-type declarations.
For example, you can humanize the access to the List interface using an aspect such as follows:
public aspect HumanizeListAccess {
public E List<E>.first() {
return size() > 0 ? get(0) : null;
}
public E List<E>.last() {
return size() > 0 ? get(size()-1) : null;
}
}
The aspect introduces two new methods, along with their implementations, to the List interface. Now you can use the interface as follows:
List<string> tickers = new ArrayList</string><string>();
tickers.add("GOOG");
tickers.add("SUNW");
tickers.add("YHOO");
firstTicker = tickers.first();
latestTicker = tickers.last();
The only problem with the above implementation: it works only if you weave this aspect into rt.jar!
Here is another example of humanizing an interface the AOP way (and does not require weaving into rt.jar). Consider the following Inventory interface:
public interface Inventory {
public void addItem(Item item, int count);
public void removeItem(Item item, int count);
public Collection<item> getItems();
public int getItemCount(Item item);
}
You can humanize this interface to provide price awareness using an aspect as follows:
public aspect HumanizeInventoryPriceAwareness {
public Item Inventory.getLeastExpensiveItem() {
float leastPrice = Float.MAX_VALUE;
Item leastExpensiveItem = null;
for(Item item : getItems()) {
if(item.getPrice() < leastPrice) {
leastPrice = item.getPrice();
leastExpensiveItem = item;
}
}
return leastExpensiveItem;
}
public Item Inventory.getMostExpensiveItem() {
... similar to getLeastExpensiveItem()
}
public Collection<Item> Inventory.inRangeItems(float lowPrice, float highPrice) {
Collection</item><item> inRangeItems = new ArrayList</item><item>();
for(Item item : getItems()) {
if((item.getPrice() >= lowPrice)
|| (item.getPrice() < = highPrice)) {
inRangeItems.add(item);
}
}
return inRangeItems;
}
public Collection<Item> Inventory.cheaperThanItems(float price) {
return inRangeItems(0, price);
}
public Collection</item><item> Inventory.expensiveThanItems(float price) {
return inRangeItems(price, Float.MAX_VALUE);
}
}
The HumanizeInventoryPriceAwareness aspect uses inter-type declarations introducing new methods, along with their implementations, to the Inventory interfaces. Notice, how cheaperThanItems() and expensiveThanItems() themselves use the convenience methods introduced by the same aspect.
Now you can use the humanized Inventory interface as follows:
Inventory inventory = ... Item leastExpensive = inventory.getLeastExpensiveItem(); Collection</item><item> itemsForFriends = inventory.inRangeItems(20f, 50f); Collection</item><item> itemsForSpouse = inventory.expensiveThanItems(400f);
Note that nothing prevents you from including the aspects in the same source file as the interface or even as nested aspects inside the interface. If you do so, you will get compartmentalization instead of full separation.
So far, we relied on static crosscutting alone. We can implement more complex convenience methods when we throw in dynamic crosscutting. Here dynamic crosscutting tracks the required information and static crosscutting exposes it. For example, here we track access to the inventory and expose the last accessed item through a method:
public aspect HumanizeInventoryTracking {
private Item Inventory.lastAccessedItem;
public Item Inventory.getLastAccessedItem() {
return lastAccessedItem;
}
after(Inventory inventory, Item item) returning
: (execution(* Inventory.addItem(Item, ..))
|| execution(* Inventory.removeItem(Item,..)))
&& this(inventory)
&& args(item, ..) {
inventory.lastAccessedItem = item;
}
}
You can follow this pattern to implement more interesting methods such as addedSinceItems(Date), mostAccessedItem(), and leastAccessedItem().
Implementing humane interfaces with the help of AOP allows developing and maintaining each module separately. It also allows easy customization of an interface to meet a project’s needs by simply choosing a suitable set of aspects. For example, if price awareness isn’t required in your project simply exclude the HumanizeInventoryPriceAwareness aspect.
AspectJ in Action
Cool !
About static crosscutting, besides behaviour, can I introduce data to the class ? And can I have 2 simultaneous aspects (files) statically introducing stuff into the same class ?
Thanks
Yes, you can introduce data to interfaces and classes. In my example, the
HumanizeInventoryTrackingaspect introducedlastAccessedItemto theInventoryinterface (which is then exposed through the introducedgetLastAccessedItem()method).You can have any number of aspects (placed either in the same source as the interface or away from it) introducing data and methods. In my example, you can include both
HumanizeInventoryPriceAwarenessandHumanizeInventoryTrackingin the same project. This will make the methods introduced by both aspects to be available to clients.How would this aspect approach be any better than a wrapper / decorator approach. The Humanize… aspects could also be written as a wrapper, no?
–Das
Das, using decorator pattern will force you to decorate every single method (core methods included) to delegate its implementation to the underlying object. Therefore, you will end up writing a lot of boilerplate code. Further, decorator pattern has the object identity issue. For example, should the decorated object be treated the same as the decorator object by the
equals()method?Aspects, on the other hand add the functionality in place avoiding any code to delegate core methods and avoiding the object identity problem.
The limitation of the List example is unfortunate. Do you know if AspectJ has any plans on the horizon to make such a thing work without having to mess around with rt.jar?
There is no current plans to allow weaving into rt.jar. The real solution to this problem is to support AOP at VM-level itself. See http://dev2dev.bea.com/pub/a/2005/08/jvm_aop_1.html for more information.
Very nicely motivated use of AspectJ. But I wanted you to say more on “separate humanizing modules”. To feed back into Fowler’s blog entry, what’s humane can depend on the context and use. AspectJ permits you to have more and less humane interfaces tailored to a set of users/clients. That raises the question: why don’t you extend the interface, so clients can distinguish A from Humane_A, and bind to that? That permits you to compile without AspectJ and weave later, and gives you a way to control different clients (and have different (aspect) implementations of Humane_A (e.g., cache v. lookup) for different projects).