Domeingedreven ontwerp heeft tot doel een zo accuraat mogelijke afspiegeling te maken van de context waarin bepaalde bedrijfsprocessen draaien in een model. Tijdens het maken van deze modellen wordt nauwelijks of geen rekening gehouden met zaken als responsetijden of geheugengebruik. Dat is in het vroegste stadium ook niet interessant.
In tweede instantie, bij het daadwerkelijk executeren van een model in een productiesysteem, dienen deze aspecten wel degelijk meegenomen te worden. In dit artikel ga ik in op een aantal onderdelen binnen het domeinmodel zelf en hoe de keuzes die hierin gemaakt worden implicaties kunnen hebben op het uiteindelijke systeem als totaal. Een model dat – in eerste instantie – een goede representatie vormt van de te automatiseren bedrijfsvoering is bijvoorbeeld niet per definitie schaalbaar. Ik zal twee afzonderlijke benaderingen illustreren aan de hand van een veelvoorkomende praktijksituatie en deze benaderingen vergelijken op schaalbaarheid.
Case
Laten we het volgende modelfragment onder de loep nemen. We hebben een bepaalde business die werkt vanuit geografische gebieden (Area’s) waarin zich klanten (Customer’s) bevinden. Een klant bestaat alleen binnen een gebied. Elk van deze klanten heeft een aantal niet nader te specificeren transacties aan zich geassocieerd met een bepaalde waarde en een datum (Transaction’s).

Fig. 1: Class-model van de case
Eén bepaalde vraag aan het systeem is het geven van de opgetelde waarde van de transacties binnen een gebied van het huidige jaar. Dit zijn vormen van gegevensaggregatie die in heel veel businessdomeinen op een bepaalde manier terugkomen en vaak real-time vanuit een systeem moeten kunnen worden samengesteld. De methode is gedefinieerd in de Area class als GetTransactionValueThisYear().
Volgens de verantwoordelijkheidsgedreven ontwerpmethode – met het uitgangspunt dat objecten slechts één verantwoordelijkheid mogen hebben – ontstaan er vaak iteratieve sequences zoals in figuur 2 weergegeven. Zodra er een aggregatievraag gesteld wordt aan bijvoorbeeld de Area dan wordt er een iteratie gestart over de geassocieerde Customer objecten. Figuur 2 geeft twee sequences weer; die van het toevoegen van een transactie door de business-actor, en die van het opvragen van een totaalcijfer door de business-actor.
Objecten mogen slechts één verantwoordelijkheid hebben
De AddTransaction() aanroep waarmee de eerste sequence begint is rechttoe rechtaan. Het Customer-object krijgt bepaalde gegevens betreffende de transactie binnen, creëert het object en associeert het met zichzelf. Er vindt verder geen iteratie plaats en deze sequence schaalt dus lineair.
In de tweede sequence stelt de business-actor de vraag GetTransActionValueThisYear() aan een Area-object. Deze area start een iteratie over al zijn customers met de vraag GetTransactionValueForYear(2009); we zijn immers uitgegaan van het doorgeven van de vraag als daar zelf geen afdoende antwoord op gegeven kan worden. Op zijn beurt zal deze customer een iteratie starten over al zijn geassocieerde Transaction-objecten met een datum in het jaar 2009 met de vraag GetValue(). De customer houdt immers zelf geen transactiewaarde vast zelf en vraagt het na bij de daarvoor verantwoordelijke objecten. Dat zijn in dit geval de transactions die aan de betreffende customer geassocieerd zijn.

Fig. 2: Normale strategie
De verantwoordelijkheden zijn mooi verdeeld en ieder object is klein en schoon, met een eigen verantwoordelijkheid. Toch leidt deze benadering in de praktijk soms tot problemen.
Stel dat we voor een bepaalde Area N klanten hebben. Deze klanten hebben gemiddeld M transacties. Als we de vraag GetTransActionValueThisYear() stellen aan het systeem zullen er dus N X M objecten geraakt worden. Dit kan inhouden dat er bij bijvoorbeeld 100 klanten met ieder 1000 transacties (heel kleine aantallen eigenlijk) toch al snel 100.000 objecten geraakt worden om de vraag te beantwoorden. Bij een in-memory operatie is dat geen groot probleem, maar helaas is het vaak zo dat niet alle objecten in het geheugen geladen zijn. Lazy loading, het principe van pas ophalen van objecten als dat nodig is, biedt hier overigens geen uitkomst. Het gehele composiet Area wordt hier tot in de bladeren (de Transaction objecten) geraakt. Het stellen van de vraag GetTransactionValueThisYear() verandert de toestand van het systeem, alleen binnen de context van het model, niet. Als we een stap verder kijken dan het model zien we dat er onder water ontzettend veel veranderd aan de toestand van bepaalde objecten.
Het is duidelijk dat deze benadering valide is, maar niet schaalbaar.
Alternatieve strategie
Bertrand Meyer heeft bij de ontwikkeling van de taal Eiffel een principe gehanteerd dat ook wel Command-Query Separation genoemd wordt. Dit principe gaat uit van twee soorten vragen die gesteld kunnen worden aan een systeem: een command of een query. Bij een command, in feite een opdracht aan het systeem, is er een bepaalde verandering in toestand. Bij een query wordt er enkel informatie opgevraagd. Deze vraag heeft nooit een toestandsverandering tot gevolg.
Command-Query Separation principe: er kunnen twee soorten vragen gesteld worden, nl. een command of een query
Als we alleen naar het model kijken dan geldt dit principe in de voorgaande oplossing. Omdat ons model echter in een bepaalde context leeft, is het in dit geval zo dat de vraag GetTransactionValueThisYear() zodanig veel objecten raakt dat het wel degelijk een toestandsverandering tot gevolg heeft. We kunnen deze toestandsverandering vanuit het model elimineren.
Uitgaande van het Command Query Separation principe dienen we het ‘werk’ te doen in het command – in dit geval dus de AddTransaction() methode. We notificeren op moment van toevoegen van een transaction meteen het root-object, de area. Dit kunnen we doen door een TransActionAdded() message met de binnengekomen gegevens te sturen naar de area. In de area dient nu wel een lokaal attribuut in de vorm van een lookup table of iets dergelijks opgenomen te worden om het object zelf de vraag te kunnen laten afhandelen. Dit is in figuur 1 ook al wel opgenomen in de vorm van het TransactionValues-object. We zien nu de volgende twee sequences ontstaan:

Fig. 3: Alternatieve Strategie
We nemen hiermee een kleine penalty qua complexiteit van de AddTransaction() message naar de customer (het notificeren van de area via TransactionAdded()) voor lief. Deze strategie biedt namelijk een zeer groot voordeel bij de aanroep van GetTransActionValueThisYear(). Het area-object kan dit nu lokaal afhandelen: bij het stellen van deze vraag zal er dus geen delegatie naar andere objecten meer plaatsvinden.
Het voordeel dat we hiermee in het productiesysteem kunnen behalen kan zeer significant zijn. Toch moet deze minimale toegevoegde complexiteit niet onderschat worden. In feite wordt er redundante informatie aan het model toegevoegd om een soort cache in te bouwen. Ga hier voorzichtig mee om en definieer de interfaces van de objecten eenduidig. Uiteraard is het uitvoerig testen van deze collaboraties zeer aan te bevelen.
Voor de buitenwereld is er niets veranderd…
Een goed object-georiënteerd ontwerp kan hierbij wel het voordeel opleveren dat we tussen de twee geschetste strategieën kunnen schakelen. Als we de twee sequence-diagrammen naast elkaar leggen zien we dat vanaf de buitenwereld gezien er niets verandert aan de vragen die we stellen.
Conclusie
De twee vergeleken modelleerstrategieën verschillen op verschillende vlakken. Hoewel de eerste strategie, die van het zoveel mogelijk delegeren van verantwoordelijkheid, de meest pure en schoonste is, kleven er in de praktijk mogelijk nadelen aan. Zo is goede analyse van de hoeveelheden gegevens noodzakelijk om de schaalbaarheid te garanderen.
De tweede strategie, gebaseerd op het uitgangspunt van command-query separation, kan schaalbaarheidsproblemen helpen te voorkomen. Het impliceert in dit geval wel het toevoegen van redundante verantwoordelijkheden. Of deze nadelen opwegen tegen de schaalbaarheidsvoordelen die deze benadering met zich meebrengt, moet per geval bekeken worden. De praktijk wijst uit dat vooral in systemen die schaalbaar van opzet moeten zijn, zoals webapplicaties, de voordelen ruimschoots opwegen tegen de nadelen.