Dela via


Arkitekturprinciper

Dricks

Det här innehållet är ett utdrag från eBook, Architect Modern Web Applications med ASP.NET Core och Azure, som finns på .NET Docs eller som en kostnadsfri nedladdningsbar PDF som kan läsas offline.

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

"Om byggare byggde byggnader som programmerare skrev program, skulle den första hackspetten som kom med förstöra civilisationen."
- Gerald Weinberg

Du bör skapa och utforma programvarulösningar med underhållsbarhet i åtanke. Principerna som beskrivs i det här avsnittet kan hjälpa dig att fatta arkitektoniska beslut som resulterar i rena och underhållsbara program. I allmänhet vägleder dessa principer dig mot att skapa program av diskreta komponenter som inte är nära kopplade till andra delar av ditt program, utan snarare kommunicerar via explicita gränssnitt eller meddelandesystem.

Vanliga designprinciper

Avgränsning av problem

En vägledande princip vid utveckling är Separation av problem. Den här principen hävdar att programvara ska separeras baserat på vilken typ av arbete den utför. Tänk dig till exempel ett program som innehåller logik för att identifiera anmärkningsvärda objekt som ska visas för användaren och som formaterar sådana objekt på ett visst sätt för att göra dem mer märkbara. Det beteende som ansvarar för att välja vilka objekt som ska formateras ska hållas separat från det beteende som ansvarar för att formatera objekten, eftersom dessa beteenden är separata problem som bara är av en tillfällighet relaterade till varandra.

Arkitektoniskt kan program skapas logiskt för att följa den här principen genom att separera kärnverksamhetens beteende från infrastruktur och logik för användargränssnittet. Helst bör affärsregler och logik finnas i ett separat projekt, som inte bör vara beroende av andra projekt i programmet. Den här separationen hjälper till att säkerställa att affärsmodellen är enkel att testa och kan utvecklas utan att vara nära kopplad till implementeringsinformation på låg nivå (det hjälper också om infrastrukturproblem är beroende av abstraktioner som definieras i affärsskiktet). Separation av problem är en viktig faktor bakom användningen av lager i programarkitekturer.

Inkapsling

Olika delar av ett program bör använda inkapsling för att isolera dem från andra delar av programmet. Programkomponenter och lager bör kunna justera sin interna implementering utan att bryta sina medarbetare så länge externa kontrakt inte överträds. Korrekt användning av inkapsling hjälper till att uppnå lös koppling och modularitet i programdesign, eftersom objekt och paket kan ersättas med alternativa implementeringar så länge samma gränssnitt upprätthålls.

I klasser uppnås inkapsling genom att begränsa extern åtkomst till klassens interna tillstånd. Om en extern aktör vill ändra objektets tillstånd bör den göra det via en väldefinierad funktion (eller egenskapsuppsättning) i stället för att ha direkt åtkomst till objektets privata tillstånd. På samma sätt bör programkomponenter och program själva exponera väldefinierade gränssnitt som deras medarbetare kan använda, i stället för att tillåta att deras tillstånd ändras direkt. Den här metoden gör att programmets interna design kan utvecklas över tid utan att oroa dig för att det kommer att bryta samarbetspartners, så länge de offentliga kontrakten upprätthålls.

Föränderligt globalt tillstånd är antitetiskt för inkapsling. Det går inte att förlita sig på ett värde som hämtats från ett föränderligt globalt tillstånd i en funktion för att ha samma värde i en annan funktion (eller ännu längre i samma funktion). Att förstå problem med föränderligt globalt tillstånd är en av anledningarna till att programmeringsspråk som C# har stöd för olika omfångsregler, som används överallt från instruktioner till metoder till klasser. Det är värt att notera att datadrivna arkitekturer som förlitar sig på en central databas för integrering inom och mellan program själva väljer att vara beroende av det föränderliga globala tillstånd som representeras av databasen. Ett viktigt övervägande i domändriven design och ren arkitektur är hur du kapslar in åtkomst till data och hur du säkerställer att programtillståndet inte blir ogiltigt genom direkt åtkomst till dess beständighetsformat.

Inversion av beroende

Beroenderiktningen i programmet bör vara i abstraktionens riktning, inte implementeringsinformation. De flesta program är skrivna så att kompileringstidsberoendeflöden i riktning mot körningskörning, vilket skapar ett direkt beroendediagram. Om klass A anropar en metod av klass B och klass B anropar en metod av klass C, beror klass A vid kompileringstid på klass B, och klass B beror på klass C, som visas i bild 4-1.

Direct dependency graph

Bild 4-1. Diagram över direktberoende.

Genom att tillämpa principen för beroendeinversion kan A anropa metoder för en abstraktion som B implementerar, vilket gör det möjligt för A att anropa B vid körning, men för att B ska vara beroende av ett gränssnitt som styrs av A vid kompileringstillfället (vilket inverterar det typiska kompileringsberoendet ). Vid körningen förblir flödet av programkörning oförändrat, men införandet av gränssnitt innebär att olika implementeringar av dessa gränssnitt enkelt kan anslutas.

Inverted dependency graph

Bild 4-2. Inverterad beroendegraf.

Beroendeinversion är en viktig del av att skapa löst kopplade program, eftersom implementeringsinformation kan skrivas för att vara beroende av och implementera abstraktioner på högre nivå, snarare än tvärtom. De resulterande programmen är mer testbara, modulära och underhållsbara som ett resultat. Metoden med beroendeinmatning möjliggörs genom att följa principen om beroendeinvertering.

Explicita beroenden

Metoder och klasser bör uttryckligen kräva alla samarbetsobjekt som de behöver för att fungera korrekt. Den kallas för principen explicita beroenden. Klasskonstruktorer ger klasser möjlighet att identifiera de saker de behöver för att vara i ett giltigt tillstånd och fungera korrekt. Om du definierar klasser som kan konstrueras och anropas, men som bara fungerar korrekt om vissa globala komponenter eller infrastrukturkomponenter finns på plats, är dessa klasser oärliga mot sina klienter. Konstruktorkontraktet talar om för klienten att den bara behöver de angivna sakerna (möjligen ingenting om klassen bara använder en parameterlös konstruktor), men vid körning visade det sig att objektet verkligen behövde något annat.

Genom att följa den explicita beroendeprincipen är dina klasser och metoder är ärliga mot sina klienter om vad de behöver för att fungera. Genom att följa principen blir din kod mer självdokumentering och dina kodningskontrakt mer användarvänliga, eftersom användarna kommer att lita på att så länge de tillhandahåller vad som krävs i form av metod- eller konstruktorparametrar, fungerar objekten de arbetar med korrekt vid körning.

Enskilt ansvar

Principen för enskilt ansvar gäller för objektorienterad design, men kan också betraktas som en arkitekturprincip som liknar separation av problem. Det står att objekt bara ska ha ett ansvar och att de bara ska ha en anledning att ändra. Mer specifikt är den enda situationen där objektet ska ändras om det sätt på vilket det utför sitt enda ansvar måste uppdateras. Genom att följa den här principen kan du skapa mer löst kopplade och modulära system, eftersom många typer av nya beteenden kan implementeras som nya klasser, i stället för att lägga till ytterligare ansvar till befintliga klasser. Det är alltid säkrare att lägga till nya klasser än att ändra befintliga klasser, eftersom ingen kod ännu är beroende av de nya klasserna.

I ett monolitiskt program kan vi tillämpa principen för enskilt ansvar på en hög nivå på skikten i programmet. Presentationsansvaret bör finnas kvar i UI-projektet, medan dataåtkomstansvaret bör behållas inom ett infrastrukturprojekt. Affärslogik bör behållas i programmets kärnprojekt, där det enkelt kan testas och kan utvecklas oberoende av andra ansvarsområden.

När den här principen tillämpas på programarkitekturen och tas till dess logiska slutpunkt får du mikrotjänster. En viss mikrotjänst bör ha ett enda ansvar. Om du behöver utöka beteendet för ett system är det vanligtvis bättre att göra det genom att lägga till ytterligare mikrotjänster i stället för att lägga till ansvar till en befintlig.

Läs mer om arkitektur för mikrotjänster

Upprepa inte dig själv (DRY)

Programmet bör undvika att ange beteende som är relaterat till ett visst begrepp på flera platser eftersom den här metoden är en frekvent källa till fel. Vid något tillfälle kräver en ändring av kraven att det här beteendet ändras. Det är troligt att minst en instans av beteendet inte uppdateras och att systemet fungerar inkonsekvent.

I stället för att duplicera logik kapslar du in den i en programmeringskonstruktion. Gör den här konstruktionen till den enda auktoriteten för det här beteendet och låt alla andra delar av programmet som kräver det här beteendet använda den nya konstruktionen.

Kommentar

Undvik att binda samman beteenden som bara är av en tillfällighet repetitiva. Till exempel, bara för att två olika konstanter båda har samma värde, betyder det inte att du bara ska ha en konstant, om de konceptuellt refererar till olika saker. Duplicering är alltid att föredra framför koppling till fel abstraktion.

Okunskap om beständighet

Persistence ignorance (PI) avser typer som måste bevaras, men vars kod inte påverkas av valet av beständighetsteknik. Sådana typer i .NET kallas ibland oformaterade gamla CLR-objekt (POCOs), eftersom de inte behöver ärva från en viss basklass eller implementera ett visst gränssnitt. Okunskap om beständighet är värdefull eftersom den gör att samma affärsmodell kan bevaras på flera sätt, vilket ger ytterligare flexibilitet för programmet. Alternativ för beständighet kan ändras över tid, från en databasteknik till en annan, eller ytterligare former av beständighet kan krävas utöver det som programmet startade med (till exempel att använda en Redis-cache eller Azure Cosmos DB utöver en relationsdatabas).

Några exempel på överträdelser av den här principen är:

  • En obligatorisk basklass.

  • En nödvändig gränssnittsimplementering.

  • Klasser som ansvarar för att spara sig själva (till exempel mönstret Aktiv post).

  • Nödvändig parameterlös konstruktor.

  • Egenskaper som kräver virtuellt nyckelord.

  • Beständighetsspecifika obligatoriska attribut.

Kravet på att klasser har någon av ovanstående funktioner eller beteenden lägger till koppling mellan de typer som ska bevaras och valet av beständig teknik, vilket gör det svårare att anta nya strategier för dataåtkomst i framtiden.

Avgränsade kontexter

Avgränsade kontexter är ett centralt mönster i domändriven design. De ger ett sätt att hantera komplexiteten i stora program eller organisationer genom att dela upp den i separata konceptuella moduler. Varje konceptuell modul representerar sedan en kontext som är separerad från andra kontexter (därav avgränsade) och kan utvecklas oberoende av varandra. Varje avgränsad kontext bör helst vara fri att välja sina egna namn för begrepp inom den och bör ha exklusiv åtkomst till sin egen beständighetsbutik.

Enskilda webbprogram bör åtminstone sträva efter att vara sin egen avgränsade kontext, med ett eget beständighetslager för sin affärsmodell, i stället för att dela en databas med andra program. Kommunikation mellan avgränsade kontexter sker via programmatiska gränssnitt, snarare än via en delad databas, vilket gör att affärslogik och händelser kan äga rum som svar på ändringar som sker. Avgränsade kontexter mappar nära mikrotjänster, som också är idealiskt implementerade som sina egna individuella avgränsade kontexter.

Ytterligare resurser