Debug-venlig kode

Når man har arbejdet med at kode igennem nogle år, har man en tendens til at udvikle en slags kode-filosofi, som i høj grad er formet af de projekter man har været involveret i og de roller man har haft i sin karriere.

I starten af min karriere var jeg ofte involveret i projekter efter de var løbet af sporet i en eller anden forstand, og skulle hjælpe til med at rette op på dem. Nogle gange var det sammen med de personer som var involveret i udviklingen inden jeg kom på projektet, andre gange var det som en del af et nyt team som overtog projektet.

Uanset hvordan det så ud organisatorisk, bestod en stor del af mit initiale arbejde i at sætte mig ind i koden, og finde ud af hvad der udestod/var problemer med.

Ofte var koden præget af at udviklerne havde været under pres i noget tid, således der ikke havde været afsat tid til nødvendig refaktorering og nedbringelse af teknisk gæld. Der var også ofte en pukkel af fejl i koden, som var blevet skubbet foran udviklingen (en vane jeg er stærk modstander af), som skulle løses før man kunne komme i gang med at løse de egentlige problemer med koden.

Jeg har tidligere refereret til udviklingsformen på denne type projekter som error-driven software development, og den er kendetegnet af at være reaktivt snarere end proaktivt.

Når man tager alt dette i betragtning, er det måske ikke overraskende at jeg har en kode-filosofi som jeg omtaler som ”debug venlig kode”. Det består af en række tiltag som jeg har oparbejdet igennem årene, som gør det nemmere for mig selv, og andre, at debugge koden, hvis man skal lede efter problemer.

Nedenfor følger en beskrivelse af mine tiltag, som jeg håber kan være til hjælp fra andre. De tager i nogen grad udgangspunkt i at man arbejder med eksisterende kode, og ikke er i gang med at starte helt fra bunden, men selv hvis man er i et greenfield projekt, mener jeg stadigvæk at de kan bruges.

Ok, nok baggrundssnak, lad os kigge på hvad man kan gøre.

Begræns scope på metoder
Alt for ofte oplever man metoder på hundredevis af linjer og med stribevis af ansvarsområder. Disse danner en barriere som er svær at overkomme når man skal danne sig et overblik over koden, særligt når man skal løse et problem.

I stedet bør man afgrænse metoder til kun at indeholde en funktionalitet. Dvs. at en metode til hentning af data fra en database ikke også bør smide den i en cache og konverterer den til et andet dataobjekt. Vælg i stedet at lave en metode til hver af disse funktionaliteter, som så kan kaldes når de skal benyttes.

Generaliser metoder
Dette burde ikke længere være nødvendigt at sige, men det er det desværre.

Hvis man har implementeret en metode som understøtter en given funktionalitet, og bagefter skal bruge den samme type funktionalitet i en anden sammenhæng, bør man se om man ikke kan generalisere metoden, således man kan nøjes med at bruge den et sted.

Det giver mindre kode at vedligeholde, og det minimerer antallet af steder man skal kigge efter en given fejl.

Sigende navngivning
Metoder, klasser, parametre mv. bør have sigende navne, således man ikke er i tvivl om hvad man arbejder med.

Hvis man har en Person klasse med et CPR nummer i, så kald den pågældende variabel CPR nummer, og ikke ID nummer. Hvis man på den anden side skal håndtere forskellige typer af ID numre, så lad være med at kalde variablen CPR nummer, selv om det er det mest udbredte ID nummer man støder på i Danmark.

Som et slags supplement til dette, så bør man også være enige om en fælles navngivningstandard på tværs af systemer som benytter hinanden. Dette gælder især domæne specifikke begreber, hvad enten de er på dansk eller engelsk.

Hvad man ikke skal gøre er at overdrive. Hvis det er tydeligt fra kontekst hvad en variabel gør, så lad vær med at komme med lange sigende navne, men hold det kort og præcist.

Brug lokale variable
Dette er vist et punkt Martin Fowler vil være uenig i, men det er nok sjældent han har debugget andre folks kode.

Hvis man indsætter et metodekald som parameter i en anden metode, f.eks. methodX(methodY(Z)), kan man ikke umiddelbart se hvilken en metode det er som giver en fejl, uden at gå ind i hver af dem. Hvis man i stedet først kalder den ene metode, og derefter den anden, vil man ved normal debugging kunne se om det er den ene metode eller den anden som giver problemer, uden man skal bruge unødvendig tid.

Så, i stedet for ovennævnte eksempel, gør følgende
Var A = methodY(Z)
methodX(A)

Kod defensivt
Dette er nok en af de minde oplagte punkter, men det er faktisk en vigtig del af debug venlig kode.

Defensiv kode er kode hvor man forventer at koden vil blive brugt forkert, f.eks. ved at den får forkert input data, og koder derefter, bl.a. ved at verificere at input data er korrekt, og smide sigende fejlbeskeder når de ikke er det.

Ved at man verificerer input data, viser man hvad man forventer, især i forhold til null-værdier, og det er en enorm hjælp for andre som senere skal fejlsøge.

Ensartet adfærd i koden
Jeg arbejdede engang på et projekt hvor der var en ToString() metode i noget central kode, som altid påsatte noget tekst for enden af tekststrengen som den dannede ud fra objektet den var tilknyttet. Eller rettere, den påførte variablen som teksten blev dannet ud fra, en tekststreng for enden.

Det vil sige at resultatet af metode kaldet varierede alt efter hvilken gang metoden var kaldt.

Dette er problematisk af mange grunde, bl.a. det var en uventet bi-effekt for dem som kaldte metoden, da det ikke er en del af den generelle adfærd for ToString() metoder, men det gjorde det også svært at fejlsøge, da fejlen var afhængig af om den pågældende metode var blevet kaldt før på det pågældende objekt.

Lad være med at bryd gængse normer for en given metode, i dette tilfælde ToString(), og lad være med at lave metoder som giver forskelligt resultat, selv når anvenderen tror der benyttes samme parameter.

Test
Dette burde ikke være nødvendigt at skrive, men unittest og integrationstest er vigtige når man skal debugge og rette fejl.

Hvis der er gode test, er det muligt at foretage rettelser med en forvisning om at der ikke lige pludseligt kommer uventede bivirkninger, som ødelægger andre dele af koden.
Såfremt der ikke er test i forvejen, bør man implementere dem i forbindelse med man foretager rettelser til koden. De kan være et vigtigt værktøj til debugging, og de er, som sagt, et godt værktøj til at evaluere bivirkninger af koderettelser og refaktoreringer.

Kommenter din kode
Der er mange som mener at kodekommentarer er spild af tid (se f.eks. her), men ikke jeg. Jeg er en varm fortaler for meningsfulde kodekommentater.

Det vigtige i denne sammenhæng er ordet ”meningsfulde”.

Der er ikke noget mere ligegyldigt og forstyrrende end kodekommentarer som forklarer hvad koden gør. Det skulle da lige være kodekommentarer som forklarer hvem der har skrevet/rettet koden. Det første kan vi se i koden, det andet kan vi se i vores source control.

Den slags kommentarer gider jeg med andre ord ikke kigge på.

Hvad jeg i stedet vil se, er kode kommentarer som forklarer hvad præmisserne for noget kode har været. Hvorfor er der blevet truffet de valg som der er blevet truffet. Det kan være evt. work-arounds som er nødvendige pga. begrænsninger i andre systemer, et hurtig oprids af hvad en given funktion skal gøre, rent forretningsmæssigt (evt. med referencer til hvor man kan læse mere), eller basale antagelser (”vi forventer at folk benytter decimal systemet og ikke imperial units”)

Løbende, nødvendig refaktorering
Som jeg skrev i starten er der ofte en del teknisk gæld på projekter som er løbet i problemer, men selv i velfungerende projekter vil der være områder af koden som kan trænge til refaktorering. Når man render ind i sådanne kode, giver det god mening at refaktorere den når man er inde og løse fejl og lignende. Ikke blot giver det god mening, men det kan være nødvendigt for at gennemskue hvor fejlen optræder.

Hvis man er i sådan en situation, skal man naturligvis refaktorere.

Pas dog på at man ikke bare begynder at refaktorere unødvendigt. Alt kode kan forbedres, men man skal også sørge for at prioritere opgaverne så de giver mest værdi.

Og husk lige at indsætte test før du refaktorerer, så du er sikker på koden fungere som den skal bagefter.

1 comment for “Debug-venlig kode

  1. Jeg er enig langt hen ad vejen, ikke kun fordi det giver kode, der er nemmere at debugge, men fordi det giver kode, der sjældnere skal debugges: Sådan noget som sideeffekter i ToString burde jo aldrig være tilladt. Generelt er et skift fra imperativ stil (“program = fremgangsmåde til ændring af tilstande”) til funktionel stil (“program = beregning af udtryk”) en rigtig nyttig indgangsvinkel, som heldigvis også vinder indpas, også i de klassisk imperative sprog. Const/final burde være default for formelle parametre og lokale variable IMHO, men det er en del af en længere diskussion.

    På ét område er jeg dog uenig: Introduktion af lokale variable. Det er selvfølgelig ærgerligt at debuggeren ikke kan steppe et udtryk ad gangen (“umuligt” i JVM/JDI, kender ikke CLR på dette område), men i Eclipse og sikkert også andre IDE’er kan man faktisk hyperlink-debugge sig ind i methodX i dit eksempel (i Eclipse ved at trykke Ctrl+museklik på methodX), hvorved man kan se hvad methodY gav af resultat (da det jo er havnet som parameter i methodX). Derved slipper man for at forurene sit scope (= sin hjerne) med endnu et navn, som kun bruges én gang.

    Af samme grund ville jeg (afhængig af længden/kompleksiteten udtrykket af “methodY(Z)”) somme tider overveje at bruge udtrykket mere end én gang, hvis det gjorde koden tydeligere. Et eksempel:


    if (movementVector.length() < 0) {
      throw new IllegalArgumentException("Can't work on negative vectors");
    }
    return movementVector.length() / speed;

    Det er helt sikkert et spørgsmål om stil, men her synes jeg det er mere tydeligt hvad der returneres og checkes for end hvis man skulle indføre en lokal variabel først.

    En undtagelse til dette ville dog være en variabel som har taget lidt ekstra besvær at udregne (her i Scala):

    class Person (val friends: Set[Person], val age:Int)

    def isInTargetGroup1(person: Person, threshold: Int): Boolean = {
      person.friends.flatMap(_.friends).toSet.filter(_.age < 20).size > threshold
    }

    def isInTargetGroup2(person: Person, threshold: Int): Boolean = {
      val friendsOfFriends = person.friends.flatMap(_.friends).toSet
      friendsOfFriends.filter(_.age < 20).size > threshold
    }

    Begge er OK i min bog, selv hvis friendsOfFriends i variant 2 er på kanten af dit sidste råd i afsnittet om sigende navngivning.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *