Thursday, February 11, 2010

Hur jag använder DDD i mitt projekt

Jag har haft möjlighet att praktisera domändriven design i ett av mina uppdrag, och jag tänker försöka beskriva hur det är att jobba på det sättet, varför det kan leda till högre kvalitet och produktivitet samt dela med mig av några tips och erfarenheter.

Med "domän" i domändriven design menar vi verksamhetsområdet, det företaget ägnar sig åt. I ett av mina uppdrag har jag verkat inom domänen elektroniska passersystem där jag arbetat med en hyfsat stor produkt med några hundra tusen rader kod och några tiotal manår utveckling bakom sig, den interagerar med olika typer av användare och andra system med hjälp av en varierad flora av gränssnitt och är sedan länge i produktion med fler än tusen installationer runt om i världen. En liten del av den här modulariserade produkten handlar om att hantera bokning av saker som konferensrum, tvättmaskiner och tennisbanor.

Inom den här modulen kan vi snäva in domänbeskrivningen till bokning, rätt och slätt. Det råkar vara en utomordentligt bra domän för att introducera DDD i en organisation, och dessutom för att skriva en artikel om arbetet, eftersom det innehåller ett visst mått av komplexitet men samtidigt är någorlunda bekant för de flesta. Problemet vi var ute efter att lösa med vår modell är att hålla reda på vem som får boka vad och när, och dessutom att leverera information till andra delar av systemet som fysiskt styr passage genom dörrar, utifrån gjorda bokningar.

Den här modulen stod i tur att genomgå större förändringar av flera orsaker, men man kunde konstatera att den var väldigt svår att jobba med, utan något entydigt centrum för affärslogik som man kunde studera och testa. Det var utspritt över hela det vertikala ledet, från JSP-sidor till SQL-satser. Genomgående var att typningen var väldigt primitiv, så man kunde se metodsignaturer som innehöll både tre, fyra och fem long som ibland var primärnycklar och ibland millisekunder.

PeriodConfiguration getPeriodConfiguration(long personId,
                                           long resourceGroupId,
                                           long bookersAccessCategoryId,
                                           long from,
                                           long to)


De klasser som fanns hade sällan något beteende eller några som helst begränsningar i vad de kunde innehålla, utan var normalt tomma databehållare med ganska yxigt (som det ofta blir) översatta engelska namn. Kort sagt, det fanns helt enkelt ingen egentlig modell, och det är tyvärr ingen ovanlig situation.

Det här ville vi förändra. Vi ville bygga en modell av hur bokningssystemet skulle fungera, som var förändringstålig och robust, och gick att testa och studera helt utan infrastruktur. En modell som vi kunde visa upp och resonera kring tillsammans med en domänexpert, kanske i skissform men allra helst hela vägen ner i koden.

Domänexperten och det gemensamma språket

Tillgång till en domänexpert är en av den viktigaste förutsättningen för att kunna bedriva domändriven design. Man kan och bör tillgodogöra sig så mycket baskunskap i ämnet som möjligt på egen hand, eftersom domänexperten ofta är en upptagen person, men för att verkligen få maximal utväxling i koden behöver man bolla med någon som verkligen vet hur det fungerar. Vårt team hade tur, vi har nämligen relativt god tillgång till en domänexpert som samtidigt är vår produktägare. Bokningsdomänen är som sagt rätt snäll, men som i varje domän och varje produkt finns det ett antal egenheter. I vårt fall handlar det om att känna till hur äldre och konkurrerande system används och vad de klarar av, vad som är efterfrågat bland användarna och hur interaktionen med existerande och framtida hårdvara ska fungera, bland annat.

Vår domänexpert tycker att det är roligt och givande att delta i modelleringsdiskussioner, och jag tror att det inte är alltför sällsynt om man utnyttjar tiden effektivt. Man bör komma väl förberedd utan att ha surrat fast sig vid bestämd idé på förhand. Ställ konstruktiva frågor som kan blottlägga avgränsningar och viktiga regler. "Förekommer det nånsin att...", "Kan man betrakta X och Y som olika varianter av Z?", "Händer det att A och B samtidigt är tomma?" och så vidare. Ge domänexperten utrymme, och undvik att hemfalla åt datatekniska termer utan var uppmärksam på hur han eller hon uttrycker sig. 


En annan viktig ingrediens i domändriven design är det gemensamma språket. En modell som är både är djupt förankrad i hur domänen fungerar och som uttrycks i ett språk som används både av domänexperten, av programmerarna sinsemellan och som dessutom återfinns i koden blir väldigt förändringstålig. Ofta ligger det gemensamma språket nära fackspråket, men det kan även innehålla nya ord som behövs för att utrycka den typ av struktur man behöver för att skriva mjukvara, eller stryka några ord som betyder ungefär samma sak för att fokusera på ett enda med kristallklar betydelse. Vi baserade vårt språk i stor utsträckning på vad saker och ting hette i de användargränssnitt som redan fanns, eftersom det med tiden hade satt sig hos alla som kommit i kontakt med systemet. Min erfarenhet är att man ska akta sig för att försöka "rätta till" ologiska termer som är väl etablerade, eftersom det blir mycket svårare att upprätthålla koplingen mellan hur man pratar och hur koden är namngiven.

Men att hitta ett gemensamt språk är inte en helt och hållet passiv övning. Ibland identifierar man ett begrepp som kanske inte finns explicit i fackspråket eller i något gränssnitt, men som är väldigt användbart är man bygger en strukturerad modell som ett datorprogram. Ett bra exempel från vårt projekt är bokningsförfrågan. Både när man vill veta vad som är möjligt att boka och när man faktiskt utför en bokning så inkluderar det vem som bokar, vad man vill boka och när man vill boka det. Det här flöt liksom omkring i våra diskussioner och lät ungefär "Ok, vi säger att en lägenhet vill boka tvättstuga 3 den 22:a oktober mellan 14.00 och 18.00...", så vi introducerade det som ett explicit begrepp vilket betyder två saker: det är någonting som både programmerare och domänexpert är överens om vad det betyder, och det finns en klass i domänmodellen med samma namn:

public class Bokningsförfrågan implements ValueObject<Bokningsförfrågan> {

    Bokningsobjekt bokningsobjekt;
    Bokare bokare;
    Pass pass;
       ...
}


Vi karaktäriserade det som ett värdeobjekt, value object, och speglade den språkliga definitionen i koden genom att konstruktorer och jämförelseoperationer som equals() och hashcode() garanterar att man aldrig stöter på en bokningsförfrågan som saknar exempelvis bokare, och där två bokningsförfrågningar med från samma bokare som avser samma bokningsobjekt vid samma tidpunkt i alla avseenden kan betraktas som lika. På så vis har vi höjt abstraktionsnivån från strängar och heltal till en klass med tydlig karaktär och beteende, och som speglar någonting som vi kan prata med domänexperten om. När önskemål om förändringar kommer i framtiden kommer man att kunna svara på dem mycket snabbare, och vår kod blir mer robust.               

Domänmodellen är som synes programmerad på svenska i så stor utsträckning som möjligt, ett rätt så kontroversiellt beslut i teamet och tror jag bland programmerare i allmänhet. Jag var själv motståndare till det för inte så länge sedan, men min erfarenhet är att det absolut är värt att göra det om alla inblandade talar svenska. Översättningar blir sällan perfekta, och framför allt förlorar man den intima kopplingen mellan det talade och skrivna ordet man har om koden är på svenska. En övning till läsaren: heter det a booking eller a reservation på engelska?

En kraftfull modell

Nu ska vi titta på ett litet men matnyttigt exempel på hur man kan gå från ett uttalande av domänexperten till konkret kod om man har en kraftfull objektmodell, lättarbetade stödbibliotek och ett gemensamt språk. Så här skulle det kunna låta:


Man ska kunna ställa in ett bokningsobjekt så att man högst får ha till exempel tre aktiva bokningar under samma månad.


Det här är en av många bokningsregler som styr huruvida en bokning kan ske eller inte. Var och en av dessa regler implementerar gränssnittet Bokningsregel som kan en enda sak: att svara ja eller nej på om den tillåter en bokningsförfrågan (den skarpsynte anar kanske att det handlar om Specification-mönstret). Vi behöver alltså titta på det aktuella bokningsobjektets inställningar, och om det finns en gräns för hur många bokningar en bokare får ha per månad, kontrollera att bokaren har högst så många bokningar på det här bokningsobjektet under den månad som passet infaller. Låter rätt enkelt, och definitivt någonting som man kan resonera kring tillsammans med domänexperten. Så här tydlig kan koden bli som faktiskt utför den här kontrollen:
          

public class MånatligBegränsning implements Bokningsregel {

    @Override
    public boolean tillåter(Bokningsförfrågan bokningsförfrågan) {
        Bokningsobjekt bokningsobjekt =
bokningsförfrågan.bokningsobjekt();
        Regelinställningar regelinställningar = bokningsobjekt.inställningar();
        Integer gräns = regelinställningar.perMånadPerBokare();

        if (ejSatt(gräns)) {
            return true;
        } else {
            TimeInterval passetsMånad = månadFörPasstart(bokningsförfrågan.pass());
            Bokare bokare =
bokningsförfrågan.bokare();
                    List<Bokning> bokningarUnderMånad = bokare.listaAktivaBokningar(bokningsobjekt, passetsMånad);

            return bokningarUnderMånad.size() < gräns;
        }
    }
   
    private boolean ejSatt(Integer gräns) {
        return gräns == null;
    }


    private TimeInterval månadFörPasstart(Pass pass) {
        TimePoint passStart = pass.somIntervall().start();
        CalendarDate datumFörPasstart = passStart.calendarDate(TIDSZON);

        return datumFörPasstart.month().asTimeInterval(TIDSZON);
    }

}

Huvuddelen av jobbet görs av Bokare-klassen, i den metod som listar alla bokningar för ett visst bokningsobjekt under ett intervall, exempelvis en kalendermånad. Den här koden ligger så nära domänexpertens förståelse av hur systemet fungerar att det nästan går att parprogrammera tillsammans. Vi har gått igenom och exekverat scenariotester på sprintavslut, vilket fungerade riktigt bra och var en välkommen omväxlig. Kanske inte något man gör varje gång, men det kan definitivt föra delar av organisationer närmare varandra och öka utomståendes förståelse och intresse för mjukvaruutveckling.

Fokusera

En sak som man brukar betrakta som del av det man kallar strategisk design är att att identifiera vad som är kärnan i verksamheten, core domain, och fokusera på det. Det låter självklart, men det är tyvärr väldigt vanligt att man lägger ner massor av arbete på saker som tillför liten eller ingen affärsnytta till produkten, antingen omedvetet eller för att man tycker att man hittat ett intressant problem.

För oss är kärnverksamheten (under det här projektet och i den här delen av produkten) det vi formulerade tidigare: att hålla reda på vem som får boka vad och när, och dessutom att leverera information till andra delar av systemet som fysiskt styr passage genom dörrar, utifrån gjorda bokningar. Företaget säljer väl integrerade helhetslösningar med både mjuk- och hårdvara, och allt måste mynna ut i att dörren till tennishallen öppnas det klockslag som bokningen är gjord. 

Fundamentet i vår affärslogik är förstås starkt knutet till tidshantering - datum, månader, tidpunkter, intervall och så vidare. Det är dock inte något som är unikt för vår produkt, eller ens för bokningsdomänen. Det hör till programmeringens allmängods och ett exempel på vad man på engelska kallar generic subdomain. Här vill vi lägga så lite energi som möjligt och istället använda färdiga och kraftfulla bibliotek. Att hacka på tid- och datumhantering i Java är lite av spel mot öppet mål, men jag påminner ändå om hur det ofta ser ut när man försöker bygga något med standardbiblioteket:

   
            Calendar cal = getCalendar(start.getTimeInMillis());
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            long startTimeMidnight = cal.getTimeInMillis();

            ...

            if (start.getTimeInMillis() == startTimeMidnight || bp.getFromTime() >= start.getTimeInMillis()) {
               periods.add(bp);
            }

            ...
 
            /* next day */
            start.set(Calendar.DAY_OF_YEAR, start.get(Calendar.DAY_OF_YEAR) + 1);
            startingPoint = Long.valueOf(start.getTimeInMillis());
      

Vi valde att jobba det väldigt trevliga Time and Money, ett fritt bibliotek som huvudsakligen är skrivet av Eric Evans, i väntan på att ett nytt och bättre standardbibliotek ska dyka upp. 

I den bokningsregel som begränsar antal bokningar per månad behöver vi veta vilken månad som passet infaller. Ett pass börjar i en tidpunkt (TimePoint), som är någonting med minimal längd och maximal precision, exempelvis 2009-09-09 09:09:09.000 GMT. Den tidpunkten infaller på något datum i varje tidszon, som är ett väldigt annorlunda begrepp än tidpunkt och representeras av en explicit typ, CalendarDate.

I vårt exempel och i tidszonen GMT+1 innebär det 2009-09-09. Slutligen infaller ett datum såklart under någon månad, här september 2009, en instans av klassen Month:       


         TimePoint passStart = pass.somIntervall().start();
        CalendarDate datumFörPassStart = passStart.calendarDate(TIDSZON);
        Month månad = datumFörPassStart.month();

 
Vår klass Bokare kan lista bokningar inom vilket tidsintervall som helst - en dag, en månad eller från idag och två veckor framåt, och på tre tydliga rader kod kan vi klara av att formulera vår fråga till Bokare-klassen. Genom att utnyttja Time and Money frigör vi massor av utvecklingstid som vi istället kan lägga på det som verkligen är unikt i vår produkt, och koden kan hållas snygg och prydlig.