Basic personligt

Daniel Brahneborgs blogg

Hemliga typer

Det tar inte så lång tid från att man har börjat programmera i C förrän man behöver definiera en egen datastruktur. Normalt görs detta med “struct datatypensNamn { … };”, och sedan kan man använda “struct datatypensNamn” som variabeltyp. Sekunden senare hittar man typedef, som gör att man kan ersätta det där med bara “datatypensNamn”, genom att istället skriva “struct datatypensNamn { … }; typedef struct datatypensNamn datatypensNamn”, eller ännu kortare med en anonym struct: “typedef struct { … } datatypensNamn;”.

So far so good. Ja, tills dess att man har två strukturer som pekar på varandra. Det funkar i och för sig att använda en pekare till “struct datatypA” innan den är definierad, men då står det “struct datatypA” inne i “struct datatypB”, men bara “datatypA” på alla andra ställen. Fult.

Sorgligt nog var det först ganska nyligen (efter att ha programmerat i språket i en bit över ett kvarts sekel – heja mig) som jag hittade knepet för hur man ska skriva för att få det helt rent, vilket fanns på Wikipedias sida om syntaxen i C. En “typedef struct datatypA datatypA;” fungerar nämligen även om man inte har definierat “datatypA” än (sidan hos Wikipedia har ett komplett exempel).

En kul sidoeffekt, som bara står som “data hiding” hos Wikipedia, är att man på det här sättet även kan minska massor av beroenden mellan de headerfiler där strukturerna definieras. Om datatypA pekar på en datatypB som i sin tur pekar på en datatypC, så måste ju dels “a.h” dra in “b.h”, för att få veta vad datatypB är för någonting. Men där behöver bara finnas en tom “typedef”-rad, istället för en full definition av “datatypB”, vilket ju skulle kräva att den drog in “c.h”. Med en sådan här anonym typedef har man då brutit det onödiga beroendet från “a.h” till “c.h”.

De funktioner som är relaterade till datatypA använder förmodligen datatypA som parameter eller returvärde här och där. Klientkoden som använder de här funktionerna kanske då inte behöver veta precis hur datatypA ser ut, så genom att flytta bort definitionen av den från “a.h” (och bara ha kvar en ensam typedef-rad), bryts först alla beroenden från “a.h” till “b.h” osv, men det blir också fritt att ändra i datatypA utan att någon utanför behöver bry sig. De får en pekare som skickas runt, men vad som faktiskt pekas på är det ingen som behöver veta. Den kan därför ändras på hur mycket som helst, utan att någon annan kod påverkas. Sånt är bra. Förmodligen påverkas även kompileringstiden positivt, dvs blir kortare.

June 25th, 2014 Posted by Daniel Brahneborg | blogg | no comments

TDD – fast baklänges

Enligt TDD, dvs Test Driven Development, ska man skriva testerna innan man skriver den riktiga koden, eftersom det har ett antal positiva effekter. Vilka, och vilka eventuella nackdelar som finns, var det som den där diskussionen handlade om.

En vanlig fras i sammanhanget är “red – green – refactor”, som effektivt sammanfattar principen.

  1. Skriv (ett) test för den nya funktionaliteten. Kör testet. Det kommer att faila eftersom den nya koden inte finns än. Testverktyget skriker då med en röd ikon (åtminstone om man kör JUnit eller någon liknande pryl).
  2. Skriv kod så att testet går igenom. Grön ikon.
  3. Refaktorera och städa upp koden så att den hålls fin.

Eftersom man redan har ett test för funktionaliteten vet man direkt när man är klar, och man kan vara trygg när man refaktorerar, eftersom både all gammal och den nya funktionaliteten har tester som ser till att ingenting går sönder. Eller att man märker när man har sönder någonting, är kanske mer korrekt.

Beroende på vad det är för typ av uppgift så varierar det faktiskt i vilken ordning jag kör steg 1 och 2. Ibland måste jag helt enkelt börja meka runt i koden först för att se vad som behövs göras, innan jag alls förstår vad det är jag ska testa.

Däremot gör jag nästan alltid refaktoreringen först, och aldrig någonting på slutet. För mig har refaktoreringar ett tydligt mål, nämligen att en viss typ av framtida ändring ska bli enklare att göra. Innan jag vet mer exakt vad som ska göras kan jag därför inte refaktorera speciellt mycket, för jag kanske drar koden åt fel håll. Hade “ju mer refaktorering desto bättre” gällt, hade det inte varit några problem, men så enkelt är det ju inte. Man kanske har kod som ser ut så här:

if (a && b) c();

Det finns ett par olika ändringar man kan göra där.

  1. Testet “a && b” kan flyttas till en egen funktion. Positivt om samma test finns på flera ställen, men negativt eftersom kopplingen till “c” minskar.
  2. Testet kan flyttas in i “c”, om alla anrop ser ut på samma sätt.
  3. Både test och anrop kan flyttas till en ny funktion “d”, om samma konstruktion finns på flera ställen, men inte alla.

En framtida ändring kanske gör att “b” försvinner, och att då ha en egen funktion (efter ändring 1) för att bara testa “a” är ren obfuskering. Det kanske tillkommer nya anrop till “c” som ser annorlunda ut, vilket gör att ändring 2 vore dum. Anropen till “d” kanske försvinner ett efter ett, om koden som gör de anropen refaktoreras ihop. I så fall är ändring 3 helt onödig. Samtidigt är koden i sin nuvarande form inte tillräckligt dålig för att någon refaktorering ska vara nödvändig överhuvudtaget.

Istället tar jag varje ny målbild, och sakta men säkert refaktorerar koden så att den “riktiga” ändringen blir så liten som möjligt. Samma princip som när man ska byta fil i trafiken. Först hittar man en lucka, ser till att man har rätt fart, och allt annat man måste göra. Sedan blir själva filbytet nästan en icke-operation, speciellt om man kör hoj då själva flytten inte handlar om så många centimeter. En liten knyck med styret, och så är man klar. Eftersom ändringen ifråga blir så liten, finns inte heller något att städa upp efteråt. Efter varje ändring är koden trots allt bättre än tidigare, så även om den slutgiltiga ändringen skulle falla bort (pga kunden ändrar sig) är ingen skada skedd. Nästa kund kanske vill ha en ändring åt samma håll, och då är man redan nästan framme innan man ens har börjat.

Alla varianter har ju sina fördelar och nackdelar, men det här är det som funkar för mig.

June 11th, 2014 Posted by Daniel Brahneborg | blogg | no comments

Dynamiska fönster

En av de huvudsakliga uppgifterna som vår sms-applikation har, är att skicka iväg sms. När den väl har kopplat upp sig och loggat in och vad som nu behövs, skickas ett sms iväg. Efter en stund kommer ett svar tillbaka, som säger om det togs emot på andra sidan eller om något var fel på det. Sedan skickas nästa sms, applikationen väntar på svar, osv.

Enkelt och bra, men onödigt långsamt. Från att sms’et är skickat tills att svaret har kommit tillbaka, sitter vår applikation och är sysslolös. Samma sak på andra sidan, mellan att den har skickat tillbaka ett svar till oss och nästa sms har hunnit komma dit, har den inget att göra.

  1. Tidpunkt a: Vi skickar ett sms.
  2. Tidpunkt b: Det tas emot.
  3. Tidpunkt c: Svar skickas.
  4. Tidpunkt d: Svar tas emot av oss.
  5. Tidpunkt e: Nytt sms skickas.
  6. Osv.

Skickar man data över internet från Sverige till Tyskland tar det ungefär 50 ms för det att komma fram, vilket alltså är tiden mellan a och b samt mellan c och d. Om applikationen på mottagarsidan är skriven av puckade amatörer (lite för vanligt, tyvärr) kanske tiden mellan b och c också är 50 ms. Tiden mellan d och e, dvs tiden som går åt i vår applikation, är i det här fallet försumbar. Hela cykeln mellan a och e är alltså 150 ms, varav bara 50 ms tillbringas med att göra något nyttigt.

Det här är ett så vanligt problem i nätverksvärlden att det har fått en standardlösning som kallas Sliding Window Protocol. Efter att sms nr ett har skickats, skickas omedelbart två stycken till. De kommer fram i tur och ordning, svar skickas, för varje svar vi får skickas ett nytt sms, så lagom tills att svaret på sms nr tre har skickats, har redan sms nr fyra kommit fram. Applikationen på andra sidan ägnar nu 100% av tiden med att jobba, varvid den genomsnittliga behandlingstakten har tredubblats. Att öka storleken på fönstret, som här alltså är tre, är ingen idé. Mottagaren kan ju ändå inte jobba snabbare, utan de extra paketen kommer bara ligga och vänta i diverse buffertar i nätverket. Skulle förbindelsen förbättras så att det bara tog 25 ms att komma fram, kan storleken sänkas till två (ju lägre dess bättre pga säkrare pga anledning). Det krävs därför ganska mycket experimenterande för att hitta den bästa storleken, dvs för att få maximal fart. Vartefter delayerna i nätverket och prestandan på applikationen på andra sidan ändras, behöver storleken också ändras.

Skumt nog så hittade jag ingen riktigt passande algoritm för att hitta rätt storlek automatiskt, så jag gjorde en egen. Det var inte så himla svårt.

  1. Börja med storlek 1.
  2. Mät hastigheten under ett antal sekunder, och öka sedan storleken till 2.
  3. Mät några sekunder till.
  4. Om hastigheten har ökat, öka storleken ett steg till. Annars minska ett steg.
  5. Repetera från punkt 3.

Hur stor del av tiden som ägnas åt nätverkstrafik respektive i applikationen är faktiskt irrelevant. Genom att låta storleken wobbla upptäcks direkt skillnader i nätverket, så att storleken kan anpassas därefter. Naturligtvis krävdes en hel del testande och lusläsande av loggfiler för att se till att alla randfall hanterades korrekt.

Det roligaste var nog buggen i punkt 4. Det var nämligen exakt samma bugg som jag fick för drygt 30 år sedan när jag skrev mitt första lite större program, ett Space Invaders eller något sådant, på en ABC80. Förvisso lite pinsamt, men å andra sidan visste jag nu precis hur jag skulle fixa den. Erfarenhet rulez.

June 5th, 2014 Posted by Daniel Brahneborg | blogg | no comments

|