Deklarativ forretningslogikk med LINQ

Deklarativ forretningslogikk med LINQ

I IT-verdenen er det en konstant streben etter å skrive kode som er lettere å forstå. Både bransjen og akademia har jobbet hardt i flere tiår for å lage verktøy, språk og programmeringsparadigmer som tillater dette. Det har medført at vi nå sitter igjen med enorme fordeler slik som kompilatoren, objektorienterte programmeringsspråk, virtuelle miljøer, omfattende rammeverk og mye mer. Men likevel kan vi alltid gjøre det litt bedre.

Deklarativ programmering beskrives ofte som at programmereren beskriver hva man vil ha utført, i stedet for hvordan det skal utføres. Dette høres jo flott ut, men hvordan gjør man det i praksis? Jo, du skriver systemet ditt i et mystisk språk som kunden aldri har hørt om og som har masse begrensninger som du ikke er vant med, der du for eksempel ikke får lov til å endre på innholdet i variabler og nesten alt må gjøres med rekursjon. På grunn av dette oppleves det deklarative programmeringsparadigmet ofte som litt sært og kanskje hovedsakelig akademisk. Men hva hvis vi kunne programmere på denne måten der det passer oss?

Les også: Bør du brukerteste under akseptansetesten?

Den imperative standarden

I bransjen er det som regel objektorienterte språk som gjelder. Det er lettere å oversette kundens domene til en objektorientert arkitektur, og utviklere har en felles grunnidé om hvordan kode skal skrives. I den objekterienterte modellen blir koden intuitiv, spesielt på høyt nivå. Men når vi graver oss lenger ned i detaljene i koden begynner tradisjonell objektorientert programmering å ligne mer på det gamle imperative paradigmet vi så i for eksempel C.

Da jeg var i fastlegeprosjektet hos Helsedirektoratet var (løsningen på) denne problemstillingen sentral. For eksempel finnes det en klasse, Helsepersonell, som beskriver alt fra sykepleiere til veterinærer og fastleger. Helsepersonell inneholder en samling med autorisasjoner som avgjør hvem av disse han eller hun er nå og eventuelt har vært i fortiden. En fastlege, Dr. Ola , har for eksempel tre autorisasjoner, én fra da han var sykepleier på 90-tallet, én fra da han var juniorlege noen år og en nåværende som autoriserer ham som fastlege. La oss si at kunden ønsker en metode for å avgjøre om et helsepersonell er autorisert som fastlege eller ikke:

Uansett hvilket programmeringsparadigme vi bruker for å løse dette, vil selve prosessoren avgjøre dette omtrent slik: Sjekk om den første autorisasjonen er en aktiv fastlegeautorisasjon, sjekk om den andre autorisasjonen er en aktiv fastlege-autorisasjon, sjekk om den tredje er… og så videre. Dette er en imperativ beskrivelse av logikken. Det vil si at fokuset ligger mer på hvordan logikken skal utføres, og mindre på hva logikken er. Heldigvis kan vi implementere dette med konstrukter som foreach som gjør dette mer lesbart:

Kodesnutt 1

Kodesnutt 1

Når man forsøker å forklare denne kodesnutten til noen som ikke kan programmere, innser man fort at dette også er svært imperativ kode. Det er vanskelig å forklare koden uten å også måtte forklare hvordan prosessoren må utføre den. Forsøk for eksempel å argumentere for en utenforstående hvordan det faktum at «return false» ligger utenfor foreachen henger sammen med forretningslogikken. Foreachen forenkler ting, men vi har fortsatt i høy grad beskrevet hvordan logikken skal utføres, i stedet for hva vi ønsker at logikken skal være, og på den måten innført støy i forretningslogikken. På høyere nivå blir det dog penere. De fleste vil kunne resonere seg fram til hva utrykket «helsespersonell.ErFastlege()» betyr. På denne måten kan vi si at tradisjonell objektorientert programmering kan være deklarativt på høyt nivå, men blir ofte imperativt på lavt nivå.

Les også: Slik gir god kravspesifisering deg løsningen du vil ha

Deklarativ programmering inn i varmen

De siste årene har vi sett at konsepter fra deklarativ programmering har begynt å snike seg inn i «mainstream» objektorientert programmering. C#/.NET har vært tidlig ute, og Java har i de siste versjonene fulgt det gode eksemplet. Det er spesielt konseptet av «høyere ordens programmering» som har begynt å gjøre objektorientert kode mer deklarativ. Navnet er lånt fra kalkulus, der en høyere ordens funksjon er definert som en funksjon som tar inn en annen funksjon som argument og/eller returnerer en funksjon som resultat. Dette baserer seg på et enkelt og elegant konsept om at program og data er to sider av samme sak. På samme måte som at vi kan deklarere en variabel til å inneholde «normal data», for eksempel et tall, kan vi også deklarere en variabel til å inneholde et «program», altså en funksjon eller metode.

Dette konseptet kan vi utnytte til å abstrahere ut all «hvordan»-logikken i koden over, slik at den består nesten utelukkende av «hva»-logikk. For å avgjøre hvilken del av logikken som er «hva»-logikk, kan vi prøve å tenke hvordan kunden ville beskrevet den, for eksempel «En fastlege er et helsepersonell med minst én autorisasjon som har autorisasjonstype Fastlege med TilDato høyere enn dagens dato». Alt i metoden som ikke direkte beskriver dette er «hvordan»-logikk. La oss prøve å implementere dette med kun «hva»-logikk:

Kodesnutt 2
Kodesnutt 2

Her har vi skrelt bort mesteparten av logikken som instruerer prosessoren i hvordan dette skal avgjøres. Det er ingenting i koden som direkte beskriver at vi skal se på én og én autorisasjon og erklære suksess når og hvis vi finner riktig autorisasjon. I stedet har vi antatt at det finnes en metode Any som returnerer hvorvidt det finnes minst ett element i lista som tilfredsstiller kondisjonen på linje 7. Med moderne objektorientert programmering som støtter høyere ordens programmering, er denne koden helt gyldig. Vi kan forstå hvordan og hvorfor den virker ved å se på to konsepter den støtter seg på: lambda og extensions.

Lambda

Et naturlig første steg når man tar i bruk høyere ordens programmering er å gi metoder som argumenter til andre metoder. Det vil si at vi definerer en metode som normalt og gir navnet på denne som argument når vi kaller på en annen metode. Problemet med dette er at metoder vi gir som argument har en tendens til å være veldig enkle og kun gi mening i ett spesifikt tilfelle. Det kan fort bli mange slike engangsmetoder som kan skape en del støy i koden. For å unngå dette kan vi bruke et lambda-utrykk for å definere en funksjon «ad-hoc» uten å trenge å ha den inni en klasse. På samme måte som vi kan deklarere en variabel til å inneholde et konkret tall

int x = 1;

kan vi deklarere en variabel til å inneholde konkret funksjonalitet:

Func<int> printTall = argument => print("Dette er tallet: " + argument);

Her har vi altså brukt en lambda til å definere en funksjon, printTall, som tar inn én variabel (tall) og printer denne variabelen. Syntaksen er «x => y» og leses «x into y». Venstresiden av pila er argument(ene) og høyresiden er koden som lambdaen skal bestå av. I den siste implementasjonen vår av ErFastlege (kodesnutt 2) er en lambda brukt som input til Any-metoden, og lambdaen tar inn et helsepersonell og returnerer en bool som angir om han er autorisert fastlege eller ikke. Merk at vi kaller Any() på listen av autorisasjoner. Hvis vi antar at det ikke finnes noe Any-metode i hverken List eller IEnumerable, kunne vi likevel skrevet koden på denne måten?

Extensions

Ja, det kan vi faktisk. I hvert fall i C#. Med extensions har vi mulighet til å skrive våre egne metoder som om de var metoder i en annen klasse. Dette gjør at vi kan utvide for eksempel systembibliotek med våre egne metoder. La oss forsøke å extende List-objektet med metoden «Any»:

Kodesnutt 3

Kodesnutt 3

Forskjellen mellom en extension-metode og en vanlig metode er at det første argumentet er prefixet med nøkkelordet «this». Klassen til dette første argumentet blir da klassen som utvides. En extension-metode må også være statisk og deklareres inni en statisk klasse.

Dersom vi ser tilbake på kodesnutt 1, 2 og 3, ser vi at kodesnutt 2 og 3 nå utfører den samme logikken som kodesnutt 1. Forskjellen er at vi har separert ut all «hvordan»-logikken ut til en egen metode, slik at vi kun trenger å forholde oss til «hva»-logikken. Dersom vi skriver om Any til å bruke den generiske typen T i stedet for Helsepersonell, og IEnumerable-interfacet i stedet for List, blir den såpass generisk at den kan gjemmes vekk som plumbing og gjenbrukes så mye vi vil. Dette var et eksempel på hva vi kan gjøre med kombinasjonen av lambda og extension, men faktisk er denne generiske Any-metoden allerede tilgjengelig i et omfattende .NET-bibliotek som heter LINQ.

LINQ

LINQ (Language Integrated Query) er et bibliotek av extension-metoder som kan brukes til å gjøre deklarative spørringer på forskjellige datastrukturer. En av fordelene ved LINQ er at du kan bruke det til å skrelle bort svært mye «hvordan»-kode fra forretningslogikken din. La oss se på noen av de mest brukte LINQ-metodene.

Where

Si at du har en liste med objekter, f.eks. List<Helsepersonell>, og du ønsker å returnere en ny liste med alle legene i denne lista som er over 50 år. Den imperative måten å gjøre dette på kunne vært å opprette en ny tom liste, iterere over alle de opprinnelige helsepersonellene og legge til alle som er over 50 i den nye lista:

Kodesnutt 4

Her har vi samme problemet som over. Vi har blandet sammen forretningslogikken (hva) med detaljer om hvordan datamaskinen må løse problemet. Her kan vi i stedet bruke LINQ sin «Where»-metode og kun sitte igjen med forretningslogikken:
var overFemti = helsepersonell.Where(hp => hp.Alder > 50);

Her har vi tatt kondisjonen i if-løkken og skrevet den om til en lambda. Denne lambdaen gir vi som argument til Where-metoden, og Where-metoden bruker dette til å produsere en ny liste med alle helsepersonell som tilfredsstiller kondisjonen.

Select

Select løser et annet velkjent problem som ofte oppstår når man har komplekse datatyper. La oss si at kunden ønsker en ny metode som returnerer en liste med navnene på alle i en liste med helsepersonell. Av samme grunner som over ønsker vi ikke å lage en ny tom liste med strings og legge til navn på den i en foreach-løkke. I stedet bruker vi Select:
var alleNavn = helsepersonell.Select(hp => hp.Navn);

Gitt at Helsepersonell-objektet inneholder en property Navn, vil dette produsere en liste med navn på alle i helsepersonell-lista. Select tar in en lambda, som i dette tilfellet tar inn et Helsepersonell og returnerer en string. Select itererer over hele lista og kaller lambdaen på hvert element. Returverdiene fra lambdaene legges på en ny liste som tilslutt returneres.

SelectMany

Ved enda mer komplekse datastrukturer kan SelectMany brukes til å flate ut strukturer. La oss f.eks. si at vi har et objekt FastlegeAvtale, og hver FastlegeAvtale har en liste med innbyggere som er knyttet til denne. Gitt en liste med FastlegeAvtaler, ønsker vi å returnere en sammenslått liste med alle innbyggere som er på en av disse fastlegeavtalene:

var innbyggere = fastlegeAvtaler.SelectMany(a => a.Innbyggere);

Komplekse spørringer

Ved første øyekast kan kanskje LINQ se litt unødvendig ut, men selv i de enkleste spørringene fjerner vi unødvendig implementasjonslogikk fra forretningslogikken. Det er dog i de komplekse spørringene LINQ virkelig redder dagen. Det er få grenser på hvordan disse metodene kan kombineres, og med god bruk av LINQ kan komplekse utrykk bli eksponentielt mer lesbare. For å øke lesbarheten enda mer går det også an å bruke language extensions som integrerer metodene inn i selve språket. Select-eksemplet over kan da skrives på følgende måte:
var alleNavn = from hp in helsepersonell select hp.Navn;

Language extensions er som regel mest praktisk i mer komplekse spørringer, så for å demonstrere dette kan vi utvide eksempelet i SelectMany. Vi ønsker nå å returnere en liste med navnet på alle innbyggere som bor i Oslo, og som har en fastlege som er over 50 år og har etternavn som slutter på «sen».

Kodesnutt 5

Her har vi brukt nøkkelordet «let» for å deklarere variabelen «lege» inni spørringen. På denne måten slipper vi å gjenta oss når vi gjør spørringer på legens alder og navn. Vi har også nøstet spørringen for å oppnå effekten av SelectMany: Vi gjør en ny spørring på innbygger inni spørringen på fastlegeavtaler, og får dermed en sammenslått liste som i SelectMany-spørringen.

Best-effort deklarativ programmering

Det som er verdt å ta med seg her er mulighetene som høyere ordens programmering innfører i språket. Vi slipper å skrive om hele systemet til Haskell, men kan programmere deklarativt når det passer oss. På denne måten kan vi abstrahere ut detaljene om hvordan forretningslogikken skal oppnås, men likevel beholde fordelene med en objektorientert arkitektur og imperativ programmering. LINQ er et kraftig bibliotek som hjelper deg å utnytte dette og skrive kode som ligner mest mulig på domenet.

Erik Lothe er .NET-utvikler i Core-avdelingen i Sopra Steria. Erik er spesielt interessert i backend og forretningslogikk, og har bl.a. vært med å utvikle API for Fastlegeordningen.

En kommentar om “Deklarativ forretningslogikk med LINQ

  1. Det var en riktig god artikkel om deklarativ programmering. Pluss for enkelhet og klarhet 🙂

Legg inn en kommentar