Techblog: Waarom de Hexagon Architecture?

Techblog: Waarom de Hexagon Architecture?

De term ‘hexagon architecture’ heb je vast al eens voorbij zien komen in de afgelopen jaren. Maar wat houdt het binnen de ict in? In deze blog wordt deze term tastbaar gemaakt. Er wordt uitgelegd wat het betekent en waarom je het zou moeten gebruiken. Ook worden er voorbeelden gegeven die je inzicht geven in hoe je een applicatie kan schrijven volgens de principes van de hexagon architecture.

1. Wat betekent het?

De term ‘hexagon’ betekent in het Nederlands ‘zeshoek’. Deze term kan op het eerste gezicht misschien wat misleidend zijn. Bij hexagon architecture gaat het namelijk niet om precies zes hoeken en vlakken. Het aantal vlakken is arbitrair. Het gaat er voornamelijk om dat elk vlak als het ware een ‘entry point’ of ‘port’ tot je applicatie representeert.

Een ‘entry point’ zou je kunnen zien als een locatie die requests of data tot je applicatie toelaat. Deze locatie kan allerlei soorten data binnen laten, bijvoorbeeld http/ftp/ssh connecties. Het is een manier van applicaties opbouwen waarbij de geschreven code niet direct afhankelijk hoeft te zijn van bepaalde frameworks of libraries. Om het concept te begrijpen maakt het dus ook niet uit om welke data het gaat.

Waar het wél om gaat is het principe van scheiding en ontkoppeling van je code. Door dit concept toe te passen zal je code namelijk veel meer ontkoppeld zijn tussen de lagen achter de entry points. Dit “ontkoppelingseffect” wordt voornamelijk behaald door gebruik te maken van ‘interfaces’. Hierover lees je meer in punt 3.

 

2. Waarom zou je het gebruiken?

De hexagon architecture laat ons toe om een extreem hoge ontkoppeling te realiseren binnen een applicatie. Het biedt ons daarom voordelen als maintainabilityexpandability en stabielere testen.

We kennen allemaal het fenomeen: hoe groter een applicatie wordt, hoe moeilijker het is om deze te onderhouden en uit te breiden. Een gemakkelijke ‘hack’ uit het verleden waarmee je dacht dat je wat tijd kon winnen kost je in de toekomst vaak juist veel meer tijd en frustratie dan dat je er initieel mee had gewonnen. Vroeg of laat moet je de prijs betalen voor de verkeerde designkeuzes die in het verleden gemaakt zijn. Om dit te vermijden willen we goede designkeuzes maken, zodat onze code in de toekomst gemakkelijker uit te breiden is.

De hexagon architecture heeft op dit vraagstuk de volgende filosofie als antwoord: “Vind de aspecten die variëren in je applicatie en scheidt die van de dingen die hetzelfde blijven.” Het scheiden van aspecten in je applicatie moet volgens de hexagon architecturefilosofie gebeuren door het veelvuldig gebruikmaken van interfaces, zie het volgende punt.

 

3. Interfaces

De interface is een integraal onderdeel van vrijwel elke OOP programmeertaal. Dat maakt ook dat het concept van hexagon architecture het gebruik van specifieke talen of frameworks ontstijgt. Maar waarom is ‘interfaces’ zo’n belangrijk concept binnen de hexagon architecture? Omdat het een uitstekende manier is om bepaald gedrag af te dwingen. Een interface zou je als het ware kunnen zien als een soort contract: het laat zien aan welke regels de interactie met jouw applicatie moet voldoen. Hieronder staat een voorbeeld van een interface.

interface Logger {
public function logError(String $errorMessage, Int $severity);
public function ErrorsBySeverity ($severity);
}

De implementatie van bovenstaande interface bevat twee methoden. We hebben nu als het ware een contract opgezet over hoe wij om willen gaan met een logger. Het maakt daarbij niet uit wat voor logger deze interface implementeert. De logger zal namelijk altijd een error kunnen loggen door een String en een Integer mee te geven en het zal altijd een aantal errors op kunnen halen door een Integer eraan mee te geven.

In het voorbeeld hieronder kun je zien dat de EmailService niet ‘weet’ met welke soort logger het te maken heeft. Het weet enkel dat de logger een bepaalde set aan regels heeft om ermee te communiceren die zijn vastgelegd in de interface.

Class EmailService{
public function __construct(Logger $logger){
$this→logger = $logger;
}
public function sendEmail(){
if(! $email→send() ){
$this→logger()→logError(‘Something went wrong’ , 10)l
}
}

Waarom is dit principe zo belangrijk voor het bouwen van goede uitbreidbare applicaties? Omdat we enkel afhankelijkheid zijn van de interface en niet van de implementatie achter de interface. Op die manier kunnen we gemakkelijk de implementatie veranderen en, als die nog steeds voldoet aan de eisen van de interface, zal alles blijven werken zoals we dat verwachten.
Onderstaand voorbeeld laat praktisch zien wat we bedoelen. Daarbij gaan we uit dat de klasse EmailLogger en ErrorLogger dezelfde interface implementeren.

//use emailLogger
$emailLogger = new EmailLogger();
$emailSerivce = new EmailService($emailLogger);
//use errorLogger
$errorLogger = new ErrorLogger();
$emailService = new EmailSerivce($errorLogger);

De logica van het loggen zal in het bovenstaande voorbeeld kunnen verschillen maar voor de werking van de applicatie zal het niet uitmaken wat voor soort logger er gebruikt wordt. Ondanks dat de implementatie misschien wel sterk verschilt zal onze applicatie er niet anders door werken omdat we enkel gebruik maken van de interface die de logger implementeert in plaats van de daadwerkelijke logger zelf.

 

4. Inhoud hexagon architecture — adapters en layers

De hexagon arhitecture beschouwt onze code als aparte segmenten van de functionele delen van onze applicatie. De adapters en ports (beiden betekenen hetzelfde) beschrijven hoe deze layers met elkaar kunnen communiceren. Elk van deze layers beschikt over twee duidelijke elementen: de daadwerkelijke code en de adapters.

Het code segment van een layer bezit alle code die jouw applicatie business value geeft. Hier zit de logica die jouw applicatie uniek maakt in vergelijking met andere applicaties. En de adapters zijn de in en uitgangen tot dit code segment.

De hexagon architecture beschrijft 3 belangrijke layers die voor scheiding en een maximale ontkoppeling zorgen, met behoud van de expandability en maintainability. Deze layers zijn de domain layer, application layer en de framework layer. Deze layers worden hieronder in meer detail beschreven.

1. Domain layer
De domain layer is de meest diepe laag en bevat het meest van de business logic. Kort gezegd, deze laag zorgt ervoor dat jouw applicatie waarde genereert. De complexiteit van je applicatie wordt voornamelijk in deze laag bepaald. Voor simpele applicaties zal hier relatief minder logica inzitten dan grote en complexere applicaties. De domain layer zal naast al je logica ook al je entities bezitten.

2. Application layer
De application layer bevindt zich net buiten je domain layer en dicteert onder andere hoe de entiteiten in je domain layer gebruikt moeten worden. Het is ook de laag die requests van je framework layer binnenlaat tot je domain layer.

3. Framework layer
De framework layer bezit de code waar jouw applicatie gebruik van maakt maar die niet jouw applicatie zelf is. Vaak bezit deze laag dus het gebruikte framework zelf en de 3rd party libraries die je gebruikt.

 

5. Hoe werken de layers samen?

Nu we de inhoud van de hexagon architecture doorgenomen hebben kunnen we wat dieper gaan. We gaan nu kijken naar hoe de layers met elkaar communiceren. Dit maken we duidelijk aan de hand van onderstaande procesbeschrijving.

Een point in de application layer zal een request van de framework layer ontvangen om iets met een bepaalde user entity te doen in je domain layer.
Dus de framework layer -die enkel een bepaalde HTTP request doorgeeft- zal via een interface van de application layer een request presenteren. De application layer zal op zijn beurt vervolgens bepaalde logica uit voeren door gebruik te maken van bepaalde interfaces van de domain layer, bijvoorbeeld een user genereren of updaten.

Zoals hierboven is beschreven bezit elke layer een set van interfaces die afstemmen hoe de layer naar buiten kan communiceren. Om nog een duidelijker beeld te schetsen is hieronder een voorbeeld uitgewerkt.

Een praktisch voorbeeld
Stel we willen een gebruiker creëren in onze applicatie. De user entity zal in onze domain layer bestaan. En we willen dat de gebruiker via een HTTP request -die via onze framework layer binnenkomt- een gebruiker kan creëren. Hieronder staat beschreven hoe dit in z’n werk gaat.

In de domain layer zullen we een interface implementeren die een user entity kan genereren.

Interface UserEntityManager{
public function create(User $user);
}

De application layer ontvangt de bovenstaande data van de domain layer om de user te kunnen creëren. De application layer zal dus gebruik moeten maken van deze interface.

De application layer bevindt zich tussen de domain en framework layer. Daarom zal het zelf ook een interface moeten definiëren en implementeren. Dit kan eruit zien als het volgende voorbeeld.

Interface UserRequestHandler{
public function handleUserCreationRequest($data);
}
class UserManagementService implements UserRequestHandler{
public function __construct(UserEntityManager $userEntityManager)
$this→userEntityManager = $userEntitiyManager;
}
public function handleUserCreationRequest($data){
$this→userEntiyManager()→create($data);
}

Nu rest ons alleen nog de implementatie bij de framework layer, deze kan er als volgt uitzien.

Class someControllerClass{
Public function handleRequest($request){
$userRequestHandler = new UserManagementService() //implementeerd UserRequestHandler
$data = $request->getData();
$userRequestHandler()->handleUserCreationRequest($data);
}
}

 

6. Wat is de kracht van hexagon architecture?

Lees hieronder de vijf krachten van de hexagon architecture.

De eerste kracht is dat elke layer op opzichzelfstaand te testen is. Het maakt voor de ene layer niet uit wat de precieze implementatie van de andere layer is, zolang het maar werkt volgens de regels die in de interface zijn afgedwongen.

De tweede kracht is dat -als je in de toekomst een API zou willen bouwen- er via data die bij de API binnenkomt ook gemakkelijk een user gegenereerd kan worden. In dat geval laten we de API ook gebruikmaken van een UserRequestHandler. Dan kan er, net zoals via de HTTP request, eenvoudig een user gecreëerd worden. Alle eerder geschreven unit tests op de diepere lagen (application- en domain layer) blijven dan nog steeds relevant.

De derde kracht is dat door het toepassen van het principe van Hexagon architecture eerder geschreven code en unit tests langer relevant blijven. Je zal veel minder bezig zijn met het herschrijven van oude code die ineens niet meer werkt. Hierdoor heb je meer tijd over voor het bouwen van nieuwe features.

De vierde kracht is dat de hexagon architecture snellere en betrouwbaardere tests biedt. De code in je domain layer heeft namelijk geen weet van je gebruikte framework, daardoor is de hoeveelheid die je moet bootstrappen minimaal en dat resulteert in snellere en betrouwbaardere tests.

De vijfde kracht is dat er door het implementeren van de hexagon architecture ingrijpende gebeurtenissen als een grote framework update of overstappen naar een ander framework veel minder heftig zijn. Dit komt omdat je enkel een aantal endpoints in de framework layer hoeft aan te passen. Alles binnen de application layer en domain layer zal namelijk niet gewijzigd hoeven te worden.

Kortom, de hexagon architecture is een filosofie van code schrijven die niet gebonden is aan specifieke taal of framework en is zeker de moeite waard om in gedachten te houden als je aan een nieuwe project begint of een nieuwe feature gaat bouwen in een bestaand project.