Friday, March 20th, 2009
Skriv en kommentar

Hver uke skriver jeg en artikkel for utviklerne i Contiki, hvor jeg forsøker å gi gode råd og tips først og fremst om ting som design og refakturering. Denne uken presenterte jeg Specification pattern, et mønster som isolerer business-regler og gjør kode som må ta mange avgjørelser enklere å lese og samtidig mere fleksibel.

Specification er et relativt avansert mønster som er en del av det som populært kalles Domenedrevet Design (DDD), som kan hjelpe oss (og da mener jeg spesielt oss i Contiki), til å eliminere duplisert kode. Jeg har alltid vært fasinert av konseptet om å lage en regelmotor, og dette mønsteret er én måte å implementere dette på.

Vi starter med litt tilfeldig kode..

For å fortelle deg om Specification vil jeg ta deg med på en liten reise, hvor vi starter med litt kode som har forbedringspotensiale, og som etterhvert vil endres til noe som er mer fleksibelt. Koden er ikke reell kode – bare noe jeg fant på i farten. SendIfPossible-metoden sender et dokument til en gitt bruker hvis noen kriterier tilfredstilles:

public class Step1

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        if (document.Status != DocumentStatus.Draft

            && document.Status != DocumentStatus.ReleaseCandidate

            && (document.Status != DocumentStatus.Final

            || !_config.ApprovalNeeded))

        {

            if (document.SecurityLevel == DocumentSecurityLevel.Public

                || (document.SecurityLevel == DocumentSecurityLevel.Restricted

                && (externalUser.TrustLevel >= 3

                || externalUser.YearlyContribution >= 10000)))

            {

                _sender.AddObject(document)

                    .ToEmail(externalUser.EmailAddress)

                    .Send();

            }

        }

    }

}

Desverre skriver de fleste av oss kode som dette fra tid til annen. Testene som gjøres her er vanskelige å lese, og reglene er vanskelige å forstå. Ok, la oss forsøke å forbedre dette litt da….

Trekk ut metoder

public class Step2

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        if (DocumentIsReadyToBeShown(document))

        {

            if (SecurityLevelOkForUser(document, externalUser))

            {

                _sender.AddObject(document)

                    .ToEmail(externalUser.EmailAddress)

                    .Send();

            }

        }

    }

 

    private bool DocumentIsReadyToBeShown(IDocument document)

    {

        return document.Status != DocumentStatus.Draft

                        && document.Status != DocumentStatus.ReleaseCandidate

                        && (document.Status != DocumentStatus.Final

                        || !_config.ApprovalNeeded);

    }

 

    private static bool SecurityLevelOkForUser(IDocument document, IUser externalUser)

    {

        return document.SecurityLevel == DocumentSecurityLevel.Public

                        || (document.SecurityLevel == DocumentSecurityLevel.Restricted

                        && (externalUser.TrustLevel >= 3

                        || externalUser.YearlyContribution >= 10000));

    }

}

Vi har nå trukket ut inneholdet i de to if-uttrykkene til separate metoder. Dette er en vanlig refaktureringsteknikk som add-ins som ReSharper, Refactor! og til og med Visual Studio kan hjelpe deg med. Gjennom å gi metodene gode navn er det klarere hvilke kriterier som må møtes for at dokumentet skal sendes til brukeren:

Dokumentet må være visningsklart, dvs. at statusen ikke må være DRAFT, ikke RELEASECANDIDATE, og hvis godkjenning av dokumenter er påkrevd så kan statusen heller ikke være FINAL. Og så må sikkerhetsnivået være ok for den gitte brukeren, dvs. at dokumentet enten må være PUBLIC, eller i tilfeller hvor det er RESTRICTED så må brukeren enten ha TRUST LEVEL 3 eller høyere, eller vi må tjene 10.000 eller mer på denne personen.

Ok, så kanskje det ikke var så lett å lese og forstå likevel. Dette bør vi kanskje splitte opp mere.

Men før vi gjør det, la oss først tenke gjennom om noe av denne logikken skulle vært flyttet til dokumentet eller bruker-objektet. For eksempel kunne vi ha opprettet en metode på IDocument for å spørre dokumentet om det var visningsklart. Men da måtte vi ha gitt IDocument en dependency mot IConfiguration, som jo holder på ApprovalNeeded-informasjonen. Eller i alle fall sende inn denne informasjonene til dokumentet på en eller annen måte. Og dette er noe dokumentet ikke burde behøve å forholde seg til. Det samme gjelder for å sjekke sikkerhetsnivå basert på bruker, hvor man må kombinere informasjon fra dokument og bruker – i tillegg til noen hardkodede konstanter som absolutt burde vært isolert bedre.

Trekk ut flere metoder

public class Step3

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        if (DocumentIsReadyToBeShown(document))

        {

            if (SecurityLevelOkForUser(document, externalUser))

            {

                _sender.AddObject(document)

                    .ToEmail(externalUser.EmailAddress)

                    .Send();

            }

        }

    }

 

    private bool DocumentIsReadyToBeShown(IDocument document)

    {

        return DocumentIsDone(document) && AllowedIfFinal(document);

    }

 

    private static bool DocumentIsDone(IDocument document)

    {

        return document.Status != DocumentStatus.Draft

            && document.Status != DocumentStatus.ReleaseCandidate;

    }

 

    private bool AllowedIfFinal(IDocument document)

    {

        return document.Status != DocumentStatus.Final

            || !_config.ApprovalNeeded;

    }

 

    private static bool SecurityLevelOkForUser(IDocument document, IUser externalUser)

    {

        return document.SecurityLevel == DocumentSecurityLevel.Public

            || (document.SecurityLevel == DocumentSecurityLevel.Restricted

            && IsUserVeryImportant(externalUser));

    }

 

    private static bool IsUserVeryImportant(IUser externalUser)

    {

        return externalUser.TrustLevel >= 3

            || externalUser.YearlyContribution >= 10000;

    }

}

Som du forhåpentlig vis ser blir koden mer lesevennlig jo mer vi splitter den opp, hovedsaklig fordi metodenavnene dokumenterer hva testene egentlig betyri domenemodellen vår. Men det gjenstår noen problemer. For det første begynner det å bli “trangt om plassen” i klassen vår – det blir rotete og uoversiktelig med mange, små metoder. For det andre er det svært sansynlig at testene som disse metodene våre utfører også vil være nyttige andre steder, sansynligvis i andre klasser. Jeg får derfor lyst til å trekke dem ut herfra…

Specification pattern (enkel variant)

Da er det endelig tid for å ta en titt på Specification mønsteret. Først trenger vi et enkelt interface:

public interface ISpecification<T>

{

    bool IsSatisfiedBy(T candidate);

}

Interfacet trenger bare én metode, og den kaller vi IsSatisfiedBy. Og nå kan vi implementere noen forretningsregler:

public class DocumentIsDoneSpecification : ISpecification<IDocument>

{

    private IConfiguration _config;

    public DocumentIsDoneSpecification(IConfiguration config)

    {

        _config = config;

    }

 

    public bool IsSatisfiedBy(IDocument document)

    {

        return DocumentIsDone(document) && AllowedIfFinal(document);

    }

 

    private static bool DocumentIsDone(IDocument document)

    {

        return document.Status != DocumentStatus.Draft

            && document.Status != DocumentStatus.ReleaseCandidate;

    }

 

    private bool AllowedIfFinal(IDocument document)

    {

        return document.Status != DocumentStatus.Final

            || !_config.ApprovalNeeded;

    }

}

 

public class DocumentHasOkSecurityLevelForUserSpecification

    : ISpecification<IDocument>

{

    private IUser _user;

    public DocumentHasOkSecurityLevelForUserSpecification(IUser user)

    {

        _user = user;

    }

 

    public bool IsSatisfiedBy(IDocument document)

    {

        return document.SecurityLevel == DocumentSecurityLevel.Public

            || (document.SecurityLevel == DocumentSecurityLevel.Restricted

            && IsVeryImportantUser);

    }

 

    private bool IsVeryImportantUser

    {

        get

        {

            return _user.TrustLevel >= 3

                || _user.YearlyContribution >= 10000;

        }

    }

}

I konstruktørene tar spesifikasjonene inn sine eksterne avhengigheter – all den informasjonen de måtte behøve for å avgjøre om et hvilket som helst dokument tilfredstiller forretningsregelen spesifikasjonen implementerer. Nå har vi et sentralt sted, en klasse, hvor regelen er definert, og som kan brukes hvor som helst i koden vår.

Ok, la oss forsøke å bruke disse reglene da:

public class Step4

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        IList<ISpecification<IDocument>> criterions = new List<ISpecification<IDocument>>();

        criterions.Add(new DocumentIsDoneSpecification(_config));

        criterions.Add(new DocumentHasOkSecurityLevelForUserSpecification(externalUser));

 

        foreach (var criterion in criterions)

            if (!criterion.IsSatisfiedBy(document))

                return;

 

        _sender.AddObject(document)

            .ToEmail(externalUser.EmailAddress)

            .Send();

 

    }

}

I denne versjonen av SendIfPossible-metoden lager vi først en liste av spesifikasjoner, og kontrollerer så hver av dem før vi sender dokumentet til brukeren. Dette er bare en måte å bruke spesifikasjoner på. Den faktiske listen kunne ha blitt konfigurert et annet sted, og for eksempel inneholdt ulike criterier i ulike situasjoner.

Komponerbare spesifikasjoner

La oss utvide konseptet noe. Vi ønsker nå å kunne kombinere ulike spesifikasjoner, og dermed lage nye, komponerte spesifikasjoner på et høyere nivå. La oss utvide ISpecification og i tillegg lage en abstrakt klasse om arver fra dette:

public interface ISpecification<T>

{

    bool IsSatisfiedBy(T candidate);

    ISpecification<T> And(ISpecification<T> other);

    ISpecification<T> Or(ISpecification<T> other);

    ISpecification<T> Not();

}

 

public abstract class CompositeSpecification<T> : ISpecification<T>

{

    public abstract bool IsSatisfiedBy(T candidate);

 

    public ISpecification<T> And(ISpecification<T> other)

    {

        return new AndSpecification<T>(this, other);

    }

 

    public ISpecification<T> Or(ISpecification<T> other)

    {

        return new OrSpecification<T>(this, other);

    }

 

    public ISpecification<T> Not()

    {

        return new NotSpecification<T>(this);

    }

}

Her har vi lagt til tre operasjoner på spesifikasjon: And, Or og Not. Den abstrakte klassen implementerer disse, en implementasjoner som vil være gjeldene for alle konkrete forretningsregler vi implementerer. Den abstrakte klassen benytter tre nye klasser som vi også må definere:

public class AndSpecification<T> : CompositeSpecification<T>

{

    private ISpecification<T> One;

    private ISpecification<T> Other;

 

    public AndSpecification(ISpecification<T> x, ISpecification<T> y)

    {

        One = x;

        Other = y;

    }

 

    public override bool IsSatisfiedBy(T candidate)

    {

        return One.IsSatisfiedBy(candidate) && Other.IsSatisfiedBy(candidate);

    }

}

 

public class OrSpecification<T> : CompositeSpecification<T>

{

    private ISpecification<T> One;

    private ISpecification<T> Other;

 

    public OrSpecification(ISpecification<T> x, ISpecification<T> y)

    {

        One = x;

        Other = y;

    }

 

    public override bool IsSatisfiedBy(T candidate)

    {

        return One.IsSatisfiedBy(candidate) || Other.IsSatisfiedBy(candidate);

    }

}

 

public class NotSpecification<T> : CompositeSpecification<T>

{

    private ISpecification<T> Wrapped;

 

    public NotSpecification(ISpecification<T> x)

    {

        Wrapped = x;

    }

 

    public override bool IsSatisfiedBy(T candidate)

    {

        return !Wrapped.IsSatisfiedBy(candidate);

    }

}

Vi kan nå implementere våre forretningsregler ved å arve fra CompositeSpecifications. Jeg overlater dette som en oppgave til leseren.., i stedet hopper vi direkte til slutten og ser hvordan dette kan brukes:

public class Step5

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        var isDone = new DocumentIsDoneSpecification(_config);

        var okSecurity = new DocumentHasOkSecurityLevelForUserSpecification(externalUser);

 

        ISpecification<IDocument> sendCriterion = isDone.And(okSecurity);

 

        if (sendCriterion.IsSatisfiedBy(document))

        {

            _sender.AddObject(document)

                .ToEmail(externalUser.EmailAddress)

                .Send();

        }

    }

}

Nå har vi laget et slags “fluent interface” (eller mer presist brukt method chaining), og vi kan dermed kjede sammen forretningsregler slik som i dette banale eksempelet: isDone.And(okSecurity). Fantasien din har nok allerede begynt å jobbe med eksempler som har mange flere forretningsregler. Og for å sjekke om kriteriene er oppfylt trenger vi bare kalle IsSatisfiedBy på den komponerte spesifikasjonen.

Dette mønsteret åpner opp mange, spennende muligheter. Koden vår kan forholde seg til det enkle ISpecification interfacet for å sjekke kriterier, uten å vite noe om hvordan disse kriteriene har blitt konstruert. Spesifikasjoner kan bli sendt fra sted til sted, og nye spesifikasjoner kan kjedes til på ulike steg i prosessen – samtidig som koden holdes ren og ryddig.

Knagger: , , , ,

Inspirasjonskilder: DDD:Specification pattern blog post av Casey Charlton, Specification pattern på Wikipedia

Kategorier: OO-design/clean code.
RSS feed for kommentarene. Tilbaketråkk.

7 kommentarer til “T-Man tipser om Specification pattern”

  1. Thor Halvor Says:

    Takk for en fin blogpost :) Har nok ofte lett for at vi implementerer businessregler litt rotete noen ganger!.. Hadde alle kodet mer som deg så hadde det vært fine kodebaser rundt omkring :)

  2. Torbjørn Says:

    Vel, det er ikke nødvendigvis sikkert at koden jeg vanligvis produserer er så mye bedre enn andres. Men jeg er opptatt av god kode, og har nå i alle fall bestemt meg for å jobbe med det og gjøre en innsats. Alle kan forbedre seg selv litt hver dag, det krever bare vilje.

    Hyggelig du likte posten.

  3. Torbjørn Says:

    Jeg fant en blogpostsom tar for seg hvordan man kan kombinere specification pattern og extension metods for å gjøre kallet til IsSatifiedBy() mere elegant. Ta en titt:

    http://dimebrain.com/2008/12/improving-the-readability-of-the-specification-pattern.html

  4. Thomas Ferris Nicolaisen Says:

    God post! Har sett patternet før, men denne er fin intro/reminder på åssen det fungerer.

  5. chwlund Says:

    veldig bra post! helt klart en veldig bra måte å lage enkel, forståelig og lett utvidbar kode på!

  6. chwlund Says:

    veldig bra post! dette er helt klart en smart måte å gjøre slik kode enklere, mer forståelig og ikke minst mer utvidbar og vedlikeholdbar!

  7. Komponere funksjoner i F# Says:

    [...] Denne teknikken har noen interessante bruksområder, sikkert mange flere enn dem jeg har sett til nå. Det fikk virkelig hjernen min til å begynne å jobbe, og drømmen jeg hadde i natt var følgelig veldig spesiell. En av ideene jeg fikk var at jeg ganske enkelt kan kombinere flere funksjoner, for å oppnå omtrent det samme som composable specification pattern i objektorientert programmering. [...]

Skriv en kommentar

Tillatte tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Siste kommentarer

Torbjørn
PS: Takk til Børge Hansen, som delte SCARF-modellen med meg!...
Børge Hansen
Denne likte jeg veldig godt. Du skriver godt og har gode betraktninger  Keep it up – flere trenger å tørre å lære mer om ledelse – du l...
Tormod
Er egentlig ikke overrasket. F# sin fortè er programmererens produktivitet/kvalitet og anledning til parallell kjøring. Men kjøremotoren har ...
Stian
Ville også prøvd med et større problem (x100 eller x1000 f.eks). Når man snakker så små brøkdeler av et sekund som her så kan tiden for en ell...
Torbjørn
Har ikke sjekket - tar en titt i morgen hvis tid :)...
Einar W. Høst
Mhp tco: hva sier ILSpy?...
Torbjørn
Har ikke sett noe på PSeq før, men kjenner til den typen funksjoner fra blant annet Clojure. Og problemet med slike funksjoner i sammenhenger som de...
Håvard
Veldig bra sammenligning! Har du sett på ytelsen av PSeq.* fra powerpakken? Tipper den vil gi performancehit på små mengder, men kan kanskje resul...
Torbjørn
Jeg kom på en demonstrasjon-variant til jeg burde inkludere, nemlig bruk av list comprehension (en type computation expression (også kalt monads)). ...
Einar W. Høst
Interessant, det blir en trade-off mellom eleganse og fart på en måte. Den funksjonelle løsningen med vanlig filter er ren og pen, mens den imperat...
Creative Commons-lisens
Innholdet på denne bloggen er tilgjengelig under Creative Commons Navngivelse-Ikkekommersiell-DelPåSammeVilkår 3.0 Norge lisens.

Programmeringsbloggen
Kjempekjekt.com

© 2006-2013 Torbjørn Marø

Jeg har vært en profesjonell programmerer siden 1999, og dette er min blogg. Målet med bloggen er å stimulere meg selv og alle andre til kontinuerlig eksperimentering og læring.

Jeg forsøker å være allsidig, og programmerer blant annet i C#, Ruby, Erlang og Clojure.

Jeg praktiserer TDD og andre smidige utviklingspraksiser. Jeg er opptatt av kvalitet og ren kode.

Dette og ganske mye mer kan du lese om på denne bloggen!