Another example of as(...) used for easily extensible APIs
Here's another simple example of how the as(...) idiom can be used for create an API which is stable, elegant, but extensible (this time unrelated to any semantic stuff). Up to a few days ago, I had the following interface in the GeoCoding API of forceTen:
public interface GeoCoderEntity extends Comparable<GeoCoderEntity>, Serializable, As, Lookup.Provider
public enum Type
public String getId();
public Coordinate getCoordinate();
public String getCode();
public String getTypeAsString();
public Type getType();
public Collection<GeoCoderEntity> findAllChildren();
public GeoCoderEntity getParent()
public <T> T as (@Nonnull Class<T> personalityType);
public <T> T as (@Nonnull Class<T> personalityType, @Nonnull NotFoundBehaviour<T> notFoundBehaviour);
Then I had to add new features to the API: the capability of retrieving an “emblem” for a geographical entity (the emblem is basically an icon: for instance, the flag for a country, or the blason for a region or province), as well as a new FactSheet class which contains things such as the population or the timezone.
Basically I could just add a few methods:
public Image findEmblem (@Nonnegative int minimumSize)
public Object getFactSheetItem (@Nonnull String key);
But adding two methods can have an impact to an existing API - for instance, in my case, because GeoCoderEntity is an interface. For this reason, some argue about not using interfaces, but abstract classes instead, as they can provide default implementation of newly added methods, without requiring the implementation classes to be immediately updated.
I'm not against that - but this is not the problem I'd like to discuss. My problem is instead the tendency of APIs to have an always increasing number of methods added to a certain interface / class. After a few time, you're going to have some “fat” classes that are large and difficult to test and understand.
Using composition is a better idea - let's split the interface / class, or - better - let's add some new interfaces / classes for the new methods; so as(...) - or the completely equivalent NetBeans Platform construct getLookup().lookup(...) - can provide the solution. In fact, GeoCoderEntity didn't get more methods at all: I just updated its implementation(s) and introduced two small classes, EmblemFactory and FactSheet, so I can write:
import static it.tidalwave.geo.geocoding.GeoCoderEntity.EmblemFactory;
import static it.tidalwave.geo.geocoding.GeoCoderEntity.FactSheet;
Image emblem = geoCoderEntity.as(EmblemFactory).findEmblem(32);
FactSheet factSheet = geoCoderEntity.as(FactSheet);
Without entering the details of FactSheet (not related to as(...)), it is possible to write:
import static it.tidalwave.geo.geocoding.GeoCoderEntity.FactSheet.*;
long population = geoCoderEntity.as(FactSheet).get(POPULATION);
TimeZone timeZone = geoCoderEntity.as(FactSheet).get(TIMEZONE);
so, as you can see, the client code is coincise - one line for each new operation - just as new ad-hoc methods were introduced.
The advantage is that this is a dynamic extension - in other words, it would be possible to have only part of GeoCoderEntity instances to have the EmblemFactory or FactSheet personalities (even though in my case I didn't apply this option).
As as side note, notice how I replicated the declaration for the two overloaded versions of as(...) - this is not required at all for an interface or an abstract class, since their declaration is inherited from As. But replicating the methods allowed me to write some specific javadoc, that e.g. pops up in my NetBeans IDE during autocompletion:
As you can see, it provides some specific hints about which personalities can be used with GeoCoderEntity, while just keeping the inherited methods and their javadocs would make this impossible. I had this idea while discussing in the JavaPosse mailing list and a good objection to this idiom was that with explicitly declared methods, such as getEmblemFactory() or getFactSheet(), autocompletion helps you if you are new to the class usage. But it's possible to keep this feature with as(...).