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.
Tillbaka till indexet |