DRY handler om mere end blot copy/paste

For nogle år siden havde vi en lille, intern arbejdsgruppe, der havde til formål at komme med forslag til, hvordan vi kunne forbedre den måde, vi skriver kode på. Som oplæg læste vi blandt andet The Pragmatic Programmer, der er en fantastisk bog spækket med fornuftige tanker og anbefalinger. Har du ikke læst den, vil jeg opfordre dig til at sætte den højt på din to do-liste.

Et af de mange gode råd, der omtales i bogen, er DRY-princippet. DRY er en forkortelse for Don’t Repeat Yourself, og det går ud på, at vi ikke skal gentage os selv, når vi skriver kode. Ikke overraskende var det et af de råd, alle kunne tilslutte sig, for uanset programmeringssprog synes de fleste udviklere at være enige om, at copy/paste-kode er en rigtig dårlig ide. Strengt taget omhandler DRY dog mere end blot kodegentagelser. Ideen bag DRY er at undgå gentagelser i enhver form, men lad os holde os til kodegentagelser i denne omgang.

Desværre er problemet omkring copy/paste-kode ikke så interessant. Vi kan blive enige om, at det er en dårlig ide, og vi behøver sjældent at debattere hvorfor, det forholder sig sådan.

Den virkelige udfordring består i at identificere alle de steder, gentagelser optræder i mere subtile forklædninger. Lad os se på et forsimplet eksempel. Lad os forestille os, at vi har en klasse, Logger, som vi kan udskrive diverse driftsrelaterede beskeder til. Logger har altså en LogMessage(), der tager en string, så almindeligvis vil den blive kaldt som følger:

logger.LogMessage("Here is a message");

Det er der næppe noget odiøst i. Andre gange er beskeden dog mere omfattende, som i dette eksempel:

logger.LogMessage(string.Format("Current status {0}", GetStatus()));

I dette tilfælde har vi et yderligere metodekald knyttet til selve logging-operationen, og i princippet kunne der være et vilkårligt antal yderligere kald. Vi ved ikke, hvor lang tid det tager at udføre GetStatus() (samt eventuelle andre kald), men hvis det er en langvarig affære, er det ikke noget, vi er interesseret i at gøre, i fald logging er slået fra. Det spidsfindige er, at vi med den nuværende konstruktion kommer til at betale prisen for kaldet til GetStatus() uanset hvad, der end sker i LogMessage().

For at komme dette til livs, kan vi jo bare undersøge, om logging er slået til, inden vi laver noget. Så kunne det f.eks. se ud som følger.

if (logger.IsLoggingEnabled) {
   logger.LogMessage(string.Format("Current status {0}", GetStatus()));
}

Så undgår vi at kalde GetStatus() i de tilfælde, hvor det er overflødigt. Vi betaler godt nok en lille merpris som følge af vores tjek, så logging er blevet en anelse dyrere, men vi slipper for det overflødige arbejde.

Desværre har vi også netop introduceret en afhængighed, der gør, at vi kommer til at gentage os selv igen og igen. Vi har med andre ord brudt DRY-princippet!

Hver gang vi vil begrænse afviklingen af kode i henhold til det aktuelle niveau af logging, er vi nødt til at have denne IsLoggingEnabled / LogMessage()-konstruktion. Det er selvfølgelig kun to linjer, men det kan let blive til rigtig mange steder, og hvad sker, hvis kravene, til hvornår disse kald skal udføres, ændrer sig? Hvis vi er heldige, kan det lægges ind under vores IsLoggingEnabled abstraktion, men der er grænser for, hvor meget vi kan feje ind under det gulvtæppe. I værste fald må vi rundt og rette mange, mange steder, og det vil vi jo gerne undgå.

Problemet og en mulig løsning

Hvad er problemet? Vores API udstiller en afhængighed mellem to kald og tvinger således brugeren til at kæde disse sammen. Denne sammenkædning skal ske hver gang, vi ønsker at begrænse udskriften af log-beskeder og dermed får vi mange gentagelser. En sådan afhængighed bør håndteres af APIet og ikke af brugeren. Gør vi det, kan vi slippe af med gentagelserne.

Der er flere måder, at komme uden om dette på. Jeg vil dog nøjes med at se på en mulig løsning, for pointen er ikke at løse problemet i mit eksempel, men blot at illustrere problematikken.

For at begrænse de potentielt dyre kald, er vi nødt til at kunne slå afviklingen af disse fra, så vi har brug for en if-konstruktion, men da vi ikke ved, hvad det er, vi forsøger, at begrænse afviklingen af, er vi nødt til at indkapsle dette. Her kommer delegates os til undsætning. En delegate er blot en reference til et stykke kode, og denne reference kan vi naturligvis give som input til en metode. Med introduktionen af lambdaudtryk er det tilmed blevet meget nemt at gøre dette.

Vi kan altså implementere en LogMessage()-variant, som tager en delegate, der indkapsler alle de dyre kald, som input. Herefter kan vi lave det nødvendige tjek. Hvis og kun hvis logging er slået til, kan vi afvikle den kode, vores delegate peger på og logge den generede besked. Metoden kunne se ud som følger:

public void LogMessage(Func<string> func) {
   if (IsLoggingEnabled) {
      LogMessage(func());
   }
}

Vi kan f.eks. kalde LogMessage() med en delegate, der indkapsler det tunge arbejde således:

logger.LogMessage(() => string.Format("Current status {0}", GetStatus()))

På den måde skal den kaldende kode ikke længere tage stilling til, om logging er slået til eller ej, hvilket ikke alene gør koden simplere. Vi slipper også for gentagelserne. Hvis vi senere vil tilføje flere betingelser, har vi også et og kun et sted at gøre det.

Opgaven her var ikke at illustrere, hvordan et logging-API kan eller skal skrues sammen. Dertil er eksemplet for overfladisk, men forhåbentlig kan det illustrere, hvordan vi let kommer til at introducere gentagelser i koden, samt hvad vi kan gøre ved det.

Der er mange varianter af problemer i forhold til DRY, og dette indlæg har kun berørt en lille del. Hver gang vi bryder princippet, risikerer vi at gøre vores kode dyrere at vedligeholde, så derfor er det en god ide at komme gentagelserne til livs, inden de vokser sig store.

9 Responses to “DRY handler om mere end blot copy/paste”

  1. Mark Seemann says:

    Mere generelt kan man simpelt hen sige: Delegates are anonymous interfaces (http://blog.ploeh.dk/2009/05/28/DelegatesAreAnonymousInterfaces.aspx)

  2. jalf says:

    Smukt. :)

    Jeg lavede noget lignende for et år eller to siden i Javascript (så vidt jeg husker forsøgte jeg at fake en assert()-funktion, hvor evalueringen af argumentet også skulle kunne undgås)

    Anyway, det varmer mit hjerte at se folk rent faktisk benytte denne slags funktionsorienterede tricks i praksis. :)

  3. Hej Mark – tak for dit link.

    Jeg er ikke helt med på, hvad du mener, når du siger, at delegates ikke er OO. Hvorfor skulle metoder ikke kunne være instanser af en type? For mig at se er delegates i C# netop typestærke funktionspointere.

    Anyway, uanset hvad vi kalder dem, så er de særdeles anvendelige, og det ser vi ud til at være enige om :)

  4. Hej Jalf

    Jeg er ret vild med delegates og de muligheder de giver. I realiteten bruger vi dem jo ofte til compare-metoder etc. Jeg har i hvert fald brugt dem i mange år i Perl til netop den slags, og jeg er glad for, at de efterhånden er ret almindelige i C#, for de er meget brugbare.

  5. Udmærket indlæg, DRY bør nemlig altid være et punkt i code review på lige linje med YAGNI og Broken Windows :)

    Brian -> Compare? øhh?

  6. Lige netop med logging ønsker vi jo at normalscenariet (uden logging) påvirkes så lidt som muligt af muligheden for specialscenarier (med logging).

    Den foreslåede delegate-udgave kræver instantiering af et objekt plus metodekald i alle tilfælde (med/uden logging) – hvilket er et potentielt større performanceoverhead end if-tjekket.

    Et spændende bud på, hvordan vi både får kode, der kører hurtigt, og som ikke indeholder duplikeret kode, er PostSharper (http://www.postsharp.org).

  7. @Janus: Det er begge vigtige principper, men personligt synes jeg, at DRY har større indflydelse på min kode, og derfor koncentrerede jeg indlægget om det princip.

    Mht Compare er jeg helt med. Det var ikke for at sige, at den slags ikke findes i C#, men blot at min første erfaring med den brug af delegates stammer fra Perl.

  8. @Frederik: Jeg skriver specifikt, at mit indlæg ikke handler om logging, men om hvordan man kan bruge delegates til at undgå gentagelser. Derfor er det egentlig ikke interessant, hvorvidt mit eksempel er den optimale måde at logge på eller ej. Dertil er det alt for simpelt.

  9. Christian says:

    DRY – er altid klar til at blive glemt. Desværre også af erfarne udviklere…

    I går så jeg noget kode, hvor en fil skulle gemmes. Udvikleren havde fået en fejlrapport med at der blev smidt en IO exception. Han undersøgte problemet – og så lavede han løsning, hvor tegnet “:” blev fjernet fra det generede filnavn.

    Han undersøgte ikke om der måske skulle være andre ulovlige tegn eller om C# havde en klasse med en metode som kunne klare det for ham…

    Nå, men den findes – og jeg kan så mindst fem steder i vores kode se følgende konstruktion:

    var invalidcharacters = Path.GetInvalidFileNameChars();
    int invalidindex;
    do {
    invalidindex = name.IndexOfAny(invalidcharacters);
    if (invalidindex >= 0) {
    name = name.Remove(invalidindex, 1);
    }
    } while (invalidindex >= 0);

    Og et par varianter over samme tema.

    Nu skal jeg så tage stilling til spørgsmålet “skal jeg rydde op og simplificere først – eller skal jeg bare sikre at jeg ikke kommer til få en exception i dette specifikke spørgsmål?” Guess what resultatet bliver når fokus er at lave meget kode…

    DRY skal tages bogstaveligt ellers sander selv ny kode til på meget, meget kort tid.

Leave a Reply