JensGustavsson.se
Förstasidan | Arkiv | Artiklar | CodeCompanion | Länkar | Vem är Jens Gustavsson?
Förstasidan : september 2005 : Article

Skriv begriplig källkod

I den här artikeln ska jag berätta hur man kan mäta programkods komplexitet och vad man har för nytta av att göra det. Till hjälp har jag hittat på något som jag kallar komplexitetsmassa som beskriver hur besvärligt det är att förstå sig på en enhet programkod. Jag har skrivit ett verktyg som mäter komplexitetsmassan hos källkod, men innan jag berättar mer om det ska vi ta en titt på vad komplexitetsmassa är och varför man vill mäta den.

Varför bry sig om komplexitetsmassa?

Idén med komplexitetsmassa är att den ska vara ett mått på hur svårt det är att förstå ett program eller en del av ett program. Komplexitetsmassan är ett tal som är hyfsat proportionellt mot den tid det tar att förstå programmet. Den tiden påverkar starkt kostnaden för underhåll och vidareutveckling av systemet. När man väl förstår systemet är det ofta lätt att utföra rätt förändringar av det. OK, jag erkänner att det kan kräva en hel del tankearbete och praktiskt jobb att utföra ändringarna, men innan detta kan påbörjas måste man begripa strukturen av det existerande programmet. Detta är svårt, tidskrävande och dyrt. Och ofta ganska frustrerande. Bristen på riktig förståelse av det existerande systemet är den enskilt största orsaken till att programmerare introducerar nya fel när gamla rättas.

Låg komplexitetsmassa leder alltså till billigare underhåll och man ska komma ihåg att ofta läggs mer pengar på att underhålla ett system än vad nyutvecklingen av det kostade.

Vad är komplexitetsmassa?

Som alla vet är olika programrader olika svåra att förstå sig på. Vissa rader måste man knappt läsa för att förstå vad de är till för, medan andra kan kräva minuter eller till och med timmar av eftertanke. För skojs skull kan vi titta på ett par exempel. Här är två 21-raders kodsnuttar (som båda har sina ursprung i Javas klassbibliotek). Titta lite snabbt på dem, utan att bry dig om vad de egentligen gör. Försök istället snabbt bedöma vilken av dem som skulle vara lättast att utföra en förändring i.

Här är den första kodsnutten:

nFractBits = countBits( fractBits );
nTinyBits = Math.max( 0, nFractBits - binExp - 1 );
if ( binExp <= maxSmallBinExp && binExp >= minSmallBinExp ){
    if ( (nTinyBits < long5pow.length) && ((nFractBits + n5bits[nTinyBits]) < 64 ) ){
        long halfULP;
        if ( nTinyBits == 0 ) {
            if ( binExp > nSignificantBits ){
                halfULP = 1L << ( binExp-nSignificantBits-1);
            } else {
                halfULP = 0L;
            }
            if ( binExp >= expShift ){
                fractBits <<= (binExp-expShift);
            } else {
                fractBits >>>= (expShift-binExp) ;
            }
            developLongDigits( 0, fractBits, halfULP );
            return;
        }
    }
}

Här är den andra kodsnutten:

public static String toString(int mod) {
    StringBuffer sb = new StringBuffer();
    int len;

    if ((mod & PUBLIC) != 0)        sb.append("public ");
    if ((mod & PROTECTED) != 0)     sb.append("protected ");
    if ((mod & PRIVATE) != 0)       sb.append("private ");
    if ((mod & ABSTRACT) != 0)      sb.append("abstract ");
    if ((mod & STATIC) != 0)        sb.append("static ");
    if ((mod & FINAL) != 0)	         sb.append("final ");
    if ((mod & TRANSIENT) != 0)     sb.append("transient ");
    if ((mod & VOLATILE) != 0)      sb.append("volatile ");
    if ((mod & SYNCHRONIZED) != 0)  sb.append("synchronized ");
    if ((mod & NATIVE) != 0)        sb.append("native ");
    if ((mod & STRICT) != 0)        sb.append("strictfp ");
    if ((mod & INTERFACE) != 0)     sb.append("interface ");
    
    if ((len = sb.length()) > 0)    /* trim trailing space */
        return sb.toString().substring(0, len-1);
    return "";
}

Jag tror inte det är någon tvekan om att den andra kodsnutten är mindre komplex och därmed lättare att modifiera.

Vad är det då som gör programkod komplex? Några exempel är, att krångliga uttryck är vanliga, att det finns djupa nästlingar i programflödesstrukturen, att många variabler används och att klasser har många beroenden till andra klasser. Och det finns många fler sådna faktorer som tillsammans kan fungera som mått på kodens komplexitet. Jag menar inte att det alltid är dåligt att använda många variabler eller att använda avancerade uttryck. Men om allt annat är lika är det lättare att förstå ett program med färre variabler, och om allt annat är lika är det lättare att förstå ett program som inte har djupa nästlingar i flödesstrukturen.

Om man försöker minimera en enskild faktor så finns det alltid risk att man gör det på bekostnad av någon annan, till exempel kan man ofta minska krångligheten hos uttryck om man tillåter sig att använda fler variabler. Det man bör sträva efter är ett slutresultat med minsta möjliga totala komplexitet och därför bör man balansera faktorerna mot varandra. Vilket är vad alla programmerare jobbar med hela dagarna, även om inte alla är medvetna om att det är det de gör.

Om man definierar komplexitet lite striktare än jag gjort här så går det att automatiskt mäta hur komplex varje del av ett program är. Man skulle kunna använda en skala där 0 är lägsta tänkbara komplexitet och 1 är högsta tänkbara. Om man gör den här mätningen för varje metod i programmet och ritar in resultatet i ett stapeldiagram skulle det kunna se ut så här:

I diagrammet kan man läsa ut hur komplex varje metod är, eller mer precist den genomsnittliga komplexiteten för koden i metoden. Till exempel att metoden B är mycket komplexare än metoden A. Däremot finns det inget sätt att se hur stora de olika metoderna är. Det skulle gå att låta staplarnas bredd visa hur lång respektive metod är. Som storleksmått skulle det gå att använda antal rader kod, men jag föredrar att istället räkna antal satser. Resultatet blir ungefär likadant, men man slipper fenomenet med att antalet rader kan ändras om koden formateras om. Så här ser det då ut:

Nu kan vi se att metod A är mycket större än metod B.

Och nu mina damer och herrar kommer vi äntligen till definitionen av komplexitetsmassa för en metod. Komplexitetsmassan för en metod är lika med arean av den metodens stapel. Eller, med andra ord: komplexitetsmassan är den genomsnittliga komplexiteten för metodens källkod multiplicerat med metodens storlek. Det är detta mått som jag hävdar är hyfsat proportionell med hur svårt det är att förstå metoden. Och det är detta mått som därför bör minimeras.

Det går att hitta på massor av roliga diagram som på olika sätt illustrerar kodens komplexitet. För att få en överskådlig bild kan man slå ihop alla metoder som hör till samma klass till en stapel. Stapelns höjd får bli den genomsnittliga komplexiteten för alla kod i klassen och dess bredd får motsvara summan av alla klassens metoders storlek. På samma sätt går det att skapa en stapel för ett helt system eller ett antal staplar för olika delsystem. På så sätt går det att snabbt få en överblick av riktigt stora system.

Hur kan man använda mätresultaten då?

Det bästa av allt är att komplexitetsmassan kan mätas automatiskt och presenteras för projektledare i form av snabböverskådliga diagram. Istället för att läsa all källkod låter man ett verktyg göra jobbet. För projektledaren blir det som om man hade en konsult som hela tiden granskade källkoden och gav utlåtanden om hur komplexa olika delar av systemet är. Det ger en överblick av programkoden som annars är svår att få i stora projekt. Problemmoduler kan identifieras tidigt och genom att hela tiden aktivt arbeta med att hålla komplexitetsmassan låg minskar man risken att sitta med mängder av programkod som bara gör nästan vad den ska och som i bästa fall programmeraren som skrivit den kan förstå.

Många programmerare vet hur viktigt det är med god kodstruktur och vill lägga tid på att skapa god kod, men har svårt att motivera detta för chefer, eftersom skillnaden mellan bra och dålig kod inte är uppenbar omedelbart. Hur får man chefen att gå med på att man vill lägga ned arbetstid på uppgifter som inte gör någon märkbar skillnad? För någon som aldrig skrivit en programrad är det inte lätt att förstå att det i längden är dyrt att ha många beroenden mellan klasser eller många obegripliga villkorssatser. En bra sak med komplexitetsmassa är att den kan uttryckas som en enkel siffra, eller som chefen nog hade kallat det: ett nyckeltal. Och vips kan man säga att man vill lägga en halvdag på att minska komplexiteten på foo-modulen från 200 till 100, något som genast låter bättre i chefens öron.

Verktyg för att mäta

Det finns inget kommersiellt verktyg som mäter komplexitetsmassa. Under några år har jag arbetat med en prototyp som fungerar bra även om den inte har alla finesser man önskar sig av ett fullfjädrat verktyg. Prototypen mäter komplexitetsmassa hos Javaprogram och visualiserar den i form av diagram. Den är testad hos ett företag, på några studentprojekt och naturligtvis på mina egna program. Jag har också kört en del tester på Javas klassbibliotek (ganska kul att se vad man kan hitta där, något jag får lov att återkomma till). Jag håller på att finjustera de metriker jag använder och tänker därför vänta med att göra verktyget allmänt tillgängligt. Men jag är på ständig jakt efter lämpliga projekt för pilotstudier. Hör av dig om du har något intressant projekt där du tror att det skulle vara nyttigt att mäta komplexiteten. Mejla gärna till jens@jensgustavsson.se.