Architectuurprincipes

Tip

Deze inhoud is een fragment uit het eBook, Architect Modern Web Applications met ASP.NET Core en Azure, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.

Architect Modern Web Applications with ASP.NET Core and Azure eBook cover thumbnail.

"Als bouwers gebouwen bouwden zoals programmeurs programma's schreven, zouden de eerste specht die langskwam de beschaving vernietigen."
- Gerard Weinberg

U moet softwareoplossingen ontwerpen en ontwerpen met behoudbaarheid in gedachten. De principes die in deze sectie worden beschreven, kunnen u helpen bij het nemen van architectuurbeslissingen die leiden tot schone, onderhoudbare toepassingen. Over het algemeen helpen deze principes u bij het bouwen van toepassingen uit afzonderlijke onderdelen die niet nauw zijn gekoppeld aan andere onderdelen van uw toepassing, maar in plaats daarvan communiceren via expliciete interfaces of berichtensystemen.

Algemene ontwerpprincipes

Scheiding van problemen

Een leidraad bij het ontwikkelen is Scheiding van zorgen. Dit principe bevestigt dat software moet worden gescheiden op basis van het soort werk dat wordt uitgevoerd. Denk bijvoorbeeld aan een toepassing die logica bevat voor het identificeren van opmerkelijke items die aan de gebruiker moeten worden weergegeven en waarmee dergelijke items op een bepaalde manier worden opgemaakt om ze duidelijker te maken. Het gedrag dat verantwoordelijk is voor het kiezen van de items die moeten worden opgemaakt, moet gescheiden worden gehouden van het gedrag dat verantwoordelijk is voor het opmaken van de items, omdat dit gedrag afzonderlijke problemen zijn die alleen toevallig met elkaar te maken hebben.

Architectuur kunnen toepassingen logisch worden gebouwd om dit principe te volgen door het belangrijkste bedrijfsgedrag te scheiden van infrastructuur- en gebruikersinterfacelogica. In het ideale geval moeten bedrijfsregels en logica zich in een afzonderlijk project bevinden, wat niet afhankelijk mag zijn van andere projecten in de toepassing. Deze scheiding helpt ervoor te zorgen dat het bedrijfsmodel eenvoudig te testen is en zich kan ontwikkelen zonder nauw gekoppeld te zijn aan implementatiedetails op laag niveau (het helpt ook als de problemen met de infrastructuur afhankelijk zijn van abstracties die zijn gedefinieerd in de bedrijfslaag). Scheiding van problemen is een belangrijke overweging achter het gebruik van lagen in toepassingsarchitecturen.

Inkapseling

Verschillende onderdelen van een toepassing moeten inkapseling gebruiken om ze te isoleren van andere delen van de toepassing. Toepassingsonderdelen en -lagen moeten hun interne implementatie kunnen aanpassen zonder hun medewerkers te breken zolang externe contracten niet worden geschonden. Het juiste gebruik van inkapseling helpt bij het realiseren van losse koppeling en modulariteit in toepassingsontwerpen, omdat objecten en pakketten kunnen worden vervangen door alternatieve implementaties zolang dezelfde interface behouden blijft.

In klassen wordt inkapseling bereikt door externe toegang tot de interne status van de klasse te beperken. Als een externe actor de status van het object wil bewerken, moet dit doen via een goed gedefinieerde functie (of eigenschapssetter), in plaats van directe toegang te hebben tot de privéstatus van het object. Op dezelfde manier moeten toepassingsonderdelen en toepassingen zelf goed gedefinieerde interfaces beschikbaar maken die hun medewerkers kunnen gebruiken, in plaats van dat hun status rechtstreeks kan worden gewijzigd. Deze aanpak maakt het interne ontwerp van de toepassing vrij om zich in de loop van de tijd te ontwikkelen zonder dat u zich zorgen hoeft te maken dat dit samenwerkers zal breken, zolang de overheidsopdrachten worden gehandhaafd.

De onveranderbare globale status is antithetisch voor inkapseling. Een waarde die is opgehaald uit de onveranderbare globale status in één functie, kan niet worden gebruikt om dezelfde waarde in een andere functie te hebben (of zelfs verder in dezelfde functie). Het begrijpen van problemen met veranderlijke globale status is een van de redenen waarom programmeertalen zoals C# ondersteuning bieden voor verschillende bereikregels, die overal worden gebruikt, van instructies tot methoden tot klassen. Het is de moeite waard om te vermelden dat gegevensgestuurde architecturen die afhankelijk zijn van een centrale database voor integratie binnen en tussen toepassingen, zelf afhankelijk zijn van de onveranderbare globale status die wordt vertegenwoordigd door de database. Een belangrijke overweging in domeingestuurd ontwerp en schone architectuur is het inkapselen van toegang tot gegevens en hoe u ervoor kunt zorgen dat de toepassingsstatus niet ongeldig wordt gemaakt door directe toegang tot de persistentie-indeling.

Inversie van afhankelijkheden

De richting van afhankelijkheid binnen de toepassing moet in de richting van abstractie staan, niet in de implementatiedetails. De meeste toepassingen worden zodanig geschreven dat gecompileerd afhankelijkheidsstromen in de richting van runtime-uitvoering, waardoor een directe afhankelijkheidsgrafiek wordt geproduceerd. Als klasse A een methode van klasse B aanroept en klasse B een methode van klasse C aanroept, hangt de compilatietijdklasse A af van klasse B en klasse B is afhankelijk van klasse C, zoals wordt weergegeven in afbeelding 4-1.

Direct dependency graph

Afbeelding 4-1. Directe afhankelijkheidsgrafiek.

Door het inversion-principe van afhankelijkheid toe te passen, kan A methoden aanroepen op een abstractie die B implementeert, waardoor A B tijdens runtime kan aanroepen, maar dat B afhankelijk is van een interface die wordt beheerd door A tijdens het compileren (waardoor de typische compileertijdafhankelijkheid wordt omgekeerd ). Tijdens de uitvoering blijft de uitvoering van het programma ongewijzigd, maar de introductie van interfaces betekent dat verschillende implementaties van deze interfaces eenvoudig kunnen worden aangesloten.

Inverted dependency graph

Afbeelding 4-2. Omgekeerde afhankelijkheidsgrafiek.

Afhankelijkheidsinversion is een belangrijk onderdeel van het bouwen van losjes gekoppelde toepassingen, omdat implementatiedetails kunnen worden geschreven om afhankelijk te zijn van en abstracties op een hoger niveau te implementeren, in plaats van andersom. De resulterende toepassingen zijn testbaar, modulair en onderhoudbaar als gevolg hiervan. De praktijk van afhankelijkheidsinjectie wordt mogelijk gemaakt door het inversion-principe van afhankelijkheid te volgen.

Expliciete afhankelijkheden

Methoden en klassen moeten expliciet eventuele samenwerkende objecten vereisen die ze nodig hebben om correct te kunnen functioneren. Dit wordt het principe expliciete afhankelijkheden genoemd. Klasseconstructors bieden klassen de mogelijkheid om de dingen te identificeren die ze nodig hebben om een geldige status te hebben en goed te functioneren. Als u klassen definieert die kunnen worden samengesteld en aangeroepen, maar die alleen goed werken als bepaalde globale of infrastructuuronderdelen aanwezig zijn, zijn deze klassen onaangenaam met hun clients. Het constructorcontract vertelt de client dat alleen de opgegeven zaken nodig zijn (mogelijk niets als de klasse alleen een parameterloze constructor gebruikt), maar tijdens runtime blijkt dat het object echt iets anders nodig heeft.

Door het expliciete afhankelijkhedenprincipe te volgen, zijn uw klassen en methoden eerlijk met hun klanten over wat ze nodig hebben om te kunnen functioneren. Door het principe te volgen, worden uw code meer zelfdocumentatie en uw coderingscontracten gebruiksvriendelijker, omdat gebruikers zullen vertrouwen dat zolang ze bieden wat vereist is in de vorm van methode- of constructorparameters, de objecten waarmee ze werken, zich correct gedragen tijdens runtime.

Eén verantwoordelijkheid

Het principe van één verantwoordelijkheid is van toepassing op objectgeoriënteerd ontwerp, maar kan ook worden beschouwd als een architectuurprincipe dat vergelijkbaar is met de scheiding van zorgen. Het geeft aan dat objecten slechts één verantwoordelijkheid moeten hebben en dat ze slechts één reden hebben om te veranderen. De enige situatie waarin het object moet veranderen, is als de manier waarop het object de ene verantwoordelijkheid uitvoert, moet worden bijgewerkt. Het volgen van dit principe helpt bij het produceren van losjes gekoppelde en modulaire systemen, omdat veel soorten nieuw gedrag kunnen worden geïmplementeerd als nieuwe klassen, in plaats van door extra verantwoordelijkheid toe te voegen aan bestaande klassen. Het toevoegen van nieuwe klassen is altijd veiliger dan het wijzigen van bestaande klassen, omdat er nog geen code afhankelijk is van de nieuwe klassen.

In een monolithische toepassing kunnen we het principe van één verantwoordelijkheid op hoog niveau toepassen op de lagen in de toepassing. De verantwoordelijkheid van de presentatie moet in het UI-project blijven, terwijl de verantwoordelijkheid voor gegevenstoegang binnen een infrastructuurproject moet worden bewaard. Bedrijfslogica moet worden bewaard in het kernproject van de toepassing, waar het eenvoudig kan worden getest en onafhankelijk van andere verantwoordelijkheden kan worden ontwikkeld.

Wanneer dit principe wordt toegepast op toepassingsarchitectuur en naar het logische eindpunt wordt gebracht, krijgt u microservices. Een bepaalde microservice moet één verantwoordelijkheid hebben. Als u het gedrag van een systeem wilt uitbreiden, is het meestal beter om dit te doen door extra microservices toe te voegen in plaats van door verantwoordelijkheid toe te voegen aan een bestaand systeem.

Meer informatie over microservicesarchitectuur

Herhaal jezelf niet (DROOG)

De toepassing moet het opgeven van gedrag met betrekking tot een bepaald concept op meerdere plaatsen vermijden, omdat deze praktijk een frequente bron van fouten is. Op een bepaald moment moet dit gedrag worden gewijzigd door een wijziging in de vereisten. Het is waarschijnlijk dat ten minste één exemplaar van het gedrag niet wordt bijgewerkt en dat het systeem zich inconsistent gedraagt.

In plaats van logica te dupliceren, kunt u deze inkapselen in een programmeerconstructie. Maak deze constructie de enige instantie voor dit gedrag en laat een ander deel van de toepassing die dit gedrag vereist, de nieuwe constructie gebruiken.

Notitie

Vermijd het samenbinden van gedrag dat alleen incidenteel herhalend is. Omdat twee verschillende constanten bijvoorbeeld dezelfde waarde hebben, betekent dit niet dat u slechts één constante moet hebben, als ze naar verschillende dingen verwijzen. Duplicatie verdient altijd de voorkeur om te koppelen aan de verkeerde abstractie.

Persistentie negeren

Persistentie-negeren (PI) verwijst naar typen die moeten worden persistent, maar waarvan de code niet wordt beïnvloed door de keuze van persistentietechnologie. Dergelijke typen in .NET worden soms POC's (Plain Old CLR Objects) genoemd, omdat ze niet hoeven te worden overgenomen van een bepaalde basisklasse of een bepaalde interface hoeven te implementeren. Persistentie-negeren is waardevol omdat hetzelfde bedrijfsmodel op meerdere manieren kan worden behouden, wat extra flexibiliteit biedt voor de toepassing. Persistentieopties kunnen na verloop van tijd veranderen, van de ene databasetechnologie naar de andere, of extra vormen van persistentie zijn vereist naast de toepassingen waarmee de toepassing is gestart (bijvoorbeeld het gebruik van een Redis-cache of Azure Cosmos DB naast een relationele database).

Enkele voorbeelden van schendingen van dit principe zijn:

  • Een vereiste basisklasse.

  • Een vereiste interface-implementatie.

  • Klassen die verantwoordelijk zijn voor het opslaan van zichzelf (zoals het patroon Actieve record).

  • Vereiste parameterloze constructor.

  • Eigenschappen waarvoor een virtueel trefwoord is vereist.

  • Persistentiespecifieke vereiste kenmerken.

De vereiste dat klassen een van de bovenstaande functies of gedragingen hebben, voegt koppeling toe tussen de typen die moeten worden behouden en de keuze van persistentietechnologie, waardoor het in de toekomst moeilijker is om nieuwe strategieën voor gegevenstoegang te gebruiken.

Gebonden contexten

Gebonden contexten zijn een centraal patroon in Domeingestuurd ontwerp. Ze bieden een manier om complexiteit in grote toepassingen of organisaties aan te pakken door deze op te delen in afzonderlijke conceptuele modules. Elke conceptuele module vertegenwoordigt vervolgens een context die is gescheiden van andere contexten (dus gebonden) en onafhankelijk kan worden ontwikkeld. Elke gebonden context moet in het ideale geval vrij zijn om zijn eigen namen te kiezen voor concepten in deze context en moet exclusieve toegang hebben tot een eigen persistentiearchief.

Afzonderlijke webtoepassingen moeten minimaal streven naar hun eigen gebonden context, met hun eigen persistentiearchief voor hun bedrijfsmodel, in plaats van een database te delen met andere toepassingen. Communicatie tussen gebonden contexten vindt plaats via programmatische interfaces, in plaats van via een gedeelde database, waardoor bedrijfslogica en gebeurtenissen kunnen plaatsvinden als reactie op wijzigingen die plaatsvinden. Gebonden contexten zijn nauw gekoppeld aan microservices, die ook idealiter worden geïmplementeerd als hun eigen afzonderlijke gebonden contexten.

Aanvullende bronnen