paint-brush
Hoe om te gaan met complexiteit bij het ontwerpen van softwaresystemendoor@fairday
64,464 lezingen
64,464 lezingen

Hoe om te gaan met complexiteit bij het ontwerpen van softwaresystemen

door Aleksei23m2024/02/05
Read on Terminal Reader
Read this story w/o Javascript

Te lang; Lezen

Complexiteit is de vijand! Laten we leren hoe we daarmee om kunnen gaan!
featured image - Hoe om te gaan met complexiteit bij het ontwerpen van softwaresystemen
Aleksei HackerNoon profile picture

Waar gaat het over?

Elke dag, elk moment tijdens onze ingenieurscarrière, komen we veel verschillende problemen van verschillende complexiteit en situaties tegen waarin we een beslissing moeten nemen of deze moeten uitstellen vanwege een gebrek aan gegevens. Wanneer we nieuwe services bouwen, infrastructuur construeren of zelfs ontwikkelingsprocessen vormen, raken we een enorme wereld van verschillende uitdagingen aan.


Het is een uitdaging, en misschien zelfs onmogelijk, om alle problemen op te sommen. Sommige van deze problemen kom je alleen tegen als je in een specifieke niche werkt. Aan de andere kant zijn er veel problemen die we allemaal moeten begrijpen om op te lossen, omdat ze cruciaal zijn voor het bouwen van IT-systemen. Met een grote waarschijnlijkheid kom je ze in alle projecten tegen.


In dit artikel deel ik mijn ervaringen met een aantal problemen die ik ben tegengekomen bij het maken van softwareprogramma's.

Wat is Cross-Cutting Concern?

Als we op Wikipedia kijken, vinden we de volgende definitie


Bij aspect-georiënteerde softwareontwikkeling zijn cross-cutting concerns aspecten van een programma die meerdere modules beïnvloeden, zonder de mogelijkheid om in een van hen te worden ingekapseld. Deze concerns kunnen vaak niet netjes worden ontbonden van de rest van het systeem in zowel het ontwerp als de implementatie, en kunnen resulteren in ofwel scattering (code duplicatie), tangled (significante afhankelijkheden tussen systemen), of beide.


Het beschrijft heel goed wat het is, maar ik wil het graag wat uitbreiden en vereenvoudigen:

Een cross-cutting concern is een concept of onderdeel van het systeem/de organisatie dat invloed heeft op (of 'doorsnijdt') veel andere onderdelen.


De beste voorbeelden van dergelijke zorgen zijn systeemarchitectuur, logging, beveiliging, transactiebeheer, telemetrie, databaseontwerp en er zijn er nog veel meer. We gaan er later in dit artikel uitgebreid op in.


Op codeniveau worden cross-cutting concerns vaak geïmplementeerd met behulp van technieken zoals Aspect-Oriented Programming (AOP) , waarbij deze concerns worden gemodulariseerd in afzonderlijke componenten die in de hele applicatie kunnen worden toegepast. Dit houdt de bedrijfslogica geïsoleerd van deze concerns, waardoor de code leesbaarder en beter te onderhouden is.

Aspecten Classificatie

Er zijn veel mogelijke manieren om aspecten te classificeren door ze te segmenteren met verschillende eigenschappen zoals scope, grootte, functionaliteit, belang, doel en andere, maar in dit artikel ga ik een eenvoudige scope-classificatie gebruiken. Hiermee bedoel ik waar dit specifieke aspect naartoe gaat, of het nu de hele organisatie is, een bepaald systeem of een specifiek element van dat systeem.


Ik ga de aspecten dus opsplitsen in Macro en Micro .


Met macro- aspect bedoel ik vooral de overwegingen die we volgen voor het hele systeem, zoals de gekozen systeemarchitectuur en het ontwerp ervan (monolithisch, microservices, servicegerichte architectuur), de technologie-stack, de organisatiestructuur, enzovoort. Macro- aspecten hebben vooral betrekking op strategische en hoogwaardige beslissingen.


Ondertussen ligt het Micro- aspect veel dichter bij het codeniveau en de ontwikkeling. Bijvoorbeeld, welk framework wordt gebruikt voor interactie met de database, de projectstructuur van mappen en klassen, of zelfs specifieke objectontwerppatronen.


Hoewel deze indeling niet ideaal is, helpt het om inzicht te krijgen in mogelijke problemen en het belang en de impact van de oplossingen die we daarvoor toepassen.


In dit artikel zal ik mij vooral richten op de macro-aspecten.

Macro-aspecten

Organisatiestructuur

Toen ik net begon met leren over softwarearchitectuur, las ik veel interessante artikelen over de wet van Conway en de impact ervan op de organisatiestructuur. Vooral deze . Deze wet stelt dat


Elke organisatie die een systeem (in brede zin) ontwerpt, zal een ontwerp produceren waarvan de structuur een kopie is van de communicatiestructuur van de organisatie.


Ik heb altijd geloofd dat dit concept inderdaad heel universeel is en de Gouden Regel vertegenwoordigt.


Toen begon ik de Domain-Driven Design (DDD)-benadering van Eric Evans te leren voor het modelleren van systemen. Eric Evans benadrukt het belang van Bounded Context-identificatie. Dit concept omvat het verdelen van een complex domeinmodel in kleinere, beter beheersbare secties, elk met zijn eigen beperkte set aan kennis. Deze benadering helpt bij effectieve teamcommunicatie, omdat het de noodzaak van uitgebreide kennis van het hele domein vermindert en contextswitching minimaliseert, waardoor gesprekken efficiënter worden. Contextswitching is het ergste en meest resource-intensieve ooit. Zelfs computers hebben er moeite mee. Hoewel het onwaarschijnlijk is dat het helemaal niet mogelijk is om contextswitching te bereiken, denk ik dat we daar naar moeten streven.


Fantasy about keeping in mind a lot of bounded contexts

Terugkomend op de Wet van Conway: ik heb er een aantal problemen mee gevonden.


Het eerste probleem dat ik ben tegengekomen met de wet van Conway, die suggereert dat systeemontwerp de organisatiestructuur weerspiegelt, is de mogelijkheid om complexe en uitgebreide Bounded Contexts te vormen. Deze complexiteit ontstaat wanneer de organisatiestructuur niet is afgestemd op domeingrenzen, wat leidt tot Bounded Contexts die sterk onderling afhankelijk zijn en vol zitten met informatie. Het leidt tot frequente contextwisselingen voor het ontwikkelteam.


Een ander probleem is dat organisatorische terminologie doorlekt naar het codeniveau. Wanneer organisatiestructuren veranderen, zijn codebase-aanpassingen noodzakelijk, wat waardevolle resources verbruikt.


Dus, het volgen van de Inverse Conway Manoeuvre helpt om het systeem en de organisatie te bouwen die de gewenste softwarearchitectuur aanmoedigen. Het is echter opmerkelijk om te zeggen dat deze aanpak niet erg goed zal werken in reeds gevormde architectuur en structuren, aangezien veranderingen in dit stadium langdurig zijn, maar het presteert uitzonderlijk goed in startups, aangezien zij snel veranderingen doorvoeren.

Grote modderbal

Dit patroon of “anti-patroon” drijft het bouwen van een systeem zonder enige architectuur. Er zijn geen regels, geen grenzen en geen strategie om de onvermijdelijke groeiende complexiteit te beheersen. Complexiteit is de meest geduchte vijand in de reis van het bouwen van softwaresystemen.


Entertaining illustration made by ChatGPT

Om te voorkomen dat we een dergelijk systeem bouwen, moeten we ons aan specifieke regels en beperkingen houden.

Systeemarchitectuur

Er zijn talloze definities voor softwarearchitectuur. Ik vind er veel leuk, omdat ze verschillende aspecten ervan bestrijken. Om echter over architectuur te kunnen redeneren, moeten we er natuurlijk een aantal in ons hoofd vormen. En het is opmerkelijk om te zeggen dat deze definitie kan evolueren. Dus, althans voor nu, heb ik de volgende beschrijving voor mezelf.


Bij softwarearchitectuur gaat het om de beslissingen en keuzes die u dagelijks maakt en die van invloed zijn op het gebouwde systeem.


Om beslissingen te nemen moet je principes en patronen in je "tas" hebben om opkomende problemen op te lossen, het is ook essentieel om te stellen dat het begrijpen van de vereisten de sleutel is tot het bouwen van wat een bedrijf nodig heeft. Soms zijn vereisten echter niet transparant of zelfs niet gedefinieerd, in dat geval is het beter om te wachten tot er meer duidelijkheid is of te vertrouwen op je ervaring en je intuïtie. Maar hoe dan ook, je kunt geen goede beslissingen nemen als je geen principes en patronen hebt om op te vertrouwen. Dat is waar ik kom tot de definitie van Software Architecture Style.


De softwarearchitectuurstijl is een reeks principes en patronen die bepalen hoe software gebouwd moet worden.


Er zijn veel verschillende architectuurstijlen die zich op verschillende aspecten van de geplande architectuur richten. Het is dan ook normaal dat er meerdere van deze stijlen tegelijk worden toegepast.


Bijvoorbeeld, zoals:

  1. Monolithische architectuur

  2. Domeingestuurd ontwerp

  3. Component-gebaseerd

  4. Microdiensten

  5. Pijp en filters

  6. Gebeurtenisgestuurd

  7. Microkernel

  8. Servicegericht


enzovoort…


Natuurlijk hebben ze hun voor- en nadelen, maar het belangrijkste dat ik heb geleerd, is dat architectuur geleidelijk evolueert, afhankelijk van daadwerkelijke problemen. Beginnen met de monolithische architectuur is een geweldige keuze om operationele complexiteit te verminderen. Het is zeer waarschijnlijk dat deze architectuur aan uw behoeften voldoet, zelfs nadat u de Product-market Fit (PMI)-fase van het bouwen van het product hebt bereikt. Op schaal kunt u overwegen om over te stappen op een event-driven benadering en microservices om onafhankelijke implementatie, heterogene tech stack-omgeving en minder gekoppelde architectuur te bereiken (en in de tussentijd minder transparant vanwege de aard van event-driven en pub-sub-benaderingen als deze worden aangenomen). Eenvoud en efficiëntie liggen dicht bij elkaar en hebben een grote impact op elkaar. Meestal hebben gecompliceerde architecturen invloed op de ontwikkelingssnelheid van nieuwe functies, het ondersteunen en onderhouden van bestaande functies en het uitdagen van de natuurlijke evolutie van het systeem.


Complexe systemen vereisen echter vaak een complexe en uitgebreide architectuur, wat onvermijdelijk is.


Eerlijk gezegd is dit een heel erg breed onderwerp, en er zijn veel geweldige ideeën over hoe je systemen voor natuurlijke evolutie kunt structureren en bouwen. Op basis van mijn ervaring heb ik de volgende aanpak uitgewerkt:

  1. Begint bijna altijd met de monolithische architectuurstijl, omdat het de meeste problemen elimineert die ontstaan door de aard van gedistribueerde systemen. Het is ook logisch om modulaire monoliet te volgen om te focussen op het bouwen van componenten met duidelijke grenzen. Het toepassen van een componentgebaseerde aanpak zou ze kunnen helpen om met elkaar te communiceren door middel van gebeurtenissen, maar directe aanroepen (ook wel RPC genoemd) vereenvoudigen de zaken in het begin. Het is echter belangrijk om afhankelijkheden tussen componenten bij te houden, want als component A veel weet over component B, is het misschien logisch om ze samen te voegen tot één.
  2. Wanneer u dichter bij de situatie komt waarin u uw ontwikkeling en systeem moet opschalen, kunt u overwegen het Stangler- patroon te volgen om geleidelijk componenten te extraheren die onafhankelijk moeten worden geïmplementeerd of zelfs moeten worden geschaald met specifieke vereisten.
  3. Nu, als u een duidelijke visie op de toekomst hebt, wat een beetje ongelooflijk geluk is, kunt u beslissen over de gewenste architectuur. Op dit moment kunt u beslissen om over te stappen op microservicesarchitectuur door ook Orchestration- en Choreography-benaderingen toe te passen, CQRS-patronen te integreren voor onafhankelijke schaalbare schrijf- en leesbewerkingen, of zelfs te besluiten om bij monolithische architectuur te blijven als dat bij uw behoeften past.


Het is ook van belang om de cijfers en statistieken zoals DAU (Daily Active Users), MAU (Monthly Active Users), RPC (Request Per Second) en TPC (Transaction Per Second) te begrijpen. Dit kan u helpen bij het maken van keuzes, omdat de architectuur voor 100 actieve gebruikers en 100 miljoen actieve gebruikers verschilt.


Als laatste opmerking zou ik willen zeggen dat architectuur een significante impact heeft op het succes van het product. Slecht ontworpen architectuur voor de producten is vereist bij het schalen, wat zeer waarschijnlijk leidt tot mislukking, aangezien klanten niet zullen wachten terwijl u het systeem schaalt, ze zullen een concurrent kiezen, dus we moeten potentiële schaalvergroting voor zijn. Hoewel ik toegeef dat het soms geen lean-aanpak kan zijn, is het idee om een schaalbaar maar nog niet geschaald systeem te hebben. Aan de andere kant kost het hebben van een zeer gecompliceerd en al geschaald systeem zonder klanten of plannen om er veel van te krijgen, u geld voor uw bedrijf voor niets.

Selectie van technologiestack

Het selecteren van een technologiestack is ook een beslissing op macroniveau, aangezien deze van invloed is op het aannemen van personeel, de perspectieven op de natuurlijke evolutie van het systeem, de schaalbaarheid en de systeemprestaties.


Dit is de lijst met basisoverwegingen voor het kiezen van een technologiestack:

  • Projectvereisten en complexiteit. Een eenvoudige webapplicatie kan bijvoorbeeld worden gebouwd met het Blazor-framework als uw ontwikkelaars er ervaring mee hebben, maar vanwege het gebrek aan volwassenheid van WebAssembly kan het kiezen van React en Typescript voor succes op de lange termijn een betere beslissing zijn
  • Schaalbaarheid en prestatiebehoeften. Als u verwacht veel verkeer te ontvangen, kan het verstandig zijn om te kiezen voor ASP.NET Core in plaats van Django vanwege de superieure prestaties bij het verwerken van gelijktijdige verzoeken. Deze beslissing is echter afhankelijk van de omvang van het verkeer dat u verwacht. Als u mogelijk miljarden verzoeken met een lage latentie moet beheren, kan de aanwezigheid van Garbage Collection een uitdaging zijn.
  • Werving, ontwikkelingstijd en kosten. In de meeste gevallen zijn dit de factoren waar we om moeten geven. Time-to-market, onderhoudskosten en stabiliteit van de werving sturen uw zakelijke behoeften zonder obstakels.
  • Teamexpertise en -middelen. De vaardigheden van uw ontwikkelteam zijn een kritische factor. Het is over het algemeen effectiever om technologieën te gebruiken waar uw team al bekend mee is, tenzij er een sterke reden is om te investeren in het leren van een nieuwe stack.
  • Volwassenheid. Een sterke community en een rijk ecosysteem van bibliotheken en tools kunnen het ontwikkelingsproces aanzienlijk vergemakkelijken. Populaire technologieën hebben vaak betere community-ondersteuning, wat van onschatbare waarde kan zijn voor het oplossen van problemen en het vinden van bronnen. Zo kunt u bronnen besparen en u vooral richten op het product.
  • Onderhoud en ondersteuning op lange termijn. Denk aan de levensvatbaarheid van de technologie op lange termijn. Technologieën die breed worden toegepast en ondersteund, raken minder snel verouderd en ontvangen over het algemeen regelmatige updates en verbeteringen.


Hoe kan het combineren van meerdere technologiestacks de bedrijfsgroei beïnvloeden?

Vanuit één perspectief zou het introduceren van nog een stack uw werving kunnen opschalen, maar aan de andere kant brengt het extra onderhoudskosten met zich mee, omdat u beide stacks moet ondersteunen. Dus, zoals ik eerder zei, zou in mijn optiek alleen extra behoefte een argument moeten zijn om meer technologiestacks te integreren.


Maar hoe zit het met het principe van het selecteren van het beste gereedschap voor een specifiek probleem?

Soms heb je geen andere keus dan nieuwe hulpmiddelen aan te schaffen om een specifiek probleem op te lossen op basis van dezelfde overwegingen als hierboven genoemd. In zulke gevallen is het zinvol om de beste oplossing te selecteren.


Het creëren van systemen zonder hoge koppeling aan een specifieke technologie kan een uitdaging zijn. Toch is het nuttig om te streven naar een conditie waarin het systeem niet nauw gekoppeld is aan technologie, en het zal niet sterven als morgen een specifiek framework of tool kwetsbaar of zelfs verouderd wordt.


Een andere belangrijke overweging heeft betrekking op open-source en propriëtaire software-afhankelijkheden. Propriëtaire software biedt u minder flexibiliteit en de mogelijkheid om te worden aangepast. Toch is de gevaarlijkste factor vendor lock-in, waarbij u afhankelijk wordt van de producten, prijzen, voorwaarden en roadmap van een leverancier. Dit kan riskant zijn als de leverancier van richting verandert, de prijzen verhoogt of het product stopzet. Open-source software vermindert dit risico, omdat een enkele entiteit het niet controleert. Het elimineren van een enkelvoudig punt van falen op alle niveaus is een sleutel tot het bouwen van betrouwbare systemen voor groei.

Enkelvoudig punt van falen (SPOF)

Een single point of failure (SPOF) verwijst naar elk onderdeel van een systeem dat, als het faalt, ervoor zorgt dat het hele systeem stopt met functioneren. Het elimineren van SPOF's op alle niveaus is cruciaal voor elk systeem dat hoge beschikbaarheid vereist. Alles, inclusief kennis, personeel, systeemcomponenten, cloudproviders en internetkabels, kan falen.


Er zijn verschillende basistechnieken die we kunnen toepassen om 'single points of failure' te elimineren:

  1. Redundantie. Implementeer redundantie voor kritieke componenten. Dit betekent dat u back-upcomponenten hebt die het kunnen overnemen als de primaire component uitvalt. Redundantie kan worden toegepast op verschillende lagen van het systeem, waaronder hardware (servers, schijven), netwerken (links, switches) en software (databases, applicatieservers). Als u alles in één cloudprovider host en daar zelfs back-ups hebt, overweeg dan om regelmatig een extra back-up in een andere te maken om uw verloren kosten in geval van een ramp te beperken.
  2. Datacenters. Verdeel uw systeem over meerdere fysieke locaties, zoals datacenters of cloudregio's. Deze aanpak beschermt uw systeem tegen locatiespecifieke storingen zoals stroomuitval of natuurrampen.
  3. Failover. Pas een failover-aanpak toe voor al uw componenten (DNS, CDN, Load balancers, Kubernetes, API Gateways en Databases). Omdat problemen onverwacht kunnen ontstaan, is het cruciaal om een back-upplan te hebben om elk component indien nodig snel te vervangen door zijn kloon.
  4. High availability services. Zorg ervoor dat uw services vanaf het begin horizontaal schaalbaar en zeer beschikbaar zijn door de volgende principes te volgen:
    • Oefen service statelessness en vermijd het opslaan van gebruikerssessies in in-memory caches. Gebruik in plaats daarvan een gedistribueerd cachesysteem, zoals Redis.
    • Vermijd het vertrouwen op de chronologische volgorde van het berichtenverbruik bij het ontwikkelen van logica.
    • Minimaliseer breaking changes om te voorkomen dat API-consumenten worden verstoord. Kies waar mogelijk voor backwards-compatible changes. Houd ook rekening met de kosten, want soms kan het implementeren van een breaking change kosteneffectiever zijn.
    • Integreer migratie-uitvoering in de implementatiepijplijn.
    • Stel een strategie op voor het verwerken van gelijktijdige verzoeken.
    • Implementeer servicedetectie, monitoring en logging om de betrouwbaarheid en zichtbaarheid te verbeteren.
    • Ontwikkel een bedrijfslogica die idempotent is en erken dat netwerkstoringen onvermijdelijk zijn.
  5. Afhankelijkheidsbeoordeling. Controleer en minimaliseer externe afhankelijkheden regelmatig. Elke externe afhankelijkheid kan potentiële SPOF's introduceren, dus het is essentieel om deze risico's te begrijpen en te beperken.
  6. Regelmatige kennisdeling. Vergeet nooit hoe belangrijk het is om kennis binnen uw organisatie te verspreiden. Mensen kunnen onvoorspelbaar zijn en het is riskant om op één persoon te vertrouwen. Moedig teamleden aan om hun kennis te digitaliseren door middel van documentatie. Wees echter voorzichtig met overdocumentatie. Gebruik verschillende AI-tools om dit proces te vereenvoudigen.

Conclusie

In dit artikel bespreken we verschillende belangrijke macro- aspecten en hoe we met de complexiteit ervan kunnen omgaan.


Bedankt voor het lezen! Tot de volgende keer!