Aldrig Fluent Interface på bekostnad av god design
Johan Lindfors har ett blogginlägg om hur man kan instansiera ett objekt, en KaffeLatte, med fluent interface, eller member chaining. Eftersom han ville bolla idéen så kommer här min replik på det. Så här ser Johans exempel ut:
KatteLatte latte = KatteLatte.beställ
.dubbel
.fettfri
.vanilj;
Klassen måste då se ut så här:
public class KatteLatte
{
public static KatteLatte beställ
{
get
{
return new KatteLatte();
}
}
public KatteLatte dubbel
{
get
{
this.Dubbel = true;
return this;
}
}
public KatteLatte vanilj
{
get
{
this.Vanilj = true;
return this;
}
}
public KatteLatte fettfri
{
get
{
this.Vanilj = true;
return this;
}
}
public bool Vanilj { get; private set; }
public bool Dubbel { get; private set; }
public bool Fettfri { get; private set; }
}
Inte så snyggt, om du frågar mig, och andra invänder mot det i kommentarerna till Johans inlägg. Fluent Interface är trevligt, men det får inte ske på bekostnad av principer för god objekt orienterad programmering.
I det här fallet är latten ett typexempel på Decorator Pattern (just kaffe finns med som exempel på Wikipedia). Jag går inte in på varför det är en bättre lösning, utan konstaterar bara att det är det. :)
Vidare bör man inte blanda ihop det man skapar med hur man skapar det. Här kommer ett exempel på betydligt bättre design OCH fluent interface.
Om vi börjar med Decorator-mönstret. I korthet kan man säga att man kedjar ihop liknande objekt med varandra. Dessa kan hakas ihop i valfri ordning och man kan därmed bygga både komplexa objekt och nya strukturer man inte förutsåg när man började. I det här fallet skapar vi upp de olika ingredienserna och med hjälp av mönstret kan vi haka ihop dessa till olika drycker.
public abstract class Dryck
{
private readonly Dryck _dryck;
protected Dryck() { }
protected Dryck(Dryck dryck) { _dryck = dryck; }
…
}
public class Espresso : Dryck
{
public Espresso() { }
public Espresso(Dryck dryck) : base(dryck) { }
…
}
public class Mjölk : Dryck
{
public Mjölk() { }
public Mjölk(Dryck dryck) : base(dryck) { }
…
}
public class FettfriMjölk : Dryck
{
public FettfriMjölk() { }
public FettfriMjölk(Dryck dryck) : base(dryck) { }
…
}
public class Vanilj : Dryck
{
public Vanilj() { }
public Vanilj(Dryck dryck) : base(dryck) { }
…
}
… fler drycker
Vi kan nu skapa lite olika drycker så här:
var latte=new Espresso(new Milk());
var dubbelEspresso=new Espresso(new Espresso());
var americano=new Espresso(new Vatten());
(Mängden mjölk i en latte kan diskuteras, men om du frågar mig är det alltid för mycket mjölk i en kaffe med mjölk i :)
Sen skapas en Abstract Factory (jag tog bort dubbel för att göra det hela lite enklare). Fabriken har till uppgift att skapa en dryck.
public interface IDryckFactory
{
IDryckFactory Vanilj();
IDryckFactory Fettfri();
Dryck BeställningKlar();
}
public class LatteFactory : IDryckFactory
{
private bool _vanilj;
private bool _fettfri;
public static IDryckFactory Beställ()
{
return new LatteFactory();
}
public IDryckFactory Vanilj() { _vanilj = true; return this; }
public IDryckFactory Fettfri() { _fettfri = true; return this; }
public Dryck BeställningKlar()
{
Dryck latte = _fettfri ?
new Espresso(new FettfriMjölk()) :
new Espresso(new Mjölk());
if(_vanilj)
{
latte = new Vanilj(latte);
}
return latte;
}
}
För att skapa en latte nu så kan man skriva:
var latte = LatteFactory.Beställ()
.Fettfri()
.Vanilj()
.BeställningKlar();
Inte så mycket mer för utvecklaren som ska skapa en latte att skriva, men mycket, mycket bättre underliggande design.
Mönstret <Skapa ett objekt, anropa lite metoder, avsluta med en “avsluta”-metod", få ut ett resultat> går att använda till fler saker. Metoden fungerar bra till valideringar t.ex
string myString;
...
bool isValid=myString.StartValidation().
IsNotNull().
LengthIsAtLeast(5).
LengthIsAtMost(10).
DoesNotContain("-").
Validate();
Går inte in i detalj på hur man gör, kanske kommer det en post om det i framtiden, men här används extension metoder och generiska klasser på ett listigt sätt. Det går att utöka så att man, utöver en bool, kan få ut felmeddelanden om varje del som inte validerar.