Så inför du modulär arkitektur på ett säkert sätt i äldre mjukvara
Christian Clausen
Christian is a Technical Agile Coach working in Aarhus. He has written a book: Five lines of code - how and when to refactor. Before he was working as a Senior CRM Consultant. He likes wood-working and he has built a climbing wall at his home. He loves helping others and learning. His dream is one day to start a family or live on Mars, whichever comes first.
<br>
<br>
<a href="https://www.eficode.com/christian-clausen">Find out more about Christian</a>
I mjukvaruutveckling är tät koppling en av våra största fiender. På funktionsnivå gör den applikationen svår att förändra och skör. Tyvärr är tät koppling som entropin i mjukvaruutveckling, så vi måste alltid arbeta för att minska den.
Introduktion
I det här blogginlägget förklarar jag hur du inför en modulär arkitektur i ett äldre system. Målet är att presentera en process som tar oss från det mest tätt kopplade systemet, en monolit, till det mest löst kopplade system som organisationen tillåter. Vi vill att separata team ska kunna leverera och testa separata komponenter för att möjliggöra mindre batchar och kortare återkopplingsloopar. Vi vill också att systemet ska vara robust och enkelt att skala.
Utgångspunkt
För att den här guiden ska vara tillämplig behöver några saker stämma. Vi utgår från att systemet är verksamhetskritiskt och används i dag. Vi utgår också från att fler än ett utvecklingsteam arbetar med systemet. Vanligtvis får dessa team funktionsförfrågningar med kopplat affärsvärde eller prioritet. Vi antar även att vi arbetar i ett kompilerat objektorienterat språk, som Java eller C#. Det mesta gäller för alla arbetssätt med datainkapsling, till exempel tjänster som kommunicerar via REST. Vi utgår också från att källkoden versionshanteras.
Begränsningar
Vi arbetar med verklig mjukvara som utvecklas och används av verkliga människor, så det finns några mänskliga begränsningar som vi behöver ta hänsyn till för att göra den här transformationen praktiskt genomförbar.
Conways lag
Conways lag säger att en organisation skapar mjukvara som speglar dess kommunikationsstruktur. Det innebär att det inte räcker att dela upp mjukvaran. Vi måste säkerställa att kommunikationen i organisationen styrs av verksamheten på ett sätt som motsvarar den struktur vi vill se i koden. Om vi vill frikoppla modulerna måste vi frikoppla teamen.
Alltid ”grönt”
Vanligtvis syftar ”grönt” på godkända tester, men här använder vi begreppet mer generellt för att beskriva att systemet fungerar. Vi är ”gröna” om mjukvaran körs och användarna kan använda den. Vi utgår inte från att mjukvaran har automatiserade tester. När vi arbetar med ett verksamhetskritiskt system som är i drift är det inte ett alternativ att stänga ner det, refaktorera det och sedan starta det igen. Människor är beroende av systemet för att kunna utföra sitt arbete, så vi måste säkerställa att systemet förblir grönt under hela processen. Ett sätt att säkerställa det är genom omfattande testning, men i vissa situationer är det inte möjligt. Då behöver vi få så mycket hjälp som möjligt från andra verktyg, till exempel kompilatorn. Om ett kritiskt problem uppstår måste vi kunna åtgärda det med så lite extra arbete som möjligt.
Upprätthåll servicenivån
Att upprätthålla servicenivån vad gäller prestanda, funktionalitet och felfrekvens är också en viktig begränsning. Systemet bör inte bli märkbart sämre under transformationen. Det bör faktiskt bli bättre i slutändan.
Steg 0: Identifiera moduler
Om vi vill att varje team ska kunna driftsätta oberoende behöver vi först avgöra vad de ska driftsätta. Låt oss ha Conways lag i åtanke – modulerna i vår kod kommer att spegla kommunikationen i organisationen. Helst vill vi kommunicera mycket inom teamet och betydligt mindre med andra team, men det fungerar inte om vi saknar den data eller åtkomst vi behöver.
Vi behöver dela upp applikationens funktionalitet på ett sätt som begränsar behovet av samordning och överlämningar mellan team. Det kan vara svårt att hitta en bra ansvarsfördelning, men en användbar metod är att börja med att titta på datan. Låt varje team ange vilken data de bör äga och ge dem sedan den. Merparten av datan kommer endast att efterfrågas av ett team, men viss data kommer flera team att göra anspråk på. Då har vi tre alternativ. Om datan behövs av en stor andel av fler än ett team kan det vara rimligt att slå ihop dem. Om den behövs av ungefär en tredjedel av båda teamen kanske de två teamen bör bli tre, där ett team endast ansvarar för den gemensamma delen. Om behovet är litet för båda teamen är det förmodligen bäst att duplicera datan.
Steg 1: Isolera moduler
När vi har identifierat vilka moduler vi vill ha behöver vi göra dem oberoende. Vi gör det stegvis, och det första steget är att skapa en enda ingångspunkt. Den här ingångspunkten visar exakt vilket externt gränssnitt vi har just nu. Den gör också att vi kan förändra det interna arbetssättet hur mycket vi vill, så länge det externa gränssnittet förblir detsamma. Det gör oss mindre sårbara. För att göra detta säkert skapar vi en tom klass, fasaden. Sedan gör vi varje metod i modulen tillgänglig endast inom modulen. Då genererar kompilatorn fel överallt där vi använder en metod utanför modulen. För varje sådant fel ändrar vi helt enkelt anropet till en ny publik metod i fasaden. Den kan nu anropa den interna metoden eftersom den finns i vår modul.
Steg 2: Minska kommunikationen
Nästa steg är att minska kommunikationen mellan moduler. Det gör vi genom att flytta funktionalitet så nära datan som möjligt. I stället för att hämta data och sedan omvandla den flyttar du omvandlingen till datan och begär resultatet. Det gäller både inom moduler och över modulgränser. Att flytta funktionaliteten innebär också att vi eliminerar det mesta av behovet av att returnera värden. När beräkningen kommer närmare datan kan vi skicka den som parametrar och anropa vidare. Det finns flera refaktoreringsmönster som kan hjälpa till med detta, men de ligger utanför ramen för det här inlägget. Läs mer om denna specifika uppgift här:
Steg 3: Eliminera direkt kommunikation
Det enda som hindrar oss från att driftsätta separata moduler i det här läget är de cirkulära beroenden som uppstår när vi anropar moduler som i sin tur anropar oss. Efter det sista steget är det enkelt att gå över till ett helt push-baserat system. Vi inför en central meddelandekö som alla kan skicka meddelanden till. Vi lägger också till en observer-klass i varje modul som kan övervaka meddelandekön. När ett relevant meddelande hamnar i meddelandekön anropar observern rätt metod i fasaden. Nu gör vi metoderna i fasaden åtkomliga endast inom vår modul, precis som vi gjorde tidigare. Återigen visar kompilatorn överallt där vi använder fasaden, och där skickar vi i stället en relevant meddelandetyp till den centrala meddelandekön. Om meddelandekön är mycket enkel kanske vi inte behöver underhålla den alls. Så är fallet om vi använder en molnlösning eller om det är en vanlig intern kö. Annars behöver vi avsätta ett team för att även underhålla den. Nu har allt bara ett beroende: den centrala meddelandekön, som inte har några beroenden. Vi kan nu flytta våra moduler till egna projekt, i egna repos, och kompilera dem separat från allt annat – förutom meddelandekön. Det innebär också att vi kan leverera vår dll, jar eller tjänst oberoende av övriga delar.
Vanliga utmaningar
Den här transformationen medför några utmaningar. Här går vi igenom de vanligaste.
Kommunikation mellan team
Som vi diskuterade tidigare räcker det inte att ändra vår mjukvara – enligt Conways lag måste vi också hantera hur organisationen kommunicerar, särskilt mellan team. Eftersom vår arkitektur bygger på en central meddelandekö bör kommunikationen följa samma mönster: ett centralt API som dokumenterar alla metoder som systemet stöder. Eftersom vårt system nu är push-baserat bör även vår kommunikation vara det. Vi skickar funktionsförfrågningar till andra team. Sedan använder vi vår föredragna Agile-process för att utveckla den nya funktionen. När funktionen är klar publicerar det utvecklande teamet information i det centrala API:t om nya meddelandetyper som möter våra behov. Detta bör vara den enda professionella kommunikation som behövs.
Beroenden och versionshantering
Vårt mål var att frikoppla komponenter så att vi kunde driftsätta dem separat. Det väcker naturligt frågan om hur vi håller dem synkroniserade med varandra. Vi löser vanligtvis detta genom att behandla det som ett REST API. Vi kodar versionsnumret i meddelandet. Därefter ansvarar varje observer för att anropa rätt metod i fasaden. Det innebär att vår kod kan ha flera versioner av samma funktionalitet parallellt, så länge vi behöver stödja dem.
Testning
Att dela upp en applikation på det här sättet blottlägger ofta delar av vår kod som inte har testats eller har testats bristfälligt. Precis som med funktionaliteten bör vi sträva efter att flytta testerna så nära datan som möjligt. Varje gång något misslyckas bör vi därför fråga oss: ”Hur kan vi flytta det här felet ett steg närmare datan?”
För fullständighetens skull visar vi relevanta testnivåer:
Enhetstester av metoder i en modul. Ägs av teamet eller utvecklaren.
Komponenttester som säkerställer att ett meddelande till en modul i slutändan leder till att rätt meddelande eller meddelanden skickas till meddelandekön. Ägs av teamet.
Integrationstester som säkerställer att flera komponenter hanterar meddelanden korrekt. Ägs gemensamt av de berörda teamen.
Systemtester för funktionalitet som omfattar de flesta eller alla komponenter. Ägs av organisationen.
Produktion testar om systemet fungerar. Ägs av organisationen.
Slutsats
I det här läget har vi ett antal team, vart och ett med en modul som de kan bygga och leverera oberoende av varandra. Vi har gjort systemet mindre sårbart genom att isolera varje modul. Om vi dessutom vill skala horisontellt kan vi enkelt anpassa våra observers för att använda en intern arbetskö och koppla fler instanser av modulen till den.
Systemet stöder till och med att ny funktionalitet läggs till dynamiskt i form av plugins. Det beror på att vi kan prenumerera på samma meddelanden som systemet använder, eller skapa nya, utan att ändra eller påverka någon befintlig kod. Vi har också sett till att systemet har varit i drift från början till slut. Det vi har brutit har alltid varit lokalt, mellan commits. Därför bör produktionen inte ha påverkats. När vi har brutit något har vi gjort det under korta perioder, varje gång med hjälp av kompilatorn. Vi har potentiellt också öppnat nya möjligheter för infrastrukturen. Om vi började med en enda applikation kan vi nu välja att göra den centrala meddelandekön tillgänglig över nätverket och driftsätta tjänster på olika servrar, vilket skapar en mikrotjänstarkitektur. Vi kan till och med dela upp våra moduler så att varje funktion i vår fasad blir en egen minimodul som kan driftsättas separat. Det skulle vara en serverless-arkitektur.
Det kan vara en lång process att komma hit, men Rom byggdes inte på en dag. Fördelarna med mindre batchstorlekar, mer robust kod och de kortare feedbackcykler som transformationen möjliggör gör den väl värd insatsen.
- DevOps
- CI/CD
Subscribe to our newsletter
Related blogs