Patternskolen del 9 - Fluent interface

I forrige del sammenlignet vi Active Record og Repository. Denne gangen skal vi nøye oss med å se på ett pattern: Fluent interface. Det er ofte et pattern man konsumerer heller enn å implementere selv.

Bjørn Herve Moslet

Mange er nok kjent med det fra før, f.eks. fra rammeverk som jOOQ, JMock, Linq, Moq eller jQuery. Fluent interface brukes hovedsakelig til å implementere domenespesifikke språk (DSL). Den som benytter APIet kan skrive kode som leses mer eller mindre som vanlige setninger, noe som gjør det lett å forstå koden. Med auto complete kan man også få god hjelp av IDEen til å benytte APIet.

«Setningene» man skriver kan bli ganske lange, men for enkelhets skyld fatter vi oss i korthet. Under er et eksempel på et API som verifiserer om datoer og tidspunkter er innenfor et gitt intervall fra en gitt dato. Det kan være nyttig i f.eks. enhetstester der man må forholde seg til datoer, men ikke trenger absolutte verdier. Eksempler i C#:

enDato.ErInnen(3).Timer.Etter.Nå();
enDato.ErInnen(4).Dager.Før.Gitt(dato);

Implementasjon

Det vanlige er å implementere vha. method chaining. Man følger et metodekall med et nytt metodekall, adskilt med punktum (som i eksempelet over).

Returverdien fra en metode eller property i kjeden angir neste metode/property. Ofte er returverdien den samme som for forrige metode. En void-metode avslutter metodekjeden. Det vanlige er å bruke metoder når du skal angi en verdi og for å avslutte rekken av kall. Properties kan brukes når man ikke trenger å angi verdier.

Merk at Fluent interface er mer enn bare method chaining. APIet må «flyte» som lesbare setninger når du benytter det.

Under er den fullstendig implementasjon av eksempelet over. Først en extension method som starter metodekjeden:

public static class DateTimeExtension
{
public static Innen ErInnen(this DateTime gittDato, int antall)
{
return new Innen(gittDato, antall);
}
}

Deretter tre klasser som implementerer resten av “språket”:

public class Innen
{
private DateTime _gittDato;
private int _antall;

public Innen(DateTime gittDato, int antall)
{
_gittDato = gittDato;
_antall = antall;
}

public Retning Sekunder
{
get { return new Retning(_gittDato, TimeSpan.FromSeconds(_antall)); }
}

public Retning Minutter
{
get { return new Retning(_gittDato, TimeSpan.FromMinutes(_antall)); }
}

public Retning Timer
{
get { return new Retning(_gittDato, TimeSpan.FromHours(_antall)); }
}

public Retning Dager
{
get { return new Retning(_gittDato, TimeSpan.FromDays(_antall)); }
}
}

public class Retning
{
private DateTime _gittDato;
private TimeSpan _tidsspenn;

public Retning(DateTime gittDato, TimeSpan tidsspenn)
{
_gittDato = gittDato;
_tidsspenn = tidsspenn;
}

public Tidspunkt Etter
{
get
{
return new Tidspunkt(_gittDato, _tidsspenn, (gittDato, dato, spenn) =>
{
if (gittDato <= dato) throw new Exception($"{gittDato} må være etter {dato}");
if (!(gittDato - dato < spenn)) throw new Exception($"{gittDato - dato} er ikke mindre enn {spenn}");
});
}
}

public Tidspunkt Før
{
get
{
return new Tidspunkt(_gittDato, _tidsspenn, (gittDato, dato, spenn) =>
{
if (gittDato >= dato) throw new Exception($"{gittDato} må være før {dato}");
if (!(dato - gittDato < spenn)) throw new Exception($"{dato - gittDato} er ikke mindre enn {spenn}");
});
}
}
}

public class Tidspunkt
{
private DateTime _gittDato;
private TimeSpan _tidsspenn;
private Action<DateTime, DateTime, TimeSpan> _sammenligning;

public Tidspunkt(DateTime gittDato, TimeSpan tidsspenn, Action<DateTime, DateTime, TimeSpan> sammenligning)
{
_gittDato = gittDato;
_tidsspenn = tidsspenn;
_sammenligning = sammenligning;
}

public void Nå()
{
Gitt(DateTime.Now);
}

public void Gitt(DateTime dato)
{
_sammenligning(_gittDato, dato, _tidsspenn);
}
}

Hver klasse gir den neste som returverdi og videresender alle nødvendige parametre. På denne måten utgjør hver klasse et «ord i setningen».

Ulemper

Det krever litt tankearbeid å utvikle et (godt) Fluent interface. Både for å få språket til å “flyte” godt og fordi en skal holde tunga rett i munnen for å få alle klassene til å henge sammen i komplekse APIer.

En annen utfordring er hvis man må mocke et Fluent interface for enhetstesting. En risikerer å måtte mocke hver eneste klasse/metode i det som kan være en lang rekke av kall. Dette er både kjedelig, tidkrevende og sårbart for endringer i APIet.

Konklusjon

Fluent interface er et morsomt pattern. Det er godt egnet til å utvikle DSLer og gjør koden veldig lett å lese. Vanligvis vil man konsumere andres APIer, ofte for konfigurasjon av rammeverk. Det er ikke så ofte man implementerer det selv og det kan være litt utfordrende å programmere selve patternet. Det er tidkrevende å mocke et Fluent interface for enhetstesting.