I vår ständiga strävan efter enkelhet och separation befinner vi oss nu i eran av webbtjänster, eller mikrotjänster om man vill ta konceptet till sin extrem.

Vi är på väg att jobba oss bort från de överlagrade och överkomplexa monoliterna från igår, med goda skäl. Men att bygga mikrotjänster är inte heller okomplicerat. I värsta fall får man det sämsta av två världar: en distribuerad monolit, som inte bara är svåröverskådlig för att den är stor och komplex, utan dessutom för att den är fragmenterad och utspridd.

Så det första man bör tänka på är att hålla sina tjänster väl separerade, med litet eller inget beroende till varandra.

Nästa problem blir då att avgöra vad som är en tjänst och vad som snarare bör vara uppdelat i flera tjänster, eller kanske ingå som en del av en tjänst. Det korta svaret är att det beror på, framförallt på hur domänen ser ut och vilka användarfall applikationen ska uppfylla. En hel del klokt har skrivits om detta inom området domändriven design, varför vi inte behöver gå in närmare på det här.

Men anta att vi har hittat ett väl avgränsat ansvar för vår webbtjänst. Hur bör webbtjänsten organiseras för att på bästa sätt uppfylla användarnas behov och vara anpassningsbar efter framtida krav? Det här är ett mindre väl utforskat område. Många projekt-team hittar sin egen ansats, genom en blandning av kunskap, erfarenhet och digert tankearbete. Eller så blir varje tjänst olika, beroende på vem som skriver den eller andra tillfälligheter. Dessutom händer det allt som oftast att en från början välkomponerad webbtjänst degenererar till en big ball of mud efterhand som funktionaliteten utökas och fler kockar blandar sig i soppan.

Att organisera en webbtjänst

För att råda bot på den osäkerhet som ofta råder kring hur en webbtjänst bör utformas och utvecklas har jag kondenserat mina erfarenheter på området till en tydlig modell för webbtjänstutveckling: Mission-Story-Domain-modellen.

Det här är en modell som bygger på S.O.L.I.D-principerna och som kan tillämpas oberoende av domän och teknikval (T ex webbramverk, REST/SOAP, CQRS/synkron m.m.). Men för att hålla det någorlunda konkret antar vi att tjänsten följer REST-protokollet, är synkron (konsumenten väntar på resultat som svar på sina kommandon) och är implementerad som en Microsoft ASP.NET web service ovanpå .Net Core.

Modellen tar sin utgångspunkt i att utveckling av en webbtjänst (liksom i stort sett all annan mjukvaruutveckling) är en inkrementell process: Man börjar litet och lägger till funktionalitet undan för undan. Därför är tiden en integral del av modellen - vartefter som tjänsten växer delas den in i fler moduler med snävare ansvarsområden.

Låt oss därför gå igenom en webbtjänsts anatomi i kronologisk ordning.

Main

Det är här man börjar, i dubbel bemärkelse. Det är den första modul man skapar och dess primära ansvarsområde är att sköta uppstart av webbtjänsten, vilket bland annat innefattar att binda samman tjänstens olika delar med dependency injection. I Main lägger man även global felhantering, autentiseringslogik och liknande aspekter som inte berör tjänstens primära ansvarsområde. Så mycket som möjligt av den generella webbtjänstlogiken paketeras med fördel i externa moduler som kan återanvändas av andra tjänster i lösningen, t ex med Nuget.

Main.Test

Det rekommenderas starkt att man driver fram, eller åtminstone täcker, den funktionalitet man utvecklar med tester. Det gäller även uppstarts-logik. Till exempel är det en god idé att testa att varje controller-klass kan bindas till de service-klasser den använder vid uppstart. Varje annan modul bör på samma sätt åtföljas av motsvarande test-modul, med ".Test" som prefix till namnet.

Story

Antag att vi nu har skapat en fullt funktionell webbtjänst som är sjösatt i en testmiljö och integrerad med de kringtjänster som den behöver för till exempel loggning och versionshantering. Kanske har den en Ping-metod som visar att vi kan anropa tjänsten och få ett svar tillbaka. Innan vi faktiskt börjar implementera användarfall så skapar vi en ny modul (samt motsvarande testprojekt) som är helt oberoende av den infrastruktur som uppstartslogiken kräver. Vi kallar den Story, för det är här våra user stories ska implementeras.

I vårt fall (kom ihåg att vi bygger en REST-tjänst med ASP.NET MVC) består Story-modulen av ett antal service-klasser och tillhörande service-interface som vi kan binda till våra controller-klasser (som fortfarande ligger i Main-projektet).

Nu ser vår tjänstarkitektur ut såhär:

Main > Story

Mission

Efter hand som Story implementerar allt fler användarfall uppstår behovet av interna service-klasser och interna modeller, utöver de som används av våra controllers och som kommuniceras med användaren. Då är det dags att tydliggöra vad som är tjänstens mission: Mer konkret, vilka uppgifter utför den och vilken data tar den emot respektive levererar till sina konsumenter. Till en början kan Mission vara en mapp under Story med alla service-interface och modeller som används av controllers i Main. Men ännu tydligare blir uppdelningen om man lägger dem i en egen modul, som inte behöver vara beroende av något annat i Story.

Vår tjänstarkitektur blir då:

Main > Story > Mission

(Main är även beroende av Mission eftersom den sköter dependency injection mellan Story och Mission)

Front

För att renodla separationen mellan Story och Mission även för kompilatorn, så är det nu dags att bryta ut det som inte är uppstart-logik från Main, det vill säga de controller-klasser som tar emot anrop från tjänstens konsumenter och delegerar till de service-interface som finns i Mission. Controller-klasserna placeras i Front, tillsammans med eventuella attribut och input-modeller som har beroenden till webbramverket (t ex för valideringslogik). Front är alltså, till skillnad från Story, beroende av vilken infrastruktur som används för att kommunicera med användaren. Därför är det en god idé att flytta så mycket logik som möjligt från Front till Story. Då blir det enklare att i framtiden välja en annan webbteknik, eller något annat än webben som överföringsmekanism. Man slipper även dessa beroenden i sina användarfall-tester.

Nu ser vår tjänstarkitektur ut såhär:

Front > Mission < Story

(vi utelämnar Main, som då den sköter dependency injection har beroenden till alla övriga moduler)

Back

De allra flesta webbtjänster behöver anropa andra tjänster i sin tur för att utföra delar av sitt uppdrag, eller har andra beroende som man kan vilja kapsla in (t ex en databas, filsystemet eller systemklockan). I enlighet med dependency inversion-principen i S.O.L.I.D vill man abstrahera bort dessa detaljer från den affärslogik som implementeras av Story. Det uppnår vi genom att lägga dem i en eller flera Back-moduler som beror på Story, eller mer specifikt implementerar klient-interfaces i Story.

Vi får då följande struktur:

Front > Mission < Story << Back

Domain

Nu till den sista av våra tre huvudmoduler. Efterhand som tjänsten växer och tar på sig större ansvar med allt fler användarfall finns risk att Story börjar likna en monolit: Dess ansvarsområde blir för stort och därmed otydligt, givet den detaljrikedom som krävs för att implementera alla användarfall. Det är också möjligt att man än så länge saknar en tydlig modell över den domän man rör sig inom och att tjänsten därför börjar kännas fragmenterad: Man ser inte skogen för alla träd.

Lösningen är att separera logik som berör användarfallen från logik som mer handlar om hur domänen fungerar, oberoende av vad användaren vill utföra inom den. Vi får då en eller flera Domain-moduler som innehåller affärsregler och begrepp inom domänen och kan hålla Story-modulen på en högre abstraktionsnivå som koncentrerar sig kring användarfallen.

En annan fördel med att dela in tjänsten i flera domain-moduler är att varje back-modul då inte behöver känna till alla detaljer om hela domän-kontexten för att uppfylla det ansvar som definieras av dess klient-interface. Den behöver bara ha kännedom om den specifika domain-modul som delegerar till den.

Vi får då en hierarkiskt struktur med en eller flera domain-moduler som var och en delegerar till en eller flera back-moduler, enligt följande:

Front > Mission < Story << Domain << Back

Utility

Den sjunde och sista modulen uppstår ofta under utvecklingens gång, ur behovet att slippa upprepa algoritmer som inte finns tillgängliga i det underliggande ramverket (.Net Core i vårt exempelfall). Dessa samlas lämpligen i ett projekt, Utility, som alla övriga moduler kan referera vid behov. Det är viktigt att Utility är fri från beroenden som kan tynga ner de moduler som refererar till den. Hur benhård man vill vara är en avvägning, men tumregeln är att om en funktion i Utility har ett beroende som inte alla andra moduler redan har, bör den snarare kapslas in i en proxy och tillgängliggöras via dependency inversion för lämplig domän och/eller Story.

Mission-Story-Domain-modellen

Vi har nu steg för steg arbetat oss fram till den fullständiga modellen:

Main
v
Front > Mission < Story << Domain << Back
v
Utility

Ett annat sätt att förstå modellen är att se att var och en av dess delar besvarar en specifik fråga om tjänsten:

  • Front: Vem är jag?
    Svar, t ex: En .Net Core web service med en mission

  • Mission: Varför finns jag?
    Svar, t ex: För att utföra dessa uppgifter åt mina konsumenter

  • Story: Vad gör jag?
    Svar, t ex: Implementerar dessa användarfall

  • Domain: Hur gör jag det?
    Svar, t ex: Genom att modellera domänen i kontexten av min berättelse (story)

  • Back: Var finns mina resurser?
    Svar, t ex: Hos dessa externa tjänster

  • Main: Vilka delar har jag jag och hur hänger de ihop?
    Svaret ges av valfritt dependency injection-ramverk, tillsammans med behörighetskontroll, felhantering och loggning

  • Utility: Kan jag utföra den här operationen?
    Svaret bör vara jakande, och finns inte stödet i det underliggande ramverket bör en utility-metod utvecklas

När man har kommit så långt att man brutit ner tjänsten i alla sju modul-typer, varav flera domain- och back-moduler, samt tillhörande testklasser, så kan det vara en bra idé att dela in modulerna i tre grupper (solution folders om man använder Visual Studio):

  • Application, som innehåller Main, Front, Mission, Story och Utility (och tillhörande test-projekt)

  • Domain, som innehåller domain-modulerna, samt

  • Back, som innehåller back-modulerna

När även denna struktur börjar kännas för trång är det hög tid att dela upp sin tjänst i flera, lämpligtvis genom att bryta ut någon av de domän-moduler den innehåller till en egen tjänst.

Hoppas ni kan använda den här modellen framgångsrikt i ert projekt. Kommentera gärna nedan om den löste något specifikt problem i projektet, eller om ni var tvungna att frångå den för att uppfylla något särskilt behov.