English

Programmera på 'rätt' sätt
tankar kring och erfarenheter av programmering

En återkommande fråga från studenter som ska lära sig programmera är "Hur ska man tänka när man programmerar, var ska man börja?" Frågan är inte lätt att besvara, det finns ingen patentlösning som beskriver det korrekta tankesättet. Var och en måste hitta sitt eget sätt att tänka på och bemästra programmeringsspråket. Det finns dock ett antal saker som kan underlätta på vägen mot fullkomlig kontroll av ett språk (som om man någonsin skulle få det). Tänk igenom och försök följa dessa 'regler'. Det kommer att löna sig, det lovar jag!

Denna sida innehåller många programmeringsrelaterade termer som man efter ett tags programmerande tar för givet att alla vet vad de betyder. För att minska förvirringen har jag därför sammanställt en ordlista med de vanligaste orden. Ordlistan hittar du här!

Namnge variabler med förklarande namn

Variabelnamn som pt, n och f är sällan motiverade och gör att koden blir svårläst. Svårläst kod ger lätt upphov till missförstånd och onödiga fel. Det är därför viktigt att man namnger sina variabler och funktioner med beskrivande namn. Genomtänkt namngivning ger lättläst kod vilket i sin tur gör att man kan plocka ut en del av koden och direkt se vad den gör, utan att behöva läsa igenom hela programmet. Detta är precis vad man eftersträvar när man programmerar eftersom det underlättar när man vid ett senare tillfälle återkommer till koden och ska försöka förstå den igen. Det finns i allmänhet ingen fördel med korta namn. Att det skulle krävas mer minne med långa namn är rent trams. Alla namn översätts av kompilatorn till rena adresser och de har samma storlek oavsett vad namnet du skrev i koden var.

Värt att tänka på i detta sammanhang är även hur man skriver namnen på sina variabler och funktioner. Många programmeringsspråk har sina egna regler för hur det ska se ut (konventioner brukar det kallas). Ibland kräver språket att en variabel alltid börjar med stor bokstav (Erlang är ett sådant språk) i en del andra språk får man göra som man vill (till exempel C och Java). Att göra "som man vill" är sällan en bra idé i detta sammanhang. För nästan alla programmeringsspråk så finns det konventioner som visar hur det bör vara. Det är alltid en god början att ta reda på vad konventionen säger om det språk man vill programmera i.

Använd inte globala variabler

Variabler som lever i hela koden och kan ändras av flera funktioner är mycket svåra att hålla koll på. När man använder en variabel i en funktion förväntar man sig oftast att den ska innehålla samma värde efter ett funktionsanrop som den gjorde innan. Problemet med globala variabler är att detta inte nödvändigtvis är sant. Funktionen man anropar kan ändra i den globala variabeln, och dess värde är kanske inte längre det man förväntar sig.

Även här är läsbarhet en nyckelfråga. Det är mycket förvirrande om det plötsligt dyker upp en variabel som inte är deklarerad i den funktion man läser. Hur ska man veta vad den har för typ och värde och var i resten av koden den används?

Använd namngivna konstanter

Av ren självbevarelsedrift bör man tänka på att alltid använda namngivna konstanter där det är möjligt. Om man har konstanta värden som man använder i sitt program tjänar man både tid och kraft på att deklarera dessa med namn i stället för att skriva det numeriska värdet överallt i koden. Det blir annars ett förfärligt jobb att gå igenom programmet och leta efter alla ställen där man använt detta värde när man senare vill ändra på det. En mycket vanlig orsak till fel är att man inte hittat alla ställen och ett gammalt värde ligger kvar och stör. Deklarerar man värdet som en konstant och använder den i koden kan fel av det slaget aldrig inträffa. Det handlar förstås även om läsbarhet. En namngiven konstant är lättare att se vad den betyder än en magisk siffra i koden.

Vid flera tillfällen i ett program kan det hända att man vill använda en konstant med en smärre förändring. Till exempel kanske man har en konstant ANTAL och vill komma åt värdet ANTAL - 1. Skriv då ANTAL - 1 i koden! Det ger betydligt högre läsbarhet än att skriva 17, bara för att man råkar veta att ANTAL är 18. Subtraktionen kommer att utföras av kompilatorn och resultatet i den körbara filen blir exakt det samma. I detta exempel har jag skrivit konstanten med STORA bokstäver. Det är vanligt i många språk att man följer den regeln, men som tidigare nämnts ska man alltid kolla upp exakt vad som gäller i kodkonventionen för just det språk man arbetar med.

Använd aldrig GOTO

I en del programmeringsspråk finns kommandon som ovillkorligen hoppar till ett annat ställe i koden. Detta görs utan att ta ansvar för vad som händer runt omkring. Man ser den ofta med namn som goto eller longjmp. Hopp av detta slag bryter det normala kontrollflödet och gör att det blir mycket svårt att följa koden, det blir så kallad spagettikod. Det finns de som hävdar att man i vissa fall vill använda goto av effektivitetsskäl i vissa systemnära, tidskritiska delar av ett program. Ingen av dessa personer har dock lyckats visa upp ett exempel där man inte kunnat lösa det utan goto med samma slutresultat. Man får inte glömma att kompilatorn har en hel del hyss för sig när den omarbetar koden till ett körbart program.

Använd små, effektiva gränssnitt

Ett program kan ha flera olika nivåer av abstraktion. På en låg nivå kommunicerar funktioner och metoder med varandra genom sina argument och på högre nivåer är det moduler och klasser som kommunicerar genom att skicka meddelanden till varandra eller anropa varandras metoder. Gränsen mellan olika delar i ett program kallas för gränssnitt (interface på engelska). För att förenkla användningen och förståelsen av ett gränssnitt bör man hålla det så rent som möjligt. Låt inte klasser ha publika metoder som inte har en specifik (publik) uppgift. Låt inte funktioner ta argument som går att härleda i funktionen.

Man bör se till att vid funktionsanrop skicka med så få argument som möjligt. I de flesta fall kan variabler deklareras som temporärer i funktionen. Onödiga argument ger ett svåranvänt gränssnitt till funktionen och kan ge upphov till onödiga fel. När det gäller argument är det även en fråga om prestanda. Det kostar att skicka argument eftersom dessa måste kopieras till stacken, och även om jag i denna text vill framhäva kompilatorns förmåga att optimera bort det mesta så är det tyvärr mycket svårt för en kompilator att optimera bort onödiga argument.

Känner man behov av att skicka med väldigt många variabler till en funktion kanske det är dags att tänka om. Behövs verkligen alla argument? Många argument till en funktion är ofta ett tecken på att man försöker göra för mycket i samma funktion. Går det att dela upp arbetet i mindre delar och lägga delarna i separata funktioner? Om man verkligen behöver alla argumenten kan man oftast slå ihop flera av dem i en struktur eller ett objekt av något slag. De flesta programmeringsspråk har någon konstruktion för att sätta samman olika värden till en större enhet (kallas ofta struct, class, record eller liknande). Använd dessa!

Bygg ditt program av små, återanvändbara delar

Ett steg för att undvika massiva indataklumpar till funktioner är att bygga upp sitt program av små delar som utför en sak var. Dessa byggstenar skrivs lämpligen på ett såpass generellt sätt att de går att återanvända på flera ställen i programmet och kanske även i andra program - det är onödigt att uppfinna hjulet flera gånger. Än en gång får vi ökad läsbarhet. Denna gång tack vare funktionsanrop. Det är mycket lättare att få överblick över flera små funktioner än en jättestor. De flesta kompilatorer kommer automatiskt att lägga in små funktioner direkt på plats i det slutliga programmet (inlining) så inte heller detta medför någon prestandaförlust. Tvärt om kan det ge ökad prestanda då kodstorleken normalt blir mindre om man låter bli att duplicera kod.

En lärare sa en gång till mig att ingen funktion fick vara större än en skärmsida och ingen källkodsfil fick vara längre än 100 rader (det var på den tiden då en skärmsida var ca 25 rader á 80 tecken). Detta är kanske lite extremt, men principen är rätt. Tanken är att man ska kunna se hela funktionen på en gång utan att behöva flytta texten. Idag får man plats med mer på skärmen och gränserna för hur långa och hur många rader man kan ha är lite mer flexibla. I lite större projekt där man ska supporta olika plattformar och kanske har källkod distribuerad på olika servrar kan man än idag ha glädje av att följa den gamla 80-teckenregeln då man ibland tvingas sitta via en ssh-terminal och editera källkoden i emacs eller vi. Man ska inte förlita sig på att man alltid har tillgång till de hjälpmedel och editorer man använder till vardags och när en funktion börjar växa till ett hundratal rader eller en källkodsfil närmar sig 600-700 rader är det dags att ställa sig frågan: Kan man dela upp detta i mindre delar? Så gott som alltid så är det möjligt. En genomtänkt design minskar antalet fel i koden.

Representationsoberoende programmering

Funktioner som hanterar data och objekt ska inte bry sig om hur data är lagrat i datorns minne. Man ska aldrig utnyttja att man vet hur något representeras i ett objekt eftersom representationen kan förändras när som helst. Låt mig ge ett exempel.

Tänk dig att du skriver ett program där du hanterar personer. Du bygger upp en struktur med ett antal textfält för namn, personnummer med mera. Din representation av personnumret är en sträng "######-####". Runt om i ditt program använder du sedan denna struktur när du vill ta reda på fakta om en person. Du utnyttjar det faktum att personnummret är en sträng och att du till exempel vet att det sjunde tecknet är ett bindestreck.

Efter fem månaders kodande inser du att du istället vill ha en numerisk representation av personnummret som blir lite lättare att sortera och snabbare att jämföra med vid sökning. Du måste då ändra representation i strukturen. På hur många ställen i koden måste du nu ändra för att anpassa ditt program till denna nya representation? Förmodligen allt för många!

Hur ska man göra då?

I stället för att utnyttja att man vet att värdet lagras som en sträng skapar man en egen modul för datatypen. I ett objektorienterat språk skulle man skapa en klass för personnummer, i procedurella språk skapar men en ADT (Abstrakt Datatyp). Principen är exakt den samma. Klassen / ADT:n innehåller privat data, vars representation vi inte visar utåt, och ett antal selektorer. Selektorer är funktioner som returnerar det värde man vill ha på den formen man vill ha. Endast selektorerna får använda datastrukturen direkt, resten av programmet måste anropa någon av dessa selektorer. På så vis, när du ändrar representation kommer det endast att beröra dessa få och små funktioner, vilka blir mycket enkla att ändra eftersom de inte gör något annat än att returnera värdet. Koden i det övriga programmet kan fortsätta att använda personnummret som en sträng medan man internt kan börja betrakta det som ett tal. Vips så har du gjort om flera timmars tråkigt arbete till några minuters rent nöje!

Detta är (lite förenklat) vad som brukar kallas för representationsoberoende programmering. Är man orolig för prestandan så går det utmärkt att skriva selektorerna som makron. Normalt är dessa dock så små att de kommer att inline:as av kompilatorn ändå så det finns ingen anledning att undvika det extra abstraktionslagret. Den välkända år 2000-buggen skulle kunna ha lösts på en halvdag om man följt dessa regler från början.

Välindenterad och luftig kod

Som du säkert redan märkt handlar det nästan alltid om att göra koden mer lättläst. Alla typer av förbättringar som gjorts inom alla typer av språk handlar i botten om att göra koden mer lättläst. Det var därför man uppfann funktioner, datatyper, strukturer, klasser och högnivåspråk. Det är ingen funktionell skillnad mellan ett högnivåspråk som Java och den lägsta nivån, assembler. Det är bara lättare att överblicka koden. Nästa steg för att göra koden läslig är rent estetisk.

I nästan alla språk är det enbart för din egen skull som du trycker in en radbrytning lite då och då när du programmerar. Kompilatorn bryr sig inte om radbrytningar och blanksteg, den kan lika gärna läsa ett program som är skrivet på en enda jättelång rad. Men vi människor har lite större problem med att läsa en sådan text. Därför är det viktigt för läsbarheten av ett program att koden har en bra layout som bland annat markerar var olika stycken börjar och slutar. (Det finns ett par riktigt märkliga språk där radbrytningar och indentering har semantisk betydelse, så där kan man inte bryta mot denna regel ens om man vill.)

Ett viktigt steg i detta är indentering. Många programmeringseditorer indenterar koden automatiskt, har man tur passar denna automatiska indentering den personliga smaken. Det är svårt att säga att en indenteringsmodell är bättre än en annan även om det kanske finns ett par stilar som de flesta är överens om att de är sämre än andra. Det är naturligtvis högst personligt vad man tycker ser bäst ut och därför kan det hända att mina tips på denna punkt inte helt stämmer överens med din egen smak. Det viktiga här är inte hurvida du väljer att följa mina riktlinjer eller ej, det viktiga är att du själv funderar på hur du vill ha det i koden du arbetar med och ser till att du strikt följer de regler som du själv sätter upp. Punkterna nedan är hur jag brukar göra.

  • Indenteringen är tre tecken. Är den mindre blir det svårt att se vad som är vad och är den större blir raderna otrevligt långa.
  • Raderna är inte längre än 80-100 tecken för att man ska få en bra överblick. Detta är inte bara en artefakt från gamla terminaler med 80 tecken bred skärm, utan har även att göra med hur lätt det är för ögat att överblicka längre rader text.
  • Måsvingar hamnar först på en egen rad efter funktionshuvud, if-satser m.m. Det är mycket lättare att se vilka som hör ihop och det ger luftigare kod.
  • Blanksteg runt operatorer (+ - * / <; >; = o.s.v.) och efter komma i argumentlistor.

Kommentera koden

Hur bra man än väljer sina variabel- och funktionsnamn, hur väl man än strukturerar sin kod och delar upp den i små fina delar så kommer det inte att räcka för att någon annan ska förstå hur man som programmerare har tänkt. Kommentarer är oumbärliga för den som en dag ska läsa igenom koden, förstå den och försöka bygga vidare på den. Att sätta sig in i ett program som någon annan har skrivit är svårt nog, om det dessutom inte finns något som talar om vad de olika delarna i koden gör kan jobbet bli så svårt att det går snabbare att skriva om det hela från början. Detta gäller även när man hanterar sin egen kod och det kan räcka med ett par veckor för att man ska hinna glömma vad man gjort och varför.

Kommentarerna ska beskriva vad som händer i koden och vad olika funktioner gör. Det viktiga är att tala om varför. Alla kan se att i++ ökar värdet på i med ett, en kommentar av typen "Öka värdet med ett" är fullständigt meningslös. Det kommentaren ska säga är varför värdet ökas. Skriv kommentaren ur funktionens synvinkel. Det är oftast inte intressant att veta till exempel var argumenten kommer ifrån, det som är intressant är att veta vad de används till i den aktuella funktionen.

Exempel

Följande programexempel visar hur man kan skriva ett enkelt program på två olika sätt. Programmen är skrivna i Java men skulle se exakt lika ut i till exempel C eftersom programmet inte utnyttjar någon form av objektorientering. För dig som vill se hur programmet skulle se ut om man skrev det så som ett objektorienterat program ska se ut finns en version av detta på Kodsidan.

Båda exemplen tar samma indata och ger samma resultat, skillnaden ligger i hur lätt det är att förstå och underhålla programmen.

Exempel 1

    public class Cirkel{
     static public void main (String[] argv){
     double x=Double.valueOf(argv[0]).doubleValue();
     System.out.println("Omkrets: "+x*6.28318);
  System.out.println("Area: "+x*x*3.14159);
      }}
  

Exempel 2

    public class Cirkel
    {
       static final double PI = 3.14159;

       /*
        * Beräkna cirkelns area givet en radie
        */
       static double cirkelArea(double radie)
       {
          return radie * radie * PI;
       }

       /*
        * Beräkna cirkelns omkrets givet en radie
        */
       static double cirkelOmkrets(double radie)
       {
          return 2 * radie * PI;
       }

       /*
        * Här startar programmet.
        * En radie skall skickas med som argument.
        */
       static public void main (String[] argv)
       {
          // Hämta in radien från argumentet
          double radie = Double.valueOf(argv[0]).doubleValue();

          System.out.println("Omkrets: " + cirkelOmkrets(radie));
          System.out.println("Area: " + cirkelArea(radie));
       }
    }
  

Döm själv vilket exempel som är lättast att följa. Argument av typen "Men det är ju inte svårt att se vad som händer i det där programmet, det är ju bara sex rader kod!" köper jag inte. Riktiga program är inte sex rader långa.

Vill man nu bygga ut programmet så att det även kan räkna ut mantelytan, volymen med mera av en kon och en cylinder (till vilket man använder både omkrets och area av bottencirkeln), blir det inget större jobb i exempel 2, men i exempel 1 kan man lika gärna skriva ett nytt program - det blir inte mer arbete. En annan skillnad ligger i användandet av en konstant i exempel 2. Det underlättar betydligt om man till exempel vill öka noggrannheten på PI.

Det finns ingen anledning att skriva för kompakt kod. En bra kompilator genererar samma körbara fil för båda exemplen, eller till och med en bättre för exempel 2 då den kan förstå vad programmet egentligen gör.

    Överlåt optimeringen åt kompilatorn,
    den är bättre på det, jag lovar!