paint-brush
Hur man hanterar komplexitet när man designar mjukvarusystemförbi@fairday
64,370 avläsningar
64,370 avläsningar

Hur man hanterar komplexitet när man designar mjukvarusystem

förbi Aleksei23m2024/02/05
Read on Terminal Reader
Read this story w/o Javascript

För länge; Att läsa

Komplexitet är fienden! Låt oss lära oss hur man hanterar det!
featured image - Hur man hanterar komplexitet när man designar mjukvarusystem
Aleksei HackerNoon profile picture

Vad handlar det om?

Varje dag, varje ögonblick under vår ingenjörskarriär, stöter vi på många olika problem av olika komplexitet och situationer där vi behöver fatta ett beslut eller skjuta upp det på grund av brist på data. Närhelst vi bygger nya tjänster, konstruerar infrastruktur eller till och med formar utvecklingsprocesser, berör vi en enorm värld av olika utmaningar.


Det är utmanande, och kanske till och med omöjligt, att lista alla problem. Du kommer bara att stöta på några av dessa problem om du arbetar inom en specifik nisch. Å andra sidan finns det många som vi alla måste förstå hur vi ska lösa, eftersom de är avgörande för att bygga IT-system. Med stor sannolikhet kommer du att stöta på dem i alla projekt.


I den här artikeln kommer jag att dela mina erfarenheter av några av de problem jag har stött på när jag skapade program.

Vad är tvärgående oro?

Om vi tittar på Wikipedia hittar vi följande definition


Inom aspektorienterad mjukvaruutveckling är övergripande problem aspekter av ett program som påverkar flera moduler, utan möjlighet att vara inkapslade i någon av dem. Dessa problem kan ofta inte dekomponeras rent från resten av systemet i både design och implementering, och kan resultera i antingen spridning (kodduplicering), trassling (betydande beroenden mellan system) eller båda.


Det beskriver mycket vad det är, men jag vill utöka och förenkla det lite:

Ett övergripande problem är ett koncept eller en komponent i systemet/organisationen som påverkar (eller "skär över") många andra delar.


De bästa exemplen på sådana problem är systemarkitektur, loggning, säkerhet, transaktionshantering, telemetri, databasdesign och det finns många andra. Vi kommer att utveckla många av dem senare i den här artikeln.


På kodnivån implementeras ofta övergripande problem med hjälp av tekniker som Aspect-Oriented Programming (AOP) , där dessa problem är modulariserade till separata komponenter som kan tillämpas genom hela applikationen. Detta håller affärslogiken isolerad från dessa problem, vilket gör koden mer läsbar och underhållbar.

Aspektklassificering

Det finns många möjliga sätt att klassificera aspekter genom att segmentera dem med olika egenskaper som omfattning, storlek, funktionalitet, betydelse, mål och andra, men i den här artikeln kommer jag att använda en enkel omfattningsklassificering. Med detta menar jag var denna specifika aspekt riktas oavsett om det är hela organisationen, ett visst system eller en specifik del av det systemet.


Så jag ska dela upp aspekter i makro och mikro .


Med makroaspekt menar jag främst överväganden vi följer för hela systemet som vald systemarkitektur och dess design (monolitisk, mikrotjänster, tjänsteorienterad arkitektur), teknologistack, organisationsstruktur etc. Makroaspekter är främst relaterade till strategiska och högnivåiga aspekter beslut.


Under tiden är mikroaspekten mycket närmare kodnivån och utvecklingen. Till exempel vilket ramverk som används för att interagera med databasen, projektstrukturen för mappar och klasser, eller till och med specifika objektdesignmönster.


Även om denna klassificering inte är idealisk, hjälper den till att strukturera en förståelse av möjliga problem och vikten och effekten av lösningar vi tillämpar på dem.


I den här artikeln kommer mitt primära fokus att ligga på makroaspekterna.

Makroaspekter

Organisationsstruktur

När jag precis började lära mig om mjukvaruarkitektur läste jag många intressanta artiklar om Conways lag och dess inverkan på organisationsstrukturen. Speciellt den här . Så den här lagen säger det


Varje organisation som designar ett system (brett definierat) kommer att producera en design vars struktur är en kopia av organisationens kommunikationsstruktur.


Jag har alltid trott att detta koncept verkligen är väldigt universellt och representerar den gyllene regeln.


Sedan började jag lära mig Eric Evans's Domain-Driven Design (DDD) tillvägagångssätt för modelleringssystem. Eric Evans betonar vikten av Bounded Context-identifiering. Detta koncept innebär att dela upp en komplex domänmodell i mindre, mer hanterbara sektioner, var och en med sin egen begränsade kunskapsuppsättning. Detta tillvägagångssätt hjälper till med effektiv teamkommunikation, eftersom det minskar behovet av omfattande kunskap om hela domänen och minimerar kontextbyten, vilket gör konversationerna mer effektiva. Kontextbyte är det värsta och mest resurskrävande någonsin. Även datorer kämpar med det. Även om det är osannolikt att man uppnår en fullständig frånvaro av kontextbyte, tror jag att det är det vi bör sträva efter.


Fantasy about keeping in mind a lot of bounded contexts

För att återgå till Conways lag har jag hittat flera problem med den.


Det första problemet jag har stött på med Conways lag, som antyder att systemdesign speglar organisationsstruktur, är potentialen för att forma komplexa och omfattande Bounded Contexts. Denna komplexitet uppstår när organisationsstrukturen inte är anpassad till domängränser, vilket leder till Bounded Contexts som är starkt beroende av varandra och laddade med information. Det leder till frekvent kontextbyte för utvecklingsteamet.


En annan fråga är att organisatorisk terminologi läcker till kodnivå. När organisationsstrukturer förändras, kräver det kodbasmodifieringar, vilket förbrukar värdefulla resurser.


Följande Inverse Conway Maneuver hjälper alltså till att bygga systemet och organisationen som uppmuntrar önskad mjukvaruarkitektur. Det är dock anmärkningsvärt att säga att detta tillvägagångssätt inte kommer att fungera särskilt bra i redan bildade arkitekturer och strukturer eftersom förändringar i detta skede är förlängda, men det är exceptionellt presterande i startups eftersom de är snabba att införa ändringar.

Stor boll av lera

Detta mönster eller "anti-mönster" driver att bygga ett system utan någon arkitektur. Det finns inga regler, inga gränser och ingen strategi för hur man ska kontrollera den oundvikliga växande komplexiteten. Komplexitet är den mest formidabla fienden på resan för att bygga mjukvarusystem.


Entertaining illustration made by ChatGPT

För att undvika att konstruera en sådan typ av system måste vi följa specifika regler och begränsningar.

Systemarkitektur

Det finns otaliga definitioner för mjukvaruarkitektur. Jag gillar många av dem eftersom de täcker olika aspekter av det. Men för att kunna resonera kring arkitektur behöver vi naturligtvis forma några av dem i våra sinnen. Och det är anmärkningsvärt att säga att denna definition kan utvecklas. Så, åtminstone för nu, har jag följande beskrivning för mig själv.


Software Architecture handlar om beslut och val du gör varje dag som påverkar det byggda systemet.


För att fatta beslut du måste ha i din "väska" principer och mönster för att lösa uppkommande problem, är det också viktigt att konstatera att förståelse av kraven är nyckeln till att bygga vad ett företag behöver. Men ibland är kraven inte transparenta eller till och med inte definierade, i det här fallet är det bättre att vänta för att få mer förtydligande eller lita på din erfarenhet och lita på din intuition. Men hur som helst, du kan inte fatta beslut ordentligt om du inte har principer och mönster att lita på. Det är där jag kommer till definitionen av Software Architecture Style.


Software Architecture Style är en uppsättning principer och mönster som anger hur man bygger programvara.


Det finns många olika arkitektoniska stilar fokuserade på olika sidor av den planerade arkitekturen, och att tillämpa flera av dem samtidigt är en normal situation.


Till exempel, som:

  1. Monolitisk arkitektur

  2. Domändriven design

  3. Komponentbaserat

  4. Mikrotjänster

  5. Rör och filter

  6. Händelsestyrd

  7. Mikrokärna

  8. Serviceinriktad


och så vidare…


Naturligtvis har de sina fördelar och nackdelar, men det viktigaste jag har lärt mig är att arkitekturen utvecklas gradvis och beror på faktiska problem. Att börja med den monolitiska arkitekturen är ett utmärkt val för att minska operativa komplexiteter, mycket troligt kommer denna arkitektur att passa dina behov även efter att ha nått produkt-marknadsanpassning (PMI) stadiet för att bygga produkten. I stor skala kan du överväga att gå mot ett händelsedrivet tillvägagångssätt och mikrotjänster för att uppnå oberoende distribution, heterogen teknisk stackmiljö och mindre kopplad arkitektur (och mindre transparent under tiden på grund av karaktären av händelsedrivna och pub-sub-metoder om dessa antas). Enkelhet och effektivitet ligger nära och har stor inverkan på varandra. Vanligtvis påverkar komplicerade arkitekturer utvecklingshastigheten för nya funktioner, stöder och underhåller befintliga och utmanar systemets naturliga utveckling.


Men komplexa system kräver ofta komplex och omfattande arkitektur, vilket är oundvikligt.


Rättvist, detta är ett väldigt brett ämne, och det finns många bra idéer om hur man strukturerar och bygger system för naturlig evolution. Baserat på mina erfarenheter har jag utarbetat följande tillvägagångssätt:

  1. Börjar nästan alltid med den monolitiska arkitekturstilen eftersom den eliminerar de flesta problem som uppstår på grund av de distribuerade systemens natur. Det är också vettigt att följa modulär monolit för att fokusera på byggkomponenter med tydliga gränser. Att tillämpa ett komponentbaserat tillvägagångssätt kan hjälpa dem att kommunicera med varandra genom att använda händelser, men att ha direktanrop (aka RPC) förenklar saker i början. Det är dock viktigt att spåra beroenden mellan komponenter eftersom om komponent A vet mycket om komponent B, kanske det är vettigt att slå samman dem till en.
  2. När du kommer närmare situationen när du behöver skala din utveckling och ditt system, kan du överväga att följa Stangler- mönstret för att gradvis extrahera komponenter som behöver distribueras oberoende eller till och med skalas med specifika krav.
  3. Nu, om du har en tydlig framtidsvision, vilket är lite otrolig tur, kan du bestämma dig för den önskade arkitekturen. I det här ögonblicket kan du bestämma dig för att gå mot mikrotjänstarkitektur genom att också tillämpa tillvägagångssätt för orkestrering och koreografi, inkludera CQRS-mönster för oberoende skriv- och läsoperationer, eller till och med bestämma dig för att hålla fast vid monolitisk arkitektur om det passar dina behov.


Det är också viktigt att förstå siffrorna och mätvärdena som DAU (Daily Active Users), MAU (Monthly Active Users), RPC (Request Per Second) och TPC (Transaction Per Second) eftersom det kan hjälpa dig att göra val eftersom arkitektur för 100 aktiva användare och 100 miljoner aktiva användare är olika.


Som en sista anmärkning skulle jag säga att arkitektur har en betydande inverkan på produktens framgång. Dåligt utformad arkitektur för produkterna krävs vid skalning, vilket med stor sannolikhet leder till misslyckanden eftersom kunderna inte väntar medan du skalar systemet, de kommer att välja en konkurrent, så vi måste ligga före potentiell skalning. Även om jag erkänner att det ibland inte kunde vara ett slankt tillvägagångssätt, är tanken att ha ett skalbart men inte redan skalat system. Å andra sidan, att ha ett mycket komplicerat och redan skalat system utan kunder eller planer på att skaffa många av dem kommer att kosta dig pengar på ditt företag för ingenting.

Val av teknikstack

Att välja en teknikstack är också ett beslut på makronivå eftersom det påverkar anställning, systemets naturliga utvecklingsperspektiv, skalbarhet och systemprestanda.


Det här är listan över grundläggande överväganden för att välja en teknikstack:

  • Projektkrav och komplexitet. Till exempel kan en enkel webbapplikation byggas med Blazor-ramverket om dina utvecklare har erfarenhet av det, men på grund av bristen på mognad hos WebAssembly kan det vara ett bättre beslut att välja React och Typescript för långsiktig framgång.
  • Skalbarhet och prestandabehov. Om du räknar med att få en stor mängd trafik kan det vara ett klokt val att välja ASP.NET Core framför Django på grund av dess överlägsna prestanda vid hantering av samtidiga förfrågningar. Detta beslut beror dock på vilken trafikomfattning du förväntar dig. Om du behöver hantera potentiellt miljarder förfrågningar med låg latens, kan närvaron av Garbage Collection vara en utmaning.
  • Anställning, utvecklingstid och kostnad. I de flesta fall är det dessa faktorer vi måste bry oss om. Time to Market, underhållskostnader och anställningsstabilitet driver dina affärsbehov utan hinder.
  • Team expertis och resurser. Kompetensuppsättningen hos ditt utvecklingsteam är en kritisk faktor. Det är generellt sett mer effektivt att använda tekniker som ditt team redan är bekant med om det inte finns en stark anledning att investera i att lära sig en ny stack.
  • Mognad. En stark gemenskap och ett rikt ekosystem av bibliotek och verktyg kan avsevärt underlätta utvecklingsprocessen. Populära tekniker har ofta bättre stöd från samhället, vilket kan vara ovärderligt för att lösa problem och hitta resurser. Därmed kan du spara resurser och fokusera främst på produkten.
  • Långsiktigt underhåll och support. Tänk på teknikens långsiktiga lönsamhet. Teknik som är allmänt antagen och stöds är mindre benägna att bli föråldrade och får i allmänhet regelbundna uppdateringar och förbättringar.


Hur kan det påverka affärstillväxten att ha flera teknikstackar?

Ur ett perspektiv kan införandet av ytterligare en stack skala din anställning, men å andra sidan medför det extra underhållskostnader eftersom du behöver stödja båda stackarna. Så, som jag sa tidigare, enligt min synvinkel borde bara extra behov vara ett argument för att införliva fler teknikstackar.


Men vad handlar om principen att välja det bästa verktyget för ett specifikt problem?

Ibland har du inget annat val än att ta med nya verktyg för att lösa ett specifikt problem baserat på samma överväganden som ovan, i sådana fall är det vettigt att välja den bästa lösningen.


Att skapa system utan hög koppling till en specifik teknik kan vara en utmaning. Ändå är det bra att sträva efter ett tillstånd där systemet inte är tätt kopplat till tekniken och det inte kommer att dö om imorgon ett specifikt ramverk eller verktyg blir sårbart eller till och med utfasas.


En annan viktig faktor är relaterad till öppen källkod och proprietära mjukvaruberoenden. Proprietär programvara ger dig mindre flexibilitet och möjlighet att anpassas. Ändå är den farligaste faktorn leverantörslåsning, där du blir beroende av en leverantörs produkter, priser, villkor och färdplan. Detta kan vara riskabelt om leverantören ändrar riktning, höjer priserna eller slutar tillverka produkten. Programvara med öppen källkod minskar denna risk, eftersom en enda enhet inte kontrollerar den. Att eliminera en enda punkt av misslyckande på alla nivåer är nyckeln till att bygga tillförlitliga system för tillväxt.

Single Point of Failure (SPOF)

En enda punkt av fel (SPOF) hänvisar till vilken del av ett system som helst som, om det misslyckas, kommer att få hela systemet att sluta fungera. Att eliminera SPOF på alla nivåer är avgörande för alla system som kräver hög tillgänglighet. Allt, inklusive kunskap, personal, systemkomponenter, molnleverantörer och internetkablar, kan misslyckas.


Det finns flera grundläggande tekniker vi kan använda för att eliminera enstaka felpunkter:

  1. Redundans. Implementera redundans för kritiska komponenter. Det innebär att ha backup-komponenter som kan ta över om den primära komponenten misslyckas. Redundans kan appliceras över olika lager av systemet, inklusive hårdvara (servrar, diskar), nätverk (länkar, switchar) och mjukvara (databaser, applikationsservrar). Om du är värd för allt i en molnleverantör och till och med har säkerhetskopior där, överväg att bygga en vanlig extra säkerhetskopia i en annan för att minska dina förlorade kostnader i händelse av en katastrof.
  2. Datacenter. Distribuera ditt system över flera fysiska platser, till exempel datacenter eller molnregioner. Detta tillvägagångssätt skyddar ditt system mot platsspecifika fel som strömavbrott eller naturkatastrofer.
  3. Failover. Använd en failover-metod för alla dina komponenter (DNS, CDN, belastningsutjämnare, Kubernetes, API-gateways och databaser). Eftersom problem kan uppstå oväntat är det viktigt att ha en backupplan för att snabbt ersätta alla komponenter med sin klon efter behov.
  4. Hög tillgänglighetstjänster. Se till att dina tjänster är byggda för att vara horisontellt skalbara och mycket tillgängliga från början genom att följa följande principer:
    • Öva tjänstens statslöshet och undvik att lagra användarsessioner i minnescacher. Använd istället ett distribuerat cachesystem, som Redis.
    • Undvik att lita på den kronologiska ordningen för meddelandekonsumtion när du utvecklar logik.
    • Minimera brytande ändringar för att förhindra att API-konsumenter störs. Om möjligt, välj bakåtkompatibla ändringar. Tänk också på kostnaden eftersom det ibland kan vara mer kostnadseffektivt att implementera en brytande förändring.
    • Inkludera migreringskörning i distributionspipeline.
    • Upprätta en strategi för att hantera samtidiga förfrågningar.
    • Implementera tjänsteupptäckt, övervakning och loggning för att förbättra tillförlitligheten och observerbarheten.
    • Utveckla affärslogik för att vara idempotent och erkänna att nätverksfel är oundvikliga.
  5. Beroendegranskning. Se över och minimera externa beroenden regelbundet. Varje externt beroende kan introducera potentiella SPOF, så det är viktigt att förstå och mildra dessa risker.
  6. Regelbunden kunskapsdelning. Glöm aldrig vikten av att sprida kunskap inom din organisation. Människor kan vara oförutsägbara och att förlita sig på en enda individ är riskabelt. Uppmuntra teammedlemmar att digitalisera sin kunskap genom dokumentation. Tänk dock på att överdokumentera. Använd olika AI-verktyg för att förenkla denna process.

Slutsats

I den här artikeln täckte vi flera viktiga makroaspekter och hur vi kan hantera deras komplexitet.


Tack för att du läser! Vi ses nästa gång!