Fluent Interfaces
Ze bestaan al een paar jaar, en je ziet ze steeds vaker terugkomen. Fluent interfaces, ook wel interne DSL genoemd. Toch zie ik de techniek in de praktijk eigenlijk alleen worden gebruikt in frameworks. Jammer, want deze techniek kan eenvoudig worden toegepast om goed leesbare en op het domein toegespitste code te schrijven.
De term Fluent interface komt uit het kamp van Eric Evans en Martin Fowler; onze domein gedreven goeroes. De techniek is relatief eenvoudig te herkennen: operaties worden aaneengeschakeld geprogrammeerd. Een (bekend, niet specifiek mooi) voorbeeld is de criteria API van Hibernate:
List cats = session.createCriteria(Cat.class) .add( Restrictions.like("name", "Iz%") ) .add( Restrictions.gt( "weight", new Float(minWeight) ) ) .addOrder( Order.asc("age") ) .list();
Iedere methode aanroep geeft het criteria object door zodat je oneindig kun blijven uitbreiden zonder tijdelijke variabelen te moeten gebruiken.
Er zijn grofweg twee manieren om een fluent interface te implementeren:
Refereren naar this
De criteria API van Hibernate is een goed voorbeeld van deze methode. Je maakt eerst een criteria object en dit krijg je bij iedere methode aanroep terug. Bij het aanroepen van één van de resultaat functies (list, unique, scroll) wordt de uiteindelijk query opgebouwd en uitgevoerd. Als je een setter aanroept de al eerder is aangeroepen (setMaxResults bijvoorbeeld) overschrijf je de eerder gezette waarde. Dit laatste is een typische eigenschap van deze methode voor het ontwikkelen van Fluent interfaces.
Een andere eigenschap van deze methode is dat het erg eenvoudig is om de volgorde van de aanroepen onbelangrijk te maken:
DreamCar car1 = car().brand("Tesla").color(RED).fuelEfficient().build(); DreamCar car2 = car().color(SILVER).brand("Hyundai").fuel(DIESEL).build();
Een container object gebruiken
De tweede strategie is het gebruiken van een speciaal object wat steeds wordt doorgegeven. Je stopt de te bewerken objecten in een container waarvan de verschillende methoden steeds weer een container object doorgeven (het hoeft dus niet steeds hetzelfde type container te zijn). Voor toepassingen kun je bijvoorbeeld denken aan het bewerken van plaatjes:
Image image = manipulate(img) .blur(50, PERCENT) .rotate(90, DEGREES) .scale(50, PERCENT) .roundCorners() .render();
Het is in dit geval belangrijk dat de bewerkingen sequentieel worden uitgevoerd; het resultaat van de bewerking zou anders onvoorspelbaar zijn.
Een erg mooi voorbeeld van deze methode is overigens de FEST-Reflect bibliotheek die een Fluent interface bovenop de reflection API aanbiedt. Dat ziet er in gebruik ongeveer zo uit:
Person person = constructor().withParameterTypes(String.class) .in(Person.class) .newInstance("Yoda"); method("setName").withParameterTypes(String.class) .in(person) .invoke("Luke"); field("name").ofType(String.class) .in(person) .set("Anakin");
Zelfbouw
Zelf gebruik ik de tweede strategie in een utility klasse voor het bewerken van collecties die ik tijdens mijn huidige project maak(te). Ik begon met een aantal statische functies om collecties te filteren, sorteren en of bewerken zoals (volledige code hier):
all(Collection<T>, BooleanPredicate<T>) collect(Collection<? extends Object>, String, Class<T>) first(Collection<T>) first(Collection<T>, BooleanPredicate<T>) pick(Collection<T>) join(Collection<T>, String) sort(Collection<T>, String property) sort(Collection<T>, String property, Direction dir)
De gebruikte BooleanPredicate klasse is een eenvoudige template (bij gebrek aan closures) voor het schrijven van een element gebaseerde conditie:
public abstract class BooleanPredicate<T> { public abstract boolean eval(T t); }
Als het predikaat true oplevert wordt het betreffende object meegenomen in het resultaat. Het is met bovenstaande code al redelijk eenvoudig om een collectie te filteren:
List<String> testData = Arrays.asList(null, "appel", "peer", "sinaasappel"); Collection<String> filteredData = CollectionUtils.all(testData, new BooleanPredicate<String> (){ @Override public boolean eval(String t) { return t != null && t.contains("appel"); } }); assertEquals(2, filteredData.size()); assertTrue(filteredData.contains("appel")); assertTrue(filteredData.contains("sinaasappel"));
Overigens mag natuurlijk duidelijk zijn dat het predikaat niet per se als anonymous inner class hoeft te worden geimplementeerd; dat kun je mooi genereren aan de hand van het domein waarin je werkt waardoor je dit soort dingen kunt schrijven:
if(all(connections, ofType(AUTO_FILL)).size() < capacityAutofill){ ... }
Al behoorlijk Fluent, zonder daadwerkelijk Fluent te zijn. Om mijn utilities echt fluent te maken heb ik een tussen-object geintroduceerd, genaamd CollectionUtilsPipeline dat een kopie van de te bewerken collectie in zich heeft en steeds een nieuwe instantie van zichzelf oplevert. Het initiëren van de pipeline doe ik in een methode genaamd from:
public static <T> CollectionUtilsPipeline<T> from(Collection<T> col) { return new CollectionUtilsPipeline<T>(col); }
De CollectionUtilsPipeline heeft voor onder andere de eerder genoemde functies delegerende methodes (volledige code hier):
public class CollectionUtilsPipeline<T> { private Collection<T> collection; public CollectionUtilsPipeline(Collection<T> collection) { this.collection = collection; } public CollectionUtilsPipeline<T> all(BooleanPredicate<T> bp) { Collection<T> opResult = CollectionUtils.all(collection, bp); return new CollectionUtilsPipeline<T>(opResult); } public CollectionUtilsPipeline<T> notNull(){ return all(new NotNullPredicate<T>()); } public Collection<T> result(){ return collection; } public List<T> list(){ return (List<T>)collection; } public T first(BooleanPredicate<T> bp) { return CollectionUtils.first(collection, bp); } public T first() { return CollectionUtils.first(collection); } ... }
Hiermee kan ik dit soort dingen schrijven:
List<Address> addresses = from(persons) .notNull() .collect("address", Address.class) .sort("city") .list();
Geen boilerplate code voor loops en het checken van condities, alleen code die te maken heeft met het probleem wat ik probeer op te lossen! Leuk detail hier is trouwens dat de container begint als type Person. Na het aanroepen van de collect methode krijg je een container van het type Address terug. Dit uit zich in het type van de teruggegeven lijst maar ook in de typering van de predicaten (dus BooleanPredicate<Address>) die je aan de first of all methode mag geven. Je kunt dus steeds op compile time zien of de typering nog klopt.
Conclusie
Iemand vertelde me ooit dat er een ‘wet’ (was het Demeter?) is die voorschrijft hoeveel methode aanroepen er op een enkele regel mogen staan. Indertijd dacht ik dat ik het met hem eens was. Ondertussen denk ik dat dat eigenlijk alleen maar waar is als de API zich er niet voor leent. Ik merk zelf dat ik uiteindelijk veel minder code (== minder bugs?) schrijf om hetzelfde te bereiken als ik dit soort technieken toepas. Tevens wordt het een stuk eenvoudiger om DRY en Highly Cohesive te werken; oftewel het wordt eenvoudiger om kwalitatief betere code te schrijven!
Daarbij komt nog het voordeel wat domein gedreven onwikkelen met zich mee brengt: de vorm van de code ligt dichter bij de vorm waarin een domein specialist zijn probleem beschrijft. Hierdoor wordt het eenvoudiger om met specialisten uit het domein waarvoor je software ontwikkelt te communiceren.



Wauw, goed artikel!
Al aan gedacht om aan te sluiten bij FunctionalJ (http://functionalj.sourceforge.net)? Misschien handig voor het maken van functie objecten.
Erik van Oosten - maart 25, 2008 21:43
Ik ben voorstander van deze aanpak, maar ik voorzie wel performance-piepers die zeggen dat je op deze manier meerdere keren door al je collecties loopt. Of zie ik dat verkeerd? Iemand anders kan namelijk in 1 SQL query (met meerdere “WHERE clauses”) dit doen. Als er tenminste een SQL database achter hangt… Maar het geldt ook voor de wat ingewikkeldere for-loop.
Als ik het goed zie: is er dan ook een manier waarop je predicaten kunt “verzamelen” en dan uiteindelijk in 1 loop tegelijk toepassen? Dat zou namelijk meteen een performance-increase geven en ook de nay-sayers overtuigen…
Felix - maart 26, 2008 22:02
Felix je hebt gelijk, je zou predikaten ook kunnen opzamelen. Maar je kunt een predikaat zo ingewikkeld maken als je zelf wilt; dus als performance een issue is zou je ook meerdere checks kunnen combineren.
Ik was bezig aan een refactor slag waar bij je meerdere predikaten kunt combineren tot een enkel filter; maar door wat vervelende eigenschappen van generics (kun je eigenlijk niet gebruiken in var-args) was ik nog niet tot een mooie oplossing gekomen.
Overigens moet je dit ook zien als een voorbeeld voor het bouwen van fluent interfaces, niet per definitie als _de_ oplossing voor alle collectie ‘problemen’.
Het voordeel van deze (of iedere aanpak die zorgt dat code DRY wordt) aanpak is trouwen dat dergelijke performance tweaks maar op één enkele plek plaats hoeven te vinden.
Peter Maas - maart 26, 2008 22:10
De Criteria api zal verschillende predikaten combineren en er 1 SQL statement van maken.
Dus als je data uit een database komt, dan speelt het probleem van ‘meerdere keren aflopen van de collecties’ niet zo.
Ik zie een derrgelijke techniek niet als vervanging voor SQL/Hibernate, maar meer om collecties te filteren die op een andere manier zijn opgebouwd.
Edwin van der Elst - maart 27, 2008 11:28
@edwin inderdaad, dit is ook precies het verschil tussen de verschillende manieren waarop een fluent interface zou kunnen worden geïmplementeerd.
Peter Maas - maart 27, 2008 11:40
… Ik was bezig aan een refactor slag waar bij je meerdere predikaten kunt combineren tot een enkel filter; maar door wat vervelende eigenschappen van generics (kun je eigenlijk niet gebruiken in var-args) was ik nog niet tot een mooie oplossing gekomen. …
Nu snap ik waarom je zat te klagen over var-args en type safety etc.
Het is inderdaad curieus dat je type safety warnings krijgt bij het maken van een getypede var-args parameter.
Maar in het kader van de Fluent-heid, kan je de BooleanPredicates toch ook chainen?
new BooleanPredicate().and(new BooleanPredicate()).or(new BooleanPredicate())?
Een final evaluate() methode zou dan zorg dragen voor de uiteindelijke return waarde.
Auke van Leeuwen - maart 29, 2008 13:48
De laatste ‘evaluate’ zou je niet eens nodig hebben als de ‘and’ call een nieuw predikaat geeft.
Zelf zat ik ondertussen te denken een Filter klasse te introduceren die meerdere predikaten combineert:
public class Filter extends BooleanPredicate {
public List> predicates = new ArrayList>();
@Override
public boolean eval(T t) {
for(BooleanPredicate predicate : predicates){
if(predicate.eval(t) == false){
return false;
}
}
return true;
}
public static Filter with(BooleanPredicate firstPredicate){
Filter filter = new Filter();
return filter.and(firstPredicate);
}
public Filter and(BooleanPredicate predicate){
predicates.add(predicate);
return this;
}
}
Dit zou je dan als volgt kunnen gebruiken:
public class FilterTest extends TestCase {
public final List testData = Arrays.asList(”appel”, “aardappel”, “peer”, “sinaasappel”);
public void testFilterWithTwoPredicates(){
Collection result = from(testData).all(with(contains(”appel”)).and(doesNotContain(”sinaas”))).result();
assertEquals(2, result.size());
}
private BooleanPredicate contains(final String word){
return new BooleanPredicate() {
@Override
public boolean eval(final String t) {
return t.indexOf(word) > -1;
}
};
}
private BooleanPredicate doesNotContain(final String word){
return new BooleanPredicate() {
@Override
public boolean eval(final String t) {
return t.indexOf(word) == -1;
}
};
}
}
Maar ik moet zeggen dat ik nog niet helemaal gelukkig ben met het enorme hoeveelheid haakjes in de aanroep.
Peter Maas - maart 29, 2008 14:29
Bah, mijn code is gemangeld door wordpress….. en niet genoeg recht om het aan te passen… die generics dingen klopten wel hoor
Peter Maas - maart 29, 2008 14:38
Tja, die haakjes ontkom je denk niet helemaal aan. Overigens begrijp ik niet helemaal waarom je een Filter zou introduceren als je het ook binnen het BooleanPredicate kan oplossen? Een Filter lijkt gelimiteerd tot een serie van ANDs. Ik ben een beetje huiverig om mijn code erin te plakken, but here goes nothing: <test>
Auke van Leeuwen - maart 29, 2008 21:43
public abstract class BooleanPredicate<T> {
public final BooleanPredicate<T> and(final BooleanPredicate<T> other) {
if (other == null) {
throw new IllegalArgumentException(\"other BooleanPredicate may not be null!\");
}
return new BooleanPredicate<T>() {
@Override
public boolean eval(T t) {
return BooleanPredicate.this.eval(t) && other.eval(t);
};
};
}
public final BooleanPredicate<T> or(final BooleanPredicate<T> other) {
if (other == null) {
throw new IllegalArgumentException(\"other BooleanPredicate may not be null!\");
}
return new BooleanPredicate<T>() {
@Override
public boolean eval(T t) {
return BooleanPredicate.this.eval(t) || other.eval(t);
};
};
}
public final BooleanPredicate<T> not() {
return new BooleanPredicate<T>() {
@Override
public boolean eval(T t) {
return !BooleanPredicate.this.eval(t);
};
};
}
/**
* This method needs to be implemented.
*
* @param t
* The object to evaluate
* @return true or false, depending on your needs
*/
public abstract boolean eval(T t);
}
Auke van Leeuwen - maart 29, 2008 21:44
En (in jouw bovenstaande FilterTest):
public void testFilterWithTwoPredicates() {
Collection<String> result = from(testData).all(contains(\"appel\").and(doesNotContain(\"sinaas\"))).result();
assertEquals(2, result.size());
}
public void testFilterWithThreePredicate() {
Collection<String> result = from(testData).all((contains(\"appel\").and(doesNotContain(\"sinaas\"))).or(contains(\"peer\"))).result();
assertEquals(3, result.size());
}
public void testTautologyPredicate() {
BooleanPredicate<String> a = contains(\"appel\");
BooleanPredicate<String> b = doesNotContain(\"peer\");
BooleanPredicate<String> tautology = (a.and(b)).or(a.not()).or(b.not());
Collection<String> result = from(testData).all(tautology).result();
assertEquals(4, result.size());
}
Auke van Leeuwen - maart 29, 2008 21:46
Leuke oplossing Auke!
Toch zou ik er persoonlijk denk ik voor kiezen om een specifiek predicate (’MyFruitMatchingRule’) te schrijven voor de logica die nodig is in de all methode… zodat je gewoon zoiets kan doen:
Maar goed, we hadden het over voorbeelden natuurlijk
Peter Maas - maart 29, 2008 22:11