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 beteenden uppdelade i klasser och tillhörande interface som vi kan binda till våra controller-klasser (som fortfarande ligger i Main-projektet). Beroende på om man använder command-query pattern eller service pattern brukar dessa klasser/interface döpas till NågontingCommandHandler och NågontingQueryHandler eller NågontingService. Låt oss fortsättningsvis anta att vi använder service pattern (mest för att det är enklare att skriva.) 

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 hjälp-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 för konsumenten och vilka datamodeller tar den emot och returnerar. 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 överföringsmekanism (som inte behöver vara webbaserad). Man slipper även dessa beroenden i sina användarfall-tester.

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

Main > (Front > Mission < Story)

Proxy

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 Proxy-moduler som beror på Story, eller mer specifikt implementerar klient-interfaces i Story.

Vi får då följande struktur:

Main > (Front > Mission < Story << Proxy)

Dubbelpil, <<, indikerar att det kan finnas fler än en proxy-modul.

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 (men utan att gå för långt i generalisering - kom ihåg att tjänstens enda syfte är att uppfylla sin mission, och all onödig generalisering som inte uppfyller det syftet bör kastas överbord). Vi får då en eller flera Domain-moduler som innehåller affärsregler och begrepp inom domänen. Därmed kan Story-modulen hållas 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 proxy-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:

Main > (Front > Mission < Story << Domain << Proxy)

Utility

Till slut vill jag avråda från att använda ett behändigt anti-pattern.  Under utvecklingens gång uppstår ofta behovet avgenerella algoritmer som inte finns tillgängliga i det underliggande ramverket (.Net Core i vårt exempelfall). Typiska exempel är extension-metoder till string, Datetime, IEnumerable eller andra vanliga typer som man vill tillföra extra funktionalitet. Att samla dessa i egna klasser så att de kan återanvändas är bra, men om man faller för frestelsen att samla dem alla i ett gemensamt projekt och kalla det för något så generiskt som "Utility" kommer det projektet tendera att bli en slaskhink för all funktionalitet som inte passar någon annanstans.

Ett bättre sätt att hantera dessa klasser är att paketera dem som nuget-paket, inom väl definierade och avgränsade domäner, som då även kan delas mellan alla tjänster, inte bara den man tillfälligtvis jobbar med. Om funktionen inte är relevant för andra tjänster bör den istället kunna passa in i någon av de ovan beskrivna modulerna.

Mission-Story-Domain-modellen

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

Main > (Front > Mission < Story << Domain << Proxy)

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)

  • Proxy: 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

När man har kommit så långt att man brutit ner tjänsten i alla sex modul-typer, varav flera domain- och proxy-moduler, samt tillhörande testmoduler, 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 och Story

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

  • Proxy, som innehåller proxy-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.

Kommentera

Publiceras ej