October 20, 2008

Domain Driven Design och Dependency Injection

För ett tag sedan var jag med i ett projekt, där jag fick möjligheten att använda domän driven design. Det var spännande, men inte helt problemfritt efter ha läst POJO in Actions (Manning Publications), Applying Domain-Driven Design and Patterns (Addison-Wesley Professional) och valda bitar av Domain-Driven Design (Eric Evans), hittade jag fortfarande inte svaren på mina frågor. De problemen som jag stötte på när jag skulle realisera DDD i verkligheten vill jag dela med er, men framförallt höra era synpunkter.


Vad är då domän driven design - DDD? DDD var framtagen från en projektledares ögon, som hade problemet att utvecklare och kravställare/affärsanalytiker pratade förbi varandra. Detta ledde till missförstånd och värdefull tid gick förlorad. Detta är inget nytt och många har lösningen på detta, varav RUP är en.

Men vad DDD skiljer sig mot traditionella metoder är enkelheten. Man skulle bara behöva ett diagram i designdiskussioner mellan utvecklare och kravställare/affärsanalytiker – en domän modell. Detta kan låta som det blir klottrigt, men syftet med detta diagram är att enbart visa objekt som har förankring i funktionella krav och är av arkitekturisk vikt. Låt oss nu arbeta vidare med ett exempel för tydliggöra metodiken.


Anta att vi ska bygga ett biljettbokningssystem åt ett biografföretag och vi har följande krav.

  1. Söka efter föreställningar.
  2. Boka föreställning.
  3. Registrera användare.
  4. Kontrollera kreditkort med 3-partsystem.
  5. Skicka skriftlig bokningsbekräftelse med 3-partssystem.

Efter att ha läst krav så har vi identifierat följande substantiv, som blir våra domänobjekt:

  • Föreställning
  • Användare

Av funktionella och arkitekturiska och skäl lägger vi även till:

  • Salong
  • Stol
  • Bokning
Låt oss nu rita domän modellen.


I domänmodellen finns det flera designbeslut man kan göra, men jag vill inte fastna i dessa detaljer här, men ett anmärkningsvärd beslut är att duplicera bokningar, dvs. man skapar upp lika många bokningsobjekt som det finns stolar, med flaggan bokad till false, när man skapar en föreställning. Anledning är att snabba upp sökningen av lediga stolar och att det blir enklare att hantera flera samtidiga bokningar. Slutligen så är det rätt osannolikt att antal stolar ändras över tiden.


Nu är det dags att lägga till metoder/funktionalitet, men innan dess behöver vi även lägga till persistenshantering och interaktion med 3-partssystemen. Vi inför 3 facade klasser, som kapslar in detta:

  • Persistens
  • Kreditkort
  • Bekräftelse


Nu börjar problemen komma. Enligt DDD så vill man knyta logik och domän objekt nära varandra. I vårt fall skulle det innebära att Föreställningsobjektet skulle ha metoderna:

  • sökaFöreställning
  • bokaFöreställning

För Användare skulle det vara:

  • spara

Ett direkt val som jag inte heller tror Eric motstrider är att flytta ut alla direkta persistensanrop, till någon mera övergripande klass. I POJO in Actions kallar man dem tjänsteklasser.


Men hur gör man med den mera komplexa metoden bokaFöreställning? Till och börja kan vi bryta ner den:

  • Validera.
  • Är användare registrerad?
  • Finns önskade platser lediga?
  • Kontrollera kreditkort.
  • Genomför bokning.
  • Skicka bekräftelse.

Det vi ser är att de olika delarna behöver tillgång till våra facade klasser. Hur får de det? Vi vill definitivt inte hårdkoda detta, då vi vill kunna göra systemet testbart, samtidigt som vi vill dölja implementation av facaderna bakom interfaces, så de blir utbytbara.


Som jag ser det finns det 3 alternativt:

  1. Ha all logik i domänobjekten och skicka med facaderna som inparametrar.
    1. Nackdel: ohållbart i längden, då inparameterlistan kan bli väldigt lång.
  2. Ha set-metoder i domänobjektet, som man måste anropa innan logik metoder.
    1. Nackdel: inga kompileringsfel om man inte har anropat set-metod och enkelt att skapa fel, för inte insatta utvecklare.
  3. Ha all logik i domänobjekt, som INTE kräver facade interaktion och resterande kod i tjänsteklasser.
    1. Nackdel: man sprider ut logiken, vilket är det DDD försöker motsträva.
    2. Fördelar: Man får en naturlig uppdelning av komplexa och tunga metoder till flera små metoder, vilket leder till mera lättläst kod och gör valet av vilka metoder man skall modultesta naturligare.

Som jag ser det finns det inga andra vettiga alternativ än 3.


Vi har hittills inte pratat om Dependency Injection – DI, men innan dess skulle jag vilja fundera över när vi skapar domänobjekten. Och detta leder in oss till om vi skall använda våra domänobjekt som databärare genom hela applikationen, dvs. genom alla lagren eller inte. I vår enkla applikation är övriga lager här presentationslagret. Jag antar här att de flesta förespråkar Hibernate, som persistenslagerhanterare och där använder man samma modell rakt igenom.

I de flesta av system så är det nästan ett-till-ett förhållande mellan presentationslagret och domänmodellen och i dessa fall så ser jag inte att ett nytt databärarlager tillför något, därför rekommenderar jag att använda domänobjekten även i presentationslagret. Däremot så ska man INTE införa presentationsspecifika attribut till domänobjektet, utan oftast har man ändå någon presentationsmodell, som man lägger domänobjektet i och det är i denna presentationsmodell, som specifika presentationsattribut hör hemma.


Värt att notera är om vi inte hade valt tredje alternativet ovan, så hade jag inte rekommenderat att använda domänobjekten rakt igenom, för då hade man exponerat spara, sök, etc. logik/metoder i presentationslagret.


Så svaret på min fråga, var man skapar domänobjekten i en presentationsdriven applikation, är i presentationslagret. Om man däremot har ett integrationslager i form av Web Service eller JMS, så ska man istället använda sig av en gemensam överenskommen modell, som passar alla integrerande system och i det fallet, så måste man konvertera sin applikationsspecifika modell.


För att avsluta diskussionen med DI, så måste man nu blanda in hur man skall realisera transaktionshantering. Jag tänkte måla upp tre alternativ, men listan kan göras längre, men principen bör kvarstå.

  • Spring med enbart JTA
  • Spring med SessionBean
  • SessionBean utan Spring och DI

För att möjliggöra DI, så rekommenderar jag tjänsteklassen enbart har referenser till facadinterface och man vill använda SessionBean så får de hantera injection av beroenden. Detta leder till den mest flexibla lösningen och mest testbara.




1 comment:

Jowe said...

En viktig distinktion angående var logiken hamnar i DDD är skillnaden mellan domänlogik och programlogik. Det är inte alls så att all logik ska tryckas ner i domänlagret, den logik som handlar om hur programmet ska fungera och hur det ska använda domänlagret ("workflow" och koordinering är nyckelord), ska läggas i programlagret" (application layer).
De punkter du har med i metoden "boka föreställning" är som jag ser det en beskrivning av programlagret, där bara två av anropen kommer att beröra själva bokningsobjekten, din domänmodell. Så här menar jag:

public int boka()
{
valideraIndata();
if(användareRepository.findAnvändare(användarId)!= null)
{
Föreställning f = föreställningRepository.find(föreställningsId);
if (! f.isBokad(stolsnr) && kreditkortsService.kontrolleraKort(kortnr))
{
f.boka(användarId, stolsnr);
föreställningRepository.save(f);
mailService.skickaMail();
}
}
}

På detta sätt minskar problemet med att domänobjekten har behov av dina facader. Det kan förstås fortfarande hända, men eftersom de fallen inte är så frekventa så kan det faktiskt vara möjligt att göra ditt alternativ 1: att skicka med facaden (eller den service som behövs), in i anropet på domänen. Eller så hittar man en annan domänmodell, det finns oändligt många.

/Jöns