Lagen in de referentiearchitectuur
Door: Pieter van Boxtel, 6 October 2008In de referentiearchitectuur hebben we lagen onderkend, zoals je ze vaak tegenkomt in een J(2)EE applicatie; een presentatielaag voor presentatie en user interface logica, een servicelaag voor remoting en service gerelateerde faciliteiten, een businesslaag waar het echte werk gebeurt en een datalaag voor database mapping. Bij uitwerking van dit model kwamen echter een aantal knellende vragen naar boven rondom afhankelijkheden van het domeinmodel. Hoe kan de datalaag nu afhankelijk zijn van het domeinmodel dat zich in de businesslaag bevindt? Hoe kunnen we ongewenste koppelingen voorkomen? Tijd om een stapje terug te doen. Wat is nu eigenlijk een laag in de software architectuur?
In een lagenmodel wordt software opgedeeld in een aantal lagen die elk hun eigen nivo van abstractie hebben. In principe heeft een laag hierbij alleen kennis van de direct onderliggende laag. Omdat lagen elk een eigen abstractie, een eigen “aandachtsgebied” hebben, hebben ze ook een eigen levenscyclus en zijn ze gevoelig voor andere soorten wijzigingen. Door de applicatie in lagen onder te verdelen wordt de gevoeligheid voor wijzigingen gelokaliseerd en wordt de applicatie in zijn geheel beter onderhoudbaar. En dat is iets waar een software architect een warm gevoel van krijgt.
Het klassieke voorbeeld van een lagenmodel in software is het OSI model voor computer netwerken. Iets complex als internet is mogelijk omdat netwerk protocollen gelaagd zijn (SOAP bovenop HTTP bovenop TCP bovenop IP bovenop Ethernet). Ook voor software architectuur zijn lagen voorgesteld om de complexiteit te verminderen. Het boek “Pattern Oriented Software Architecture” [POSA] introduceert het concept van lagen in de architectuur als een pattern. POSA geeft als voorbeeld van een lagenmodel het OSI model. Het noemt ook kort de toepassing van het lagenmodel in informatiesystemen, maar gaat daar niet diep op in. De gezaghebbende (?) Java architectuur-bijbels als “Core J2EE Patterns” [Alur], “Patterns of Enterprise Application Architecture” [Fowler] en “EJB Design Patterns” [Marinescu] schetsen een opdeling van een applicatie in lagen. “Domain-Driven Design” [Evans] beschrijft hoe het lagen patroon toegepast kan worden in een domeinmodel. Maar tot zover de literatuur.
In een enterprise applicatie kun je, afhankelijk van de richting van waaruit je ernaar kijkt, verschillende soorten lagen onderscheiden:
-
à la OSI zijn er infrastructurele lagen; de applicatie gebruikt Java/JEE libraries die gebruik maken van een container en JVM die gebruik maken van een OS.
-
POSA, Fowler, et cetera onderkennen architecturale lagen voor informatie systemen / enterprise applicaties.
-
Multi-tiering brengt lagen aan in de deployment; een browser praat met een applicatieserver die met een database praat.
-
Evans beschrijft hoe je lagen kunt gebruiken om het domeinmodel te structureren.
Deze soorten lagen zijn onafhankelijk van elkaar en je zou ze weer kunnen geven in een multi-dimensioneel plaatje, ware het niet dat wij niet zo goed zijn in het tekenen en begrijpen van plaatjes met veel dimensies.
Architecten (met name architecten…) houden van simpele plaatjes. Ze beperken zich in de regel dus tot 1 dimensie; nummer 2 in bovenstaand rijtje. Dit is ook de dimensie waarin in de referentiearchitectuur wordt gekeken. Daarin onderkennen we zoals gezegd 4 lagen; Presentatielaag, Servicelaag, Bussinesslaag en Datalaag.
Echter, die laatste twee lagen, dat zit niet helemaal lekker… Hoe kan een component in de datalaag nu een domeinobject persisteren als het geen domeinobjecten kent? Volgens de lagendefinitie kent een laag immers de onderliggende laag, maar niet de bovenliggende. De oplossing die we in de referentiearchitectuur hebben gekozen blijft achteraf knagen. In de logische view hebben we het probleem genegeerd en in de implementatie view hebben we de domeinobjecten in een afzonderlijke “laag” naast de overige lagen getekend. Daarmee is het lagenplaatje meer een blokkenplaatje geworden met pijlen allerlei kanten op.
Hoe maken we van dit plaatje nu een correct lagenplaatje? Het antwoord is eigenlijk heel simpel; gum het onderscheid tussen business- en datalaag weg. Dit sluit mooi aan bij Evans; onze Data Access componenten zijn gewoon repositories die onderdeel zijn van het domein model. Weg met de datalaag dus.
Maar daarmee zijn we nog niet helemaal klaar. We hebben ook nog die service agents en wat te doen als we moeten koppelen met een database schema dat niet lekker aansluit op ons domeinmodel? Dit vraagt om infrastructurele logica waarmee we ons domeinmodel niet willen vervuilen. Hier hebben we weldegelijk een tussenliggend laagje voor nodig. Laat ik dit laagje Integratielaag noemen. In de Integratielaag vinden we good old DAO’s. Parameters van deze DAO’s zijn DTO’s die een representatie zijn van het datamodel waar de DAO aan koppelt. Ze geven dus geen domeinobjecten of afspiegelingen daarvan terug. De Integratielaag heeft, technisch maar ook conceptueel, geen enkele notie van hoe domeinobjecten eruit zien. Vertaling van extern datamodel naar domeinmodel is verantwoordelijkheid van de domeinlaag.
De Integratielaag hebben we alleen nodig als we willen koppelen met de grote boze buitenwereld. Zolang we doen wat we meestal doen, een databaseschema maken specifiek voor ons domeinmodel en dat met hibernate aan onze domeinobjecten koppelen, hebben we deze niet nodig. Hiermee kom ik uit op onderstaan plaatje. Dit lijkt sterk op het lagenmodel zoals iedereen het kent, maar wel met een kanttekening bij de toepassing van de Integratielaag. (En de businesslaag heb ik omgedoopt tot domeinlaag om te onderstrepen dat ik fan ben van DDD.)
In bovenstaand plaatje zijn ook de systeemgrenzen weergegeven. Wanneer we persisteren naar een database die binnen onze eigen systeemgrenzen valt, en die we naar believen aan kunnen passen, dan hebben we geen integratielaag nodig. Wanneer we moeten integreren met de grote boze buitenwereld dan hebben we (misschien) een integratielaag nodig om de domeinlaag “clean” te houden. In het plaatje zie je ook dat de servicelaag de plek is waar aanroepen vanuit een extern systeem opgevangen worden.
Resten nog twee vragen waar een wakkere lezer nu wellicht nog mee worstelt:
1) Waar zit de hibernatejar dan? Toch niet in de domeinlaag? Nou nee, dit is een infrastructureel ding dat je niet terugziet in dit lagenplaatje. Het zit in een andere dimensie (de eerste in bovenstaand lijstje), in een parallel universum zo je wilt. Het architecturale lagenplaatje toont niet alle spelers. Net zomin als dat de Sun SPARC Enterprise Server waar de applicatieserver op draait zichtbaar is in het plaatje, zijn alle gebruikte 3rd party libraries zichtbaar.
Een architectuurbeschrijving bevat verschillende views waarin de architectuur vanuit verschillende perspectieven beschreven wordt. Voor elk van deze views kunnen we een lagenmodel opstellen. In de deployment view tekenen we een plaatje waarin een plekje is voor onze Sun SPARC Enterprise Server. In de implementatie view kunnen we een plaatje tekenen waarin de hibernatejar zichtbaar is. Een veelgebruikte oplossing is om naast de lagen in bovenstaand plaatje een “common” laag te tekenen voor hibernate, spring en ander spul, maar zoals intussen duidelijk moge zijn ben ik daar geen voorstander (meer) van.
2) Een van de argumenten om de implementatie van persistentie in een afzonderlijke laag te stoppen is dat het dan mogelijk is om het persistentiemechanisme te vervangen door een ander mechanisme. Deze mogelijkheid wil je ons toch niet afpakken? Nee, natuurlijk niet, ik zou niet durven. Hoewel ik het eerste project nog tegen moet komen dat daadwerkelijk een persistentiemechanisme vervangt, hecht ik wel waarde aan encapsulation en loose coupling. Het lagenmodel biedt echter geen goede oplossing voor deze vraag.
We dienen dit op te lossen binnen de domeinlaag en dat kan op niet al te ingewikkelde wijze door interface en implementatie van repositories te scheiden in afzonderlijke packages en eventueel jars. Een specifieke implementatie van een repository injecteren we gewoon in het domein en als we de implementatie willen wijzigen (bijvoorbeeld stub data zodat we kunnen testen) dan injecteren we een andere implementatie. Omdat dit alles onderdeel is van één laag, hoeven we niet moeilijk te doen over het feit dat de repository-implementaties domeinobjecten geven en nemen.

