DEV Community

Cover image for Hoe breek je software op in componenten?
Jarno Vogelzang - Bitween
Jarno Vogelzang - Bitween

Posted on • Originally published at bitween.software

Hoe breek je software op in componenten?

In mijn vorige artikel ging ik in op de vraag: Waarom zou je gestructureerde software schrijven? Het antwoord is: omdat je dan sneller kunt programmeren. In dit artikel ga ik dieper in op het schrijven van modulaire code. Het gaat dus specifiek over het opbreken van software in componenten. De vraag die ik in dit artikel behandel is dus: hoe breek je software op in componenten?

Ontwerppatronen

Laten we kijken naar de basis van het programmeren. Je ontwikkelt altijd software op basis van een ontwerppatroon. Een ontwerppatroon is een structuur die je gebruikt om jouw softwareprobleem op te lossen. Er zijn 3 ontwerppatronen die vandaag worden gebruikt. Dat zijn: functioneel programmeren, object-georienteerd programmeren en gestructureerd programmeren. In dit artikel ga ik dieper in op gestructureerd programmeren, of ‘structured programming’. Deze techniek gaat namelijk over het opbreken van code in kleine stukjes functionaliteit. En die techniek kan jij gebruiken om vervolgens software op te breken in componenten.

Gestructureerd programmeren

Ooit was er een tijd waarin wij als ontwikkelaars GOTO-statements gebruikten. Een GOTO-statement zorgt ervoor dat je programma op een andere plek binnen de codebase wordt hervat. Het is alsof je een functie aanroept, maar niet meer returned vanuit die functie. Het is daarmee een zogeheten ‘one way transfer of control’. Hier een klein voorbeeldje:

In deze code worden alleen de statements op de regels 3, 6 en 7 uitgevoerd. Er wordt dus ‘gesprongen’ van regel 3 (goto) naar het label op regel 6. Met een GOTO-statement kun je dus naar een andere regel in de code springen.

Wiskundig bewijzen

Nu kwamen we er in die dagen al snel achter dat software ontwikkelen moeilijk is. Tijdens het bouwen van applicaties moet je namelijk rekening houden met heel veel zaken: valideren van invoerwaardes, opslaan van data in een database, het schoon houden van de codebase, noem maar op. Omdat je met zoveel zaken rekening moet houden, is het lastig om na te gaan of je code op een gegeven moment nog werkt. Daarom zouden we graag op de een of andere manier willen bewijzen dat onze code werkt. Stel, we zouden onze code opbouwen door middel van bewezen programmeerstructuren, dan zouden we altijd zeker weten dat onze programma’s werkten. Laat me je een voorbeeld geven om dit te verduidelijken:

Dit is een voorbeeld van een merge sort algoritme. Als we willen bewijzen dat het bovenstaande sorteeralgoritme werkt, kunnen we dit probleem opbreken in 2 punten:

  • Eerst moeten we bewijzen dat het sorteeralgoritme een array opbreekt in 2 subarrays. Daarna bewijzen we dat dit proces doorgaat, totdat er slechts 1 element over is in de array.
  • Vervolgens bewijzen we dat het sorteeralgoritme een subarray van N elementen en een andere subarray van M elementen sorteert aan de hand van ‘merging’.

Nu is dit bewijs relatief simpel te leveren. Gegeven een input array van N elementen. Deze wordt opgebroken in 2 arrays van N / 2 elementen op regel 13. Vervolgens worden deze 2 subarrays opnieuw opgebroken door de recursieve aanroepen op regels 15 en 16. Daarna worden 2 subarrays samengevoegd in de functie op regel 21. Op deze manier bewijzen we dat ons algoritme werkt. Ofwel, we breken het bewijzen op in kleinere behapbare stukjes.

De onderliggende techniek

Het idee is dus om een probleem op te pakken, dat vervolgens te ontleden in functies, die opnieuw te ontleden in functies op een lager niveau, tot je aan een functie kwam die niet meer te onleden valt. Als je dat doet, kun je die lage functies bewijzen, die vervolgens gebruiken in de hogere functies en zo uiteindelijk bewijzen dat je complete programma werkt. Dit is ongeveer de basis van vele bewijsmethodieken in de wiskunde.

Nu is deze bewijstechniek alleen mogelijk wanneer we gebruik maken van een gestructureerde ‘control flow’. Een ‘control flow‘ is de ordering waarin statements, instructies en functie-aanroepingen worden uitgevoerd. Ofwel: de volgorde waarin regels code worden uitgevoerd. Als deze volgorde geen ongecontroleerde sprongen maakt, kunnen we zeggen dat regel X altijd wordt uitgevoerd voor regel Y. Die kennis gebruiken we vervolgens om ons algoritme te bewijzen. Dit is niet mogelijk wanneer de control flow ongecontroleerde sprongen maakt door bijvoorbeeld GOTO. Een GOTO-statement is daarin een voorbeeld van direct transfer of control.

We moeten onze code dus gestructureerd (zonder rare sprongen) opbouwen. En vanuit dat idee is ‘structured programming’ ontstaan.

Wetenschappelijk bewijzen

We kunnen dus, door middel van wiskundige bewijsmethodieken, bepalen of onze software werkt of niet. Het wiskundige bewijzen werd echter nooit op grote schaal toegepast. Het is simpelweg te complex. Wat daar in de plaats van kwam was een meer wetenschappelijke manier om te bewijzen dat het werkt.

De wetenschap gaat er bij alles vanuit dat waar is, totdat het tegendeel bewezen is. Hierbij maak je gebruik van expirementen. Dit zijn herhaalbare stappen die je doorloopt om te kijken wat de uitkomst is. Als we dit vertalen naar de software-wereld, maken we gebruik van tests. Onze tests bewijzen niet zozeer dat onze code geen bugs heeft. Ze laten echter wel zien dat de code voor ons gebruik geschikt is. Dat wil zeggen: de code kan gebruikt worden op de manier zoals in de tests omschreven wordt. En als een test faalt, laat dat dus zien dat er een bug is. In dit voorbeeld is dat een PHPUnit test:

In deze test, hebben we dus een aantal voorbeelden die we als invoer pakken. Vervolgens bepalen we wat de verwachte uitkomst van het algoritme is. In dit geval is de invoer een ongesorteerde array van nummers. De verwachte uitvoer is een gesorteerde array met diezelfde elementen. Omdat de tests ‘werken’, weten we dat de class MergeSort werkt voor ons specifieke doel.

Software testen

Het hele idee van software testen is dus gebaseerd op de wetenschappelijke methode voor bewijzen. We laten dus niet zien dat onze software compleet bewezen is, maar laten zien dat het werkt voor ons doel. Dijkstra zelf zegt het mooi met zijn citaat

Testen laat alleen de aanwezigheid van bugs zien, niet de afwezigheid.

Edsger Dijkstra

En hier komt het hele punt samen van structured programming. Je kunt alleen tests schrijven voor code die ook daadwerkelijk te testen is. Dat wil zeggen: de code kan niet gebruikmaken van goto-statements. Daarnaast moet de code duidelijke functionaliteit bieden, zodat we die specifieke functies kunnen testen. Hierom is de basis van gestructureerde code dus geteste code.

Wat we dus hebben geleerd van structured programming is dat we programma’s moeten opsplitsen in kleine stukjes functionaliteit. Deze ‘units’ van functionaliteit kunnen we vervolgens afzonderlijk testen aan de hand van bijvoorbeeld ‘unit tests’. Deze afzonderlijke units worden vervolgens gebruikt om ons programma op te bouwen. Laten we naar een praktijkvoorbeeld kijken om in te zien hoe dat precies werkt.

De praktijk

Wat kunnen we leren van deze theorie? Hoe kunnen we de kennis van structured programming gebruiken om schone gestructureerde software te implementeren? Het begint met het opbreken van logica in stukjes behapbare functionaliteit. Wanneer we een specifiek systeem bekijken, willen we graag dat het systeem is opgebroken in verschillende modules. Elke module heeft hierin een taak. Voorbeelden van modules zijn: een opslagmodule, een weergavemodule, een authorizatiemodule, een authenticatiemodule, noem het maar op. We breken een groot systeem dus op in meerdere kleinere componenten. Dit komt dus overeen met het opbreken van een probleem in kleinere subproblemen, om vervolgens het probleem in zijn geheel op te lossen.

Opbreken van functionaliteit

Hoe werkt dat opbreken? Het begint met het bekijken van een nieuw te bouwen feature. Stel, we bouwen het berichtensysteem zoals ik ook in mijn vorige post heb beschreven. Een feature zou dan kunnen zijn: een gebruiker moet een bericht kunnen aanmaken. Dit bericht moet eerst gevalideerd worden. Als het geldig is, wordt het opgeslagen. Vervolgens krijgt de gebruiker een uitvoer te zien van alle berichten. Dit is de totale beschrijving van een feature. Dit is dus 1 hele grotie functie. Vervolgens is dit op te breken in kleinere stappen. Dit is dus het opbreken in subfuncties:

  • Valideer de inkomende gebruikersdata.
  • Sla deze data als Post-object op in een opslag-laag.
  • Haal alle Post-objecten uit die opslag-laag.
  • Converteer deze objecten naar een bepaald uitvoerformat en geef dat weer aan de gebruiker.

Zoals je ziet is de gehele feature opgebroken in kleinere componenten. Deze componenten kunnen vervolgens afzonderlijk worden gebouwd. Op deze manier is elke component verantwoordelijk voor 1 stap in het proces. Dit noemen we ook wel de ‘Separation of Concerns‘. Elke concern heeft hierbij dus 1 functie. Het dient tot 1 doel. Als we goed kijken, zijn deze 4 stappen de toegevoegde waarde of functionaliteit in ons systeem. Daarnaast bevatten deze 4 stappen geen implementatie-details, dat is niet interessant. Het gaat erom dat we een post kunnen opslaan en vervolgens alle posts kunnen omzetten naar een bepaald uitvoerformaat. Daarnaast moet het ook niet uitmaken hoe we het resultaat weergeven aan de gebruiker, of dat nou via een webbrowser of een mobiele applicatie is. We willen onze software dus onafhankelijk van de details bouwen, daarom abstraheren we de details achter interfaces.

Het resultaat

De functionaliteit van het ‘opslaan van een post’, zit dus nu in de interface. Die interface ziet er zo uit:

Zoals je ziet, kun je met deze interface een Post-object opslaan en vervolgens alle Post-objecten ophalen. De implementaties van deze interface bevatten vervolgens de details hoe je dat met een specifieke database-technologie kan doen. Zo is er bijvoorbeeld een specifieke class die alle berichten in memory opslaat.

De functionaliteit van weergeven van een post, ziet er dan zo uit:

Elke implementatie van deze interface kan dus zowel 1 of meerdere Post-objecten tonen aan de gebruiker. Dit ‘tonen’ is in dit geval het omzetten naar een string. Deze string wordt vervolgens getoond.

Het valideren van het post-object, ziet er vervolgens zo uit:

Een Validator-implementatie gooit dus een InvalidPostException wanneer het Post-object niet geldig is. Hoe dat precies wordt bepaald, is niet interessant. Dat kan door een Symfony Validator object, maar ook door gebruik te maken van de validatie die Laravel biedt.

De implementaties van deze interfaces zorgen er dus vervolgens voor dat onze code werkt. Maar het gaat erom dat deze details abstract zijn gemaakt, het maakt voor de applicatie niet uit of de implementatie van de Presentation interface de posts laat zien als HTML-tabel of als JSON object. Dit zijn de details, de toegevoegde waarde ligt in het feit dat het gebeurd en dat is de interface.

De interfaces bieden dus elk functionaliteit die onze applicatie gebruikt om de complete feature te implementeren. De implementatie van die feature gebeurd dan in een zogeheten ‘Use case class’. Dat ziet er zo uit:

Zoals je ziet maakt de feature-class gebruikt van deze opgebroken functionaliteit om zijn werk te doen. Die functionaliteit zit in de interfaces, de feature class is dus onafhankelijk van de details geschreven. Die details zijn de PHP frameworks, de onderliggende databases en dergelijke. De gehele implementatie van deze feature kun je vinden in mijn codebase.

Conclusie

Hoe breken we dus software op in componenten? Simpel, je breekt de volledige functionaliteit van een systeem op in kleine stukjes en zet die stukjes achter een interface. Zo heb je bijvoorbeeld een Persistence interface voor de functionaliteit ‘het opslaan van objecten’. Vervolgens zorgen de implementaties van deze interfaces voor de daadwerkelijke onderliggende logica. Deze interfaces gebruik je om je volledige systeem op te bouwen. Elke component (interface) biedt dus een klein stukje functionaliteit aan je totale applicatie.

Ik heb je in dit artikel dus laten zien hoe jij je software kunt opbreken in logische componenten. Wil jij nu een PHP webapplicatie laten bouwen die voldoet aan deze technieken? Dan ben je bij mij aan het juiste adres. Laten we kennismaken om te kijken wat ik voor je kan betekenen.

Top comments (0)