De tio artiklarna är:
fredag 27 februari 2009
10 artiklar som alla borde ha läst (minst två gånger)
Läste på http://blog.objectmentor.com/ ett inlägg av Michael Feathers. Han skriver tydligen på en ny bok och hade konstaterat att han hade tappat fokus på vilken publik han skriver för. Vad kan de som lässer boken. Kort och gott så kom han på sig med att förklara förklaringarna vilket inte var så bra. Så han plockade fram en lista på tio artiklar som i sin tur skulle förklara de begrepp han tänkte använda sig av. Några av dem är ganska gamla så det är ingen revolutionerande kunskap på något sätt. Ibland kan det vara bra att titta på annat än det som är hypat idag. Jag tänkte försöka mig på att läsa dessa tio artiklar och dessutom försöka att skriva ihop en kort sammanfattning här (om jag förstår tillräkligt mycket av dem).
De tio artiklarna är:
On the criteria to be used in decomposing systems into modules – David Parnas A Note On Distributed Computing – Jim Waldo, Geoff Wyant, Ann Wollrath, Sam Kendall The Next 700 Programming Languages – P. J. Landin Can Programming Be Liberated from the von Neumann Style? – John Backus Reflections on Trusting Trust – Ken Thompson Lisp: Good News, Bad News, How to Win Big – Richard Gabriel An experimental evaluation of the assumption of independence in multiversion programming – John Knight and Nancy Leveson Arguments and Results – James Noble A Laboratory For Teaching Object-Oriented Thinking – Kent Beck, Ward Cunningham Programming as an Experience: the inspiration for Self – David Ungar, Randall B. Smith
De tio artiklarna är:
söndag 22 februari 2009
Legacy code
Enligt wikipedia betyder "Legacy code" följande: source code that relates to a no-longer supported or manufactured operating system or other computer technology. Läser man Michel Feathers artikel "Working effectivly with legacy code" får man följande till livs: The main thing that distinguishes legacy code from non-legacy code is tests, or rather a lack of tests. Michel Feathers hävdar att man inte behöver vänta på att support för koden ska försvinna, man kan skriva legacy code, alltså koden du ser på skärmen kan uppfylla definitionen innan du ens tryckt på sparaknappen eller kompilerat den första gången.
Gammal kod är ofta svår att arbeta med. Ingen som längre jobbar med koden kommer ihåg varför man gjorde på ena eller andra sättet och ännu värre, det är inte någon som vet vad som är rätt eller fel längre, särskilt med hänsyn taget till eventuella förändrade krav från omvärden på vad koden ska göra. Detta är dock brister man kan se efter någon vecka i projekt som har fått börja från början. "Det är N som har skrivit koden, han vet hur den fungerar" är något det man kan höra ganska snart in i ett projekt. Något som kan vara ett första tecken på att man nyproducerar Legacy code.
Varför är då tester boten mot Legacy code? Tester skapar ett förtroende för att man kommer att få reda på om man förstör existerande funktionallitet och med det försvinner det största problemet med Legacy code, rädlan för att man förstör något som idag fungerar. Så Michel Feathers har säkert funderat på varför man ska vänta på att koden blir gammal och osupportad innan man kallar den för legacy code när det största problemet med legacy code kommer redan vid nyproduktion, rädslan för att ändra det som fungerar.
Men om man nu sitter med ett projekt där det inte finns några tester och ingen längre kan säga hur de tänkte när de skapade koden. Tanken på att sätta sig och börja dokumentera existernade funktionallitet kanske är så upplyftande men på något sätt måste det göras. Hur ska man annars veta om den förändringen man gör inte påverkar annan funktionallitet på ett oavsiktligt sätt.
Vi dokumenterar den existerande funktionalliteten genom att skriva tester, att sitta och klicka i applikationen är en syssla som människor är dåligt rustad för . Dessa tester är dock något annorlunda. De är inte till för att verifera att den existerande koden gör rätt utan har som enda syfte att upptäcka om den existerande funktionalliteten förändras. Om en metod för ett givet input returnerar true, så har testet som uppgift att tala om när samma input ger false. Hurvida det är rätt att returnera true för det givna inputet är inte viktigt. Det som är viktigt är att det är som koden levererar idag.
Att skriva tester för en hel applikation är inte riktigt genomförbart dock. Vi måste försöka att titta på var det är vi ska göra vår förändring och sedan försöka att ringa en "trång punkt" genom vilken funktionalliteten styrs idag och testa utifrån denna trånga punkt. Syftet är inte att ge oss en komplett bild utan något som genom en rimlig arbetsinsats kan ge oss en relativt god chans att bli informerade om att vi har förändratat vi borde hållt tassarna borta från.
Men nu när vi skrivit de tester som verkar rimliga har vi en uppgift till innan vi faktiskt kan påbörja den uppgift som är vårt faktiskta uppdrag. Vi bör refaktorera den existerande koden. Detta genom att bryta ut se till att variabler och metodnamn är vettigt döpta, bryta ut metoder och sist se om vi kan struktuerar klasserna på ett bättre sätt, tex genom att byta ut if/else mot polymorphism. För varje steg kör vi våra tester så att vi inte oavsiktligt har ändrat något.
Nu är vi äntligen framme. Vi har betalat en del av den skuld som tidigare utvecklare belastat applikationen med och kan utifrån de tester vi skrivit steg för steg skriva om testerna till att testa den funktionallitet vi önskar att applikationen ska ha och till sist anpassa applikationen så att testerna börjar lysa grönt.
Lät det krångligt? Att förändra kod till nya krav är inte alltid enkelt. Men man kan göra det på ett sätt som ger dålig nattsömn och kräver magsårs medicin eller så försäkrar man sig genom att installera larm som kan upptäcka felaktigheter.
Gammal kod är ofta svår att arbeta med. Ingen som längre jobbar med koden kommer ihåg varför man gjorde på ena eller andra sättet och ännu värre, det är inte någon som vet vad som är rätt eller fel längre, särskilt med hänsyn taget till eventuella förändrade krav från omvärden på vad koden ska göra. Detta är dock brister man kan se efter någon vecka i projekt som har fått börja från början. "Det är N som har skrivit koden, han vet hur den fungerar" är något det man kan höra ganska snart in i ett projekt. Något som kan vara ett första tecken på att man nyproducerar Legacy code.
Varför är då tester boten mot Legacy code? Tester skapar ett förtroende för att man kommer att få reda på om man förstör existerande funktionallitet och med det försvinner det största problemet med Legacy code, rädlan för att man förstör något som idag fungerar. Så Michel Feathers har säkert funderat på varför man ska vänta på att koden blir gammal och osupportad innan man kallar den för legacy code när det största problemet med legacy code kommer redan vid nyproduktion, rädslan för att ändra det som fungerar.
Men om man nu sitter med ett projekt där det inte finns några tester och ingen längre kan säga hur de tänkte när de skapade koden. Tanken på att sätta sig och börja dokumentera existernade funktionallitet kanske är så upplyftande men på något sätt måste det göras. Hur ska man annars veta om den förändringen man gör inte påverkar annan funktionallitet på ett oavsiktligt sätt.
Vi dokumenterar den existerande funktionalliteten genom att skriva tester, att sitta och klicka i applikationen är en syssla som människor är dåligt rustad för . Dessa tester är dock något annorlunda. De är inte till för att verifera att den existerande koden gör rätt utan har som enda syfte att upptäcka om den existerande funktionalliteten förändras. Om en metod för ett givet input returnerar true, så har testet som uppgift att tala om när samma input ger false. Hurvida det är rätt att returnera true för det givna inputet är inte viktigt. Det som är viktigt är att det är som koden levererar idag.
Att skriva tester för en hel applikation är inte riktigt genomförbart dock. Vi måste försöka att titta på var det är vi ska göra vår förändring och sedan försöka att ringa en "trång punkt" genom vilken funktionalliteten styrs idag och testa utifrån denna trånga punkt. Syftet är inte att ge oss en komplett bild utan något som genom en rimlig arbetsinsats kan ge oss en relativt god chans att bli informerade om att vi har förändratat vi borde hållt tassarna borta från.
Men nu när vi skrivit de tester som verkar rimliga har vi en uppgift till innan vi faktiskt kan påbörja den uppgift som är vårt faktiskta uppdrag. Vi bör refaktorera den existerande koden. Detta genom att bryta ut se till att variabler och metodnamn är vettigt döpta, bryta ut metoder och sist se om vi kan struktuerar klasserna på ett bättre sätt, tex genom att byta ut if/else mot polymorphism. För varje steg kör vi våra tester så att vi inte oavsiktligt har ändrat något.
Nu är vi äntligen framme. Vi har betalat en del av den skuld som tidigare utvecklare belastat applikationen med och kan utifrån de tester vi skrivit steg för steg skriva om testerna till att testa den funktionallitet vi önskar att applikationen ska ha och till sist anpassa applikationen så att testerna börjar lysa grönt.
Lät det krångligt? Att förändra kod till nya krav är inte alltid enkelt. Men man kan göra det på ett sätt som ger dålig nattsömn och kräver magsårs medicin eller så försäkrar man sig genom att installera larm som kan upptäcka felaktigheter.
torsdag 19 februari 2009
Design Principles and Design Patterns fortsättning
Tyckte att det blev lite långrandigt så jag bestämde mig för att dela upp min sammanställning av Bob Martins - "Design principles and Design Patterns".
Nu blir det hur man kan hantera beroenden moduler i en applikation.
Packet Cohesion Priniciples
Tre sätt att resonera runt hur man ska förpacka sina moduler
The Packet Coupling Principles
De tre kommande principerna styr hur relationerna mellan olika modulerna ska fungera.
Nu blir det hur man kan hantera beroenden moduler i en applikation.
Packet Cohesion Priniciples
Tre sätt att resonera runt hur man ska förpacka sina moduler
- The Release Reuse Equivalency Priciple (REP) - REP kopplar ihop releasehantering med återanvändning. De klasser som behövs för att kunna skapa en release av en modul ska grupperas tillsammans och därmed återanvändas tillsammans. Det gör det smidigt tex för en kund att veta att det har hänt tillräkligt med koden för att det ska vara idé att uppdatera även om han/hon bara är intresserad av en liten delmängd av de funktioner som kommer med releasen.
- The Common Closure Principle (CCP) - Klasser som ändras tillsammans bör förpackas tillsammans. Om man befinner sig i utvecklingsfasen så vill man inte hantera många olika moduler och hålla koll på vilka andra moduler som man är beroende och hur vida de modulerna ändrats på ett sätt som kräver ändring i den egna modulen. Därför minimerar man antalet moduler som behöver uppdateras för en releasecykel.
- The Common Reuse Principle (CRP) - Klasser som inte används tillsammans ska inte förpackas tillsammans. Varför ska man behöva uppdradera ett operativsystem bara för att få tillgång till en funktion? Förpacka därför bara det som används tillsammans för att inte tvinga på användaren en större förändring än nödvändigt.
The Packet Coupling Principles
De tre kommande principerna styr hur relationerna mellan olika modulerna ska fungera.
- The Acyclic Dependencies Principle (ADP) - Ganska enkel och självklar. Modulerna ska inte ha cykliska relationer, tex Modul A -> Modul B -> Modul C -> Modul A. Tänk dig att ha dataproviders som är beroende på GUI. Ha någon som håller koll på att cykliska relationer inte smyger sig in i applikationen.
- The Stable Dependencies Principle (SDP) - SDP säger att man ska försöka att rikta sina beroenden mot det som är stabilt. Vad är då stabilt? Det som är svårt att förändra av olika skäl kan sägas vara stabilt. Den viktigaste faktorn för att en modul är stabil är att den har många andra moduler som är beroende på den. Ska man ändra i modulen så får man ändra på många andra ställen. Herr Martin ställer upp en enkel formel för att beräkna om en modul är stabil eller ej.
Ca = Inkommande beroenden (klasser utanför modulen som är beroende på modulen i fråga)
Ce = Utgående beroenden (klasser som är beroende på andra klasser utanför modulen)
I = Instabilitet - Beräknas I = Ce / (Ca + Ce)
Så med formeln ovan kan vi säga att SDP betyder: Din modul ska ha högre beroenden än de den har sina beroenden på. Men ska alla moduler vara stabila? Självklart inte, vi vill ju att våra program ska kunna följa med ovärldens krav utan stora arbetsinsatser. - The Stable Abstractions Principle (SAP) - SAP är svaret på problemet som uppstår med SDP där det blir svårare och svårare att utföra förändringar desto fler beroenden modulen har. SAP säger att stabila moduler ska vara abstrakta, alltså inte ha någon implementation. Det ger oss frihet att förändra byta ut implementation utan att påverka modulen som alla andra beror på. Alltså ska de mest stabila modulerna vara abstrakta. Herr Matrin råkar ha en liten formel på att mäta detta också:
Nc = Antalet klasser i modulen
Na = Antalet abstrakta klasser/interface
A = Abstrakthet(?) A = Na/Nc
Om man jämnför A med den tidigare I så bör de följa varandra. En modul med högt I bör också ha ett högt N och en modul med lågt I bör ha lågt N.
Det finns en ideallinje som I och A följer och man kan räkna på hur långt ifrån denna ideallinje en modul ligger med hjälp av följande formel:
D = (A + I - 1) / sqrt(2) (ger ett värde mellan 0 - 0.7 ungefär) där värden närmare noll är bättre.
Naturligtvis är detta bara beräkningar och inget man ska se som absoluta fakta. Men har man gjort värdena kan man välja att tro på dem eller säga att man har gjort val som gör dem missvisande. Men då har man ett värde att resonera runt vilket förmodligen är något bättre än att bara gissa.
måndag 16 februari 2009
Design Principles and Design Patterns
Hittade ett kul dokument som Bob Matrin skriv i början av 2000-talet. Känner igen mycket av innehållet från Clean Code fast i kortare form. Tar upp några av sakerna där ifrån.
Ruttnade kod
Jag har inte varit med i något projekt där man inte har försökt att utgå från de bästa av principer och sett fram emot hur bra allt ska bli. Hur alla problem man har haft i tidigare projekt ska lösas och man ska göra allt rätt en gång för alla. Men när projektet har pågått ett tag så har de rosafluffiga drömmarna gått i kras och projektet är samma gamla ruttna soppa som de alltid blir. Man tycker att man borde ha lärt sig. En liten tröst i sammanhanget kan vara att de flesta utvecklingsprojekt tycks gå samma väga. Goda intentioner som malts sönder och samman. Bob tar upp fyra symtom på ruttnande design:
Lösningen för att hantera förändrade krav är att hantera de beroenden som finns i koden. Nedan kommer några av de principer som föreslås i dokumentet.
Open closed principle (OCP)
Alla "moduler" ska vara öppna för att byggas ut men stängda för att modifieras. Låter lite motsägelsefullt men är en av de viktigaste principerna för objektorienterad systemutveckling. Man ska kunna förändra vad modulen gör utan att behöva förändra modulens kod. Nedan kommer några tekniker för att hantera OCP.
The Liskov Substitution Principle (LSP)
Varje subklass ska kunna ersättas av sin basklass. Låter trivialt och med dagens moderna programmeringsspråk något som vi som vi får nästan gratis. Vi kan tex ta vilket objekt från en subklass och lägga det i en array typad för basklassen. Inget konstigt med det. Det finns dock några saker som vi fortfarande bör ha i bakhuvudet.
The Dependency Inversion Principle (DIP)
Många tekniker använder DIP (COM, CORBA, EJB för att nämna några). Om OCP är målet som ska uppnås kan man säga att DIP är den primära metoden att nå målet. DIP säger att man ska använda interface eller abstrakta klasser i sina gränsnitt i stället för att anropa de mer rörliga konkrekta klasserna direkt. Om man använder de konkreta klasserna är det svårt att ändra den underliggande funktionalliteten utan att behöva ge sig in och ändra i redan fungerande kod. Om det skulle uppstå en situation tex av licensskäl som innebär att du måste byta ut en existerande modul så ska du inte behöva ändra i din existerande kod utan bara plugga in den nya modulen mot det existerande gränssnittet. Genom att använda tex en Abstract Factory kan du dessutom i runtime bestämma vilket implementation av interfacet som ska användas.
Det stora motivet för DIP är att skydda sig mot förändringar och jag tycker nog att man bör skriva så många klasser som möjligt mot interface, men inte alla. Där man kan lita på att det inte kommer att uppstå förändringar är det naturligtvis inte nödvändigt att förvänta sig förändring. Problemet är att det inte är så mycket man kan lita på här i världen. För en .Net-utvecklare kanske det verkar onödigt att dölja hanteringen av sökvägar bakom ett interface för att de alltid har börjat med C: men nu med Mono kanske det inte är så självklart längre.
The Interface Segregation Principle (ISP)
Tänk att du har en webtjänst med tio metoder. Denna webtjänst används av tre andra applikationer (klienter). Om du nu måste ändra i någon metod för att stödja ett nytt behov hos en av klienterna så måste du ändra på de andra två klienterna också för att de alla ser samma gränssnitt mot tjänsten. Ingen rolig tanke tycker i alla fall jag. En bättre metod hade varit att dölja din webtjänst bakom tre stycken gränssnitt som kan förändras oberoende av varandra. Om en ändring sker i webtjänsten så kommer den bara att synas för de som får tjänsten publicerad genom sitt klientgränssnitt.
Ruttnade kod
Jag har inte varit med i något projekt där man inte har försökt att utgå från de bästa av principer och sett fram emot hur bra allt ska bli. Hur alla problem man har haft i tidigare projekt ska lösas och man ska göra allt rätt en gång för alla. Men när projektet har pågått ett tag så har de rosafluffiga drömmarna gått i kras och projektet är samma gamla ruttna soppa som de alltid blir. Man tycker att man borde ha lärt sig. En liten tröst i sammanhanget kan vara att de flesta utvecklingsprojekt tycks gå samma väga. Goda intentioner som malts sönder och samman. Bob tar upp fyra symtom på ruttnande design:
- Stelhet inträffar när varje förändring tycks sluta i att man har skrivit om halva applikationen. En förändring kräver en förändring någon annanstans som i sin tur behöver ytterligare en uppsättning med förändringar. När man ser att även små förändringar får veckor att försvinna vill man inte gärna ändra ens när det behövs.
- Skörhet inträffar när man utfört förändringar och man står inför en applikation som inte längre fungerar som den ska. Tillsynes helt orelaterade moduler slutar fungera pga ändringen. Vem vågar ändra när man vet att buggrapporterna kommer att hagla så fort man tagit applikationen i drift. Varje patch man gör tycks bara leda till fler problem än de fixade.
- Orörlighet inträffar när man inte kan återanvända existerande kod trots att specifikationen borde passa som hand i handske. Det beror ofta på att modulen man ska använda har en stor ryggsäck i form av beroenden åt alla håll och kanter vars vikt överträffar vinsten i att återanvända den existerande funktionalliteten.
- Tröghet kommer i två varianter, från design/arktektur och från utvecklingsmiljön. När trögheten kommer av design har man gjort vägval som kräver stora insatser om man ska göra "rätt". Då blir det ofta lätt att skriva ett hack eller två. Tröghet från utvecklingsmiljön kan vara långa kompileringstider. Då kan det tex vara frestande att checka in kod utan att ha kompilerat först. Dumt men frestande.
Lösningen för att hantera förändrade krav är att hantera de beroenden som finns i koden. Nedan kommer några av de principer som föreslås i dokumentet.
Open closed principle (OCP)
Alla "moduler" ska vara öppna för att byggas ut men stängda för att modifieras. Låter lite motsägelsefullt men är en av de viktigaste principerna för objektorienterad systemutveckling. Man ska kunna förändra vad modulen gör utan att behöva förändra modulens kod. Nedan kommer några tekniker för att hantera OCP.
- Dynamic polymorphism - Går kort och gott ut på att man ska definiera ett gränssnitt, tex ett interface som man sedan kan lägga till implementationer som löser de specifika uppgifter som olika omständigheter.
- Static polymorphism - Genom användning av templates eller generics kan man bygga ut funktionalliteten utan att behöva röra den existerande koden (exemplet i pappret känns lite gammalt och jag ser dem mer eller mindre som likadana).
The Liskov Substitution Principle (LSP)
Varje subklass ska kunna ersättas av sin basklass. Låter trivialt och med dagens moderna programmeringsspråk något som vi som vi får nästan gratis. Vi kan tex ta vilket objekt från en subklass och lägga det i en array typad för basklassen. Inget konstigt med det. Det finns dock några saker som vi fortfarande bör ha i bakhuvudet.
- Cirkel/Oval dilemmat - Jag översätter detta till det något mer lätthanterliga Rektangel/Kvadrat. I skolan fick jag lära mig att en kvadrat är ett specialfall av en rektangel vars höjd och bredd råkar vara lika långa. Men så enkelt är det inte i programmeringens underbara värld. Om vi väljer att låta kvadrat vara en subklass till rektangel tvingar vi på en metod som en kvadrat inte har användning för. Höjd och bredd på en kvadrat är oviktigt, en längd på en sida räcker. Så för att kunna ha kvadraten som en subklass till rektangeln måste vi fuska till kvadraten så att om vi sätter höjden så kopierar vi det till bredden så att de alltid har samma värde. Men alternativet är inte mycket roligare. Om vi använder kvadraten som basklass kan vi inte manipulera rektangeln utan att behöva kolla vilken typ av objekt vi har och sedan kasta den till lämplig subtyp som vi sedan kan manipulera. Att ha rektangeln som basklass är lämpligare än kvadraten i detta fall. Nu tror vi att vi har tänkt på allt men betänk följande testmetod:
public testSquare(){
Rectangle rectangle = new Square();
rectangle.setHeight(5);
rectangle.setWidth(3);
AssertEquals(rectangle.getHeight(), 5);
AssertEquals(rectangle.getWidth(), 3);
}
Hur ska testet gå igenom om vi dolt baken kulisserna kopierar bredden till höjden på kvadraten för att lösa vår arvsheariki?
The Dependency Inversion Principle (DIP)
Många tekniker använder DIP (COM, CORBA, EJB för att nämna några). Om OCP är målet som ska uppnås kan man säga att DIP är den primära metoden att nå målet. DIP säger att man ska använda interface eller abstrakta klasser i sina gränsnitt i stället för att anropa de mer rörliga konkrekta klasserna direkt. Om man använder de konkreta klasserna är det svårt att ändra den underliggande funktionalliteten utan att behöva ge sig in och ändra i redan fungerande kod. Om det skulle uppstå en situation tex av licensskäl som innebär att du måste byta ut en existerande modul så ska du inte behöva ändra i din existerande kod utan bara plugga in den nya modulen mot det existerande gränssnittet. Genom att använda tex en Abstract Factory kan du dessutom i runtime bestämma vilket implementation av interfacet som ska användas.
Det stora motivet för DIP är att skydda sig mot förändringar och jag tycker nog att man bör skriva så många klasser som möjligt mot interface, men inte alla. Där man kan lita på att det inte kommer att uppstå förändringar är det naturligtvis inte nödvändigt att förvänta sig förändring. Problemet är att det inte är så mycket man kan lita på här i världen. För en .Net-utvecklare kanske det verkar onödigt att dölja hanteringen av sökvägar bakom ett interface för att de alltid har börjat med C: men nu med Mono kanske det inte är så självklart längre.
The Interface Segregation Principle (ISP)
Tänk att du har en webtjänst med tio metoder. Denna webtjänst används av tre andra applikationer (klienter). Om du nu måste ändra i någon metod för att stödja ett nytt behov hos en av klienterna så måste du ändra på de andra två klienterna också för att de alla ser samma gränssnitt mot tjänsten. Ingen rolig tanke tycker i alla fall jag. En bättre metod hade varit att dölja din webtjänst bakom tre stycken gränssnitt som kan förändras oberoende av varandra. Om en ändring sker i webtjänsten så kommer den bara att synas för de som får tjänsten publicerad genom sitt klientgränssnitt.
söndag 15 februari 2009
T1-T9
Kanske borde dela upp denna men det är de sista reglerna. De berör testning och ger råd om hur man ska förhålla sig till dem. Jag får skriva detta ganska närma vad som står i boken då min personliga erfarenhet med att jobba med tester inte är så hög jag skulle önska. Tyvärr är tester något som projektledare tror att det är okej att strunta i tron om att vi programmerare skulle leverera mera fungerande kod som kan tas i produktion.
T1: Otillräkliga tester
Det är en fråga som det är väldigt enkelt att ge ett enkelt svar på. Om det existerar vägar genom koden som inte har utforskats med hjälp av tester vet du inte att din kod fungerar som den ska. Framför allt vet inte andra utvecklare att de måste ta upp en webapplikation, skriva i sökfältet, dubbelklicka på listan och sedan ändra i postnumret och sedan spara för att han/hon ska se att just den funktionen fortfarande fungerar efter de sista ändringarna. Så även om du vet hur du ser att funktionen fungerar så är testerna även en specifikation hur funktionen ska fungera.
T2: Använd test-täcknings verktyg
Tog upp lite av detta på T1 men vi människor är inte gjorda för vissa typer av uppgifter. Använd därför ett verktyg för att påvisa vad som är testat och viktigare, vad som inte är testat.
T3: Strunta inte i triviala tester
De är enkla att skriva och värdet i form av dokumentation av hur de ska fungera är ovärderligt.
T4: Ett ignorerat test är en fråga om tveksamheter
Om du inte har skrivit ett test runt en funktion visar du att du inte har förstått hur funktionen ska fungera och lämnar dessutom de andra utvecklarna med ett stort frågetecken om hur funktionen ska uppföra sig.
T5: Testa gränsfall
Vi har gått igenom gränsfall tidigare. Att testa att gränsfallen fungerar är extra viktigt. Att metoden fungerar i de flesta fall är inte tillräkligt.
T6: Testa mer runt buggar
Det finns ett ordspråk som säger att en olycka sällan är ensam. Det samma gäller för utvecklare. En trött utvecklare som har missförstått något har förmodligen inte bara gjort ett fel. Skriv inte bara ett test för att verifiera buggens existens. Undersök testtäckningen på näraliggande kod och se till att den är testad. Det är ett betydligt effektivare sätt att upptäcka fel än att invänta nästa bugg och åter igen sätta sig in i vad det är koden antas leverera.
T7: Se mönster för varför tester falerar
Om du har ett stort antal tester kan man ofta se mer mönster som man inte har förstått att man måste testa för. Tex kanske man har en begränsning på hur lång en textsträng får vara som man inte var medveten om när man skrev testerna initialt.
T8: Testtäckning påvisar mönster
Varför man testat viss kod men inte annan kod kan visa på intressanta mönster hur utvecklarna tänker. Att viss kod kanske anses vara så stabil att man inte behöver testa på den kan vara en källa till att andra tester falerar. För man vet ju trots allt inte att koden fungerar.
T9: Tester ska vara snabba
Detta är nog den viktigaste reglen för testerna. Om inte testerna är snabba är det ingen som vill köra dem. Det är också den enskilt viktigaste anledningen till varför man inte ska använda xUnit (jUnut, nUnit etc) till att skriva integationstester som springer igenom hela applikationen. Desto mindre varje enskilt test testar desto snabbare är det och desto mindre är risken att någon utvecklare tröttnar på att vänta på dem och slutar att köra testerna.
T1: Otillräkliga tester
Det är en fråga som det är väldigt enkelt att ge ett enkelt svar på. Om det existerar vägar genom koden som inte har utforskats med hjälp av tester vet du inte att din kod fungerar som den ska. Framför allt vet inte andra utvecklare att de måste ta upp en webapplikation, skriva i sökfältet, dubbelklicka på listan och sedan ändra i postnumret och sedan spara för att han/hon ska se att just den funktionen fortfarande fungerar efter de sista ändringarna. Så även om du vet hur du ser att funktionen fungerar så är testerna även en specifikation hur funktionen ska fungera.
T2: Använd test-täcknings verktyg
Tog upp lite av detta på T1 men vi människor är inte gjorda för vissa typer av uppgifter. Använd därför ett verktyg för att påvisa vad som är testat och viktigare, vad som inte är testat.
T3: Strunta inte i triviala tester
De är enkla att skriva och värdet i form av dokumentation av hur de ska fungera är ovärderligt.
T4: Ett ignorerat test är en fråga om tveksamheter
Om du inte har skrivit ett test runt en funktion visar du att du inte har förstått hur funktionen ska fungera och lämnar dessutom de andra utvecklarna med ett stort frågetecken om hur funktionen ska uppföra sig.
T5: Testa gränsfall
Vi har gått igenom gränsfall tidigare. Att testa att gränsfallen fungerar är extra viktigt. Att metoden fungerar i de flesta fall är inte tillräkligt.
T6: Testa mer runt buggar
Det finns ett ordspråk som säger att en olycka sällan är ensam. Det samma gäller för utvecklare. En trött utvecklare som har missförstått något har förmodligen inte bara gjort ett fel. Skriv inte bara ett test för att verifiera buggens existens. Undersök testtäckningen på näraliggande kod och se till att den är testad. Det är ett betydligt effektivare sätt att upptäcka fel än att invänta nästa bugg och åter igen sätta sig in i vad det är koden antas leverera.
T7: Se mönster för varför tester falerar
Om du har ett stort antal tester kan man ofta se mer mönster som man inte har förstått att man måste testa för. Tex kanske man har en begränsning på hur lång en textsträng får vara som man inte var medveten om när man skrev testerna initialt.
T8: Testtäckning påvisar mönster
Varför man testat viss kod men inte annan kod kan visa på intressanta mönster hur utvecklarna tänker. Att viss kod kanske anses vara så stabil att man inte behöver testa på den kan vara en källa till att andra tester falerar. För man vet ju trots allt inte att koden fungerar.
T9: Tester ska vara snabba
Detta är nog den viktigaste reglen för testerna. Om inte testerna är snabba är det ingen som vill köra dem. Det är också den enskilt viktigaste anledningen till varför man inte ska använda xUnit (jUnut, nUnit etc) till att skriva integationstester som springer igenom hela applikationen. Desto mindre varje enskilt test testar desto snabbare är det och desto mindre är risken att någon utvecklare tröttnar på att vänta på dem och slutar att köra testerna.
lördag 14 februari 2009
N1 - N7
Nu när vi är klara med de generella reglerna så tittar vi på de som rör namnsättning.
N1: Använd beskrivande namn
Vi har alla sett kod som ser ut ungefär så här:
public void doCalculation(){
i = j + k;
}
Det är kod där man kan fråga sig vad för beräkning doCalculation gör, letar upp den sista siffran i PI? Variablerna i, j och k säger inte heller så mycket om varför de ska finnas. Bättre då hade varit att skriva metoden så här (om nu metoden skulle beräkna löner)
public void calculateTotalSalary(){
totalSalary = baseSalary + overTime;
}
Här behöver ingen fundera på vad vare sig variablerna eller metoden har för syfte.
N2: Välj rätt namn på rätt abstraktionsnivå
Denna är lite lurig. När ska man kalla ett telefonnummer för telefonnummer och när är det en kopplingssträng eller riktnummer? Att fundera på hur abstrakt namnsättningen ska vara är oerhört viktigt då det begränsar fantasin på läsaren om vad en funktion kan ha för användningsområde. Kopplingssträng skulle kunna utnyttja vilken tjänst som helst (Skype, telefon eller två burkar med snöremellan. Telefonnummer har begränsat användningsområdet till bara telefonnummer. Ibland är det rätt att använda väldigt specifika namn på saker och ting och ibland är det bra att vara öppen för att det kan komma andra implementationer än den man ska skriva just nu. Att välja rätt är inte enkelt men oerhört viktigt. Fundera ett varv extra på varje namn om det är tillräkligt generellt eller inte.
N3: Använd standardiserad vokabulär
Att använda ord för företeelser som alla känner igen är viktigt i alla språk. Kebab med deg kanske går att få till pizza med lite fantasi men det hade varit lättare att förstå om jag sagt pizza direkt. Om det heter webcontroll eller taglib är i sig självt oviktigt. Vad de som ska läsa din kod känner till det som är däremot viktigt. Om man pratar om en AbstractFactory se då till att det är definitionen från GOF du hänvisar till för det är sannolikt den som de flesta känner till. Om du gör ett projekt för styrning av mjölkmaskiner se då till att du använder ord från den domänen i stället för att hitta på egna uttryck. I stora projekt kan man kanske tillåta sig att ha en uppsättning med egna uttryck för ett antal företeelser som är specifika för projeket men i övrigt använd uttryck som alla kan googla definitionen på.
N4: Otvetydiga namn
När man döper saker och ting ska man vara nogrann med att metoden inte kan tolkas till att den gör något annat än den gör. Metodnamn som uttrycker sig i svepande ordalag, tex rename säger inte så mycket om vad det är som ska döpas om eller hur objektet ska döpas om. Så var tydlig.
N5: Använd långa namn på stora "scope"
Att använda en "i" som ett variabelnamn kan vara ok för väldigt korta metoder där betydelsen av "i" kan framgå tydligt ändå. Men i större sammanhang behöver man längre mer beskrivande namn som klarar av att beskriva den komplexitet som kommer med större sammanhang.
N6: Koda inte koden
Detta är min favorit sedan jag började programmera i .Net-miljö där standarden (i alla fall på min arbetsplats) är att skriva variabler med m_ och s_ och prefixa interface med I. För mig är detta Ungersk kodning som är precis lika illa som btnMyTextStr. Om man nödvändigtvis måste koda sina variabler gör det som ett suffix och så långt bort från de publika gränssnitten som möjligt, tex på ett interface Company där den implementerande metoden skulle kunna kodas som CompanyImpl. Fast det bästa är om man kan ge riktiga namn utan att behöva lägga på koder. Unvik den Ungerska sjukan med all kraft.
N7: Namn ska beskriva sidoeffekter
Har ni stött på en metod som ser ut något i still med detta:
public void setName(String name);
Men i stället för att bara sätta ett namn så sparas objektet också i databasen. Om du måste göra metoder som gör två saker, se då till att det framgår av namnet. SetNameAndSave skulle ha varit ett bättre namn som inte ger utvecklarna några överraskningar.
N1: Använd beskrivande namn
Vi har alla sett kod som ser ut ungefär så här:
public void doCalculation(){
i = j + k;
}
Det är kod där man kan fråga sig vad för beräkning doCalculation gör, letar upp den sista siffran i PI? Variablerna i, j och k säger inte heller så mycket om varför de ska finnas. Bättre då hade varit att skriva metoden så här (om nu metoden skulle beräkna löner)
public void calculateTotalSalary(){
totalSalary = baseSalary + overTime;
}
Här behöver ingen fundera på vad vare sig variablerna eller metoden har för syfte.
N2: Välj rätt namn på rätt abstraktionsnivå
Denna är lite lurig. När ska man kalla ett telefonnummer för telefonnummer och när är det en kopplingssträng eller riktnummer? Att fundera på hur abstrakt namnsättningen ska vara är oerhört viktigt då det begränsar fantasin på läsaren om vad en funktion kan ha för användningsområde. Kopplingssträng skulle kunna utnyttja vilken tjänst som helst (Skype, telefon eller två burkar med snöremellan. Telefonnummer har begränsat användningsområdet till bara telefonnummer. Ibland är det rätt att använda väldigt specifika namn på saker och ting och ibland är det bra att vara öppen för att det kan komma andra implementationer än den man ska skriva just nu. Att välja rätt är inte enkelt men oerhört viktigt. Fundera ett varv extra på varje namn om det är tillräkligt generellt eller inte.
N3: Använd standardiserad vokabulär
Att använda ord för företeelser som alla känner igen är viktigt i alla språk. Kebab med deg kanske går att få till pizza med lite fantasi men det hade varit lättare att förstå om jag sagt pizza direkt. Om det heter webcontroll eller taglib är i sig självt oviktigt. Vad de som ska läsa din kod känner till det som är däremot viktigt. Om man pratar om en AbstractFactory se då till att det är definitionen från GOF du hänvisar till för det är sannolikt den som de flesta känner till. Om du gör ett projekt för styrning av mjölkmaskiner se då till att du använder ord från den domänen i stället för att hitta på egna uttryck. I stora projekt kan man kanske tillåta sig att ha en uppsättning med egna uttryck för ett antal företeelser som är specifika för projeket men i övrigt använd uttryck som alla kan googla definitionen på.
N4: Otvetydiga namn
När man döper saker och ting ska man vara nogrann med att metoden inte kan tolkas till att den gör något annat än den gör. Metodnamn som uttrycker sig i svepande ordalag, tex rename säger inte så mycket om vad det är som ska döpas om eller hur objektet ska döpas om. Så var tydlig.
N5: Använd långa namn på stora "scope"
Att använda en "i" som ett variabelnamn kan vara ok för väldigt korta metoder där betydelsen av "i" kan framgå tydligt ändå. Men i större sammanhang behöver man längre mer beskrivande namn som klarar av att beskriva den komplexitet som kommer med större sammanhang.
N6: Koda inte koden
Detta är min favorit sedan jag började programmera i .Net-miljö där standarden (i alla fall på min arbetsplats) är att skriva variabler med m_ och s_ och prefixa interface med I. För mig är detta Ungersk kodning som är precis lika illa som btnMyTextStr. Om man nödvändigtvis måste koda sina variabler gör det som ett suffix och så långt bort från de publika gränssnitten som möjligt, tex på ett interface Company där den implementerande metoden skulle kunna kodas som CompanyImpl. Fast det bästa är om man kan ge riktiga namn utan att behöva lägga på koder. Unvik den Ungerska sjukan med all kraft.
N7: Namn ska beskriva sidoeffekter
Har ni stött på en metod som ser ut något i still med detta:
public void setName(String name);
Men i stället för att bara sätta ett namn så sparas objektet också i databasen. Om du måste göra metoder som gör två saker, se då till att det framgår av namnet. SetNameAndSave skulle ha varit ett bättre namn som inte ger utvecklarna några överraskningar.
G31-G36
Nu avslutar vi de generella reglerna (där av G). Det har varit tungt att ta sig igenom dem. Det som återstår efter detta inlägg är namnsättning och tester (kanske javareglerna också men de känns lite för specifika för min smak).
G31: Dolt beroende av tid/ordning
Ofta är metodanrop beroende av den ordning de anropas. Att döpa metoderna till first, second och third är kanske inte riktigt idealiskt för att hjälpa läsaren att förstå vilken ordning som anropen måste göras. Dessutom är det inget som faktiskt hindrar en programmerare från att anropa second före first. Bättre att first returnerar ett objekt av någon typ som second kan använda och att second i sin tur returnerar ett objekt som thrid tar som argument. Dessutom behöver man inte hänvisa till någon konvention (G27) utan tvingar användaren att följa anropsordningen, eller något som i alla fall tvingar den anropande klassen att se till att ha sina saker i ordning.
G32: Var inte godtycklig
Ha en anledning till varför du strukturerar din kod på ett speciellt sätt och se sedan till att koden på ett tydligt sätt reflekterar den strukturen. Om du i kod lyckas komunicera varför dina val är som de är kommer andra utvecklare följa ditt exempel. Om din strukturen är otydlig och svävande kommer känna att de kan göra på andra och för dem bättre sätt.
G33: Kapsla in gränsfall
Gränsfall är svåra att hålla koll på. Se därför till att de enbart hanteras på ett ställe och inte sprider sig som en pest genom koden. Ett enkelt exempel är en koll där första objektet i en lista inte ska kolla om det finns något tidigare objekt i listan. Siffran noll som värde på att det är den första ska vara dold bakom en variabel tex döpt till firstObject (G25). Metoden som faktiskt vet att det är skillnad på första och andra objektet i listan bör också bara finnas på ett ställe. Övrig kod ska inte behöva veta att det är skillnad i hanteringen, det är en implementationsdetalj.
G34: Funktioner går enbart en abstraktionsnivå ner
Denna har en del med G6 att göra och jag återanvänder bilexemplet. Bilen står still i en korsning och ska svänga vänster. Metoden för att hantera denna situaion ska öka farten och styra åt vänster och sedan när tillräklig riktningsförändring skett sluta svänga. Det är alltså tre anrop till tre andra metoder som tar hand om detaljerna med att bestämma bränsleblandning, mängd bränsle som ska sprutas in i cylindrarna och annat som faktiskt behövs för att bilen ska fungera. En enkel regel för att se när man går ner en abstraktionsnivå i koden är att man använder val eller snurror. Det som händer inne i valet eller snurran är oftast på en annan abstraktionsnivå och hör där med hemma i en egen metod och håller där med den första metoden kvar på "sin" abstraktionsnivå. Detta tillsammans med G30 gör det betydligt svårare att skriva komplicerad kod.
G35: Håll konfigurerbart data på hög abstraktionsnivå
Konfiguration är viktigt, att gömma den typen av information långt ner är inte helt lyckat. Den ska vara enkel att hitta och enkel att förändra. Att gömma konfigurerbart data där det faktiskt används innebär att läsaren får leta länge efter hur man ändrar tex en ipadress till en webservice.
G36: Undvik beroenden på beroende
I normalfallet vill vi att vår kod ska känna till så lite som möjligt om annan kod. Betänk följande metod i en klass:
public void aMethod(){
a.getB().getC.doSomething()
}
Den här klassen har en medlemsvariabel som heter a. Vår klass känner till att a har en metod som heter getB och det är helt normalt. Men att klassen känner till att det som returneras från getB() också har en metod som heter getC() och att man på den kan anropa doSomething() är ganska illa. Varför det är dåligt. Om man nu skulle vilja förändra getA() så att den i stället för att returnera ett objekt som har en metod som heter getB() returnerar ett objekt som inte har den metoden så måste du även ändra på alla ställen där man gjort en anropskedja som ovan. Hur löser man problemet då? I första steget använder vi G19 (förklarande variabler) så att det går att se vad getA() och getB() och getC() returnerar för något. Nästa steg är att undvika duplicering och bryta ut anropskedjan så att den bara existerar på ett ställe. Det tredje steget är lite svårare men det går ut på är att steg för steg korta ner kedjan genom att i stället för att anropa getA() anropa doSomething() i som i sin tur anropar den del av kedjan som är kvar och sedan upprepar man tills kedjan inte längre existerar. Ser att texten blir knölig att läsa så jag skriver ett kodexempel:
// i vår klass
public void aMethod()
a.doSomething();
}
// i klassen som som objektet a kommer ifrån
public void doSomething(){
b.getC().doSomething();
}
Här har jag tagit bort ett steg ur anropskedjan i första exemplet. Det kan dock bli många metoder i en del klasser om de ska ta hand om anrop den här vägen så det kan kan ibland vara idé att implementera speciella klasser som får vara en punkt i applikationen som kanske har lite mer kunskap om ett antal klasser än de borde ha.
G31: Dolt beroende av tid/ordning
Ofta är metodanrop beroende av den ordning de anropas. Att döpa metoderna till first, second och third är kanske inte riktigt idealiskt för att hjälpa läsaren att förstå vilken ordning som anropen måste göras. Dessutom är det inget som faktiskt hindrar en programmerare från att anropa second före first. Bättre att first returnerar ett objekt av någon typ som second kan använda och att second i sin tur returnerar ett objekt som thrid tar som argument. Dessutom behöver man inte hänvisa till någon konvention (G27) utan tvingar användaren att följa anropsordningen, eller något som i alla fall tvingar den anropande klassen att se till att ha sina saker i ordning.
G32: Var inte godtycklig
Ha en anledning till varför du strukturerar din kod på ett speciellt sätt och se sedan till att koden på ett tydligt sätt reflekterar den strukturen. Om du i kod lyckas komunicera varför dina val är som de är kommer andra utvecklare följa ditt exempel. Om din strukturen är otydlig och svävande kommer känna att de kan göra på andra och för dem bättre sätt.
G33: Kapsla in gränsfall
Gränsfall är svåra att hålla koll på. Se därför till att de enbart hanteras på ett ställe och inte sprider sig som en pest genom koden. Ett enkelt exempel är en koll där första objektet i en lista inte ska kolla om det finns något tidigare objekt i listan. Siffran noll som värde på att det är den första ska vara dold bakom en variabel tex döpt till firstObject (G25). Metoden som faktiskt vet att det är skillnad på första och andra objektet i listan bör också bara finnas på ett ställe. Övrig kod ska inte behöva veta att det är skillnad i hanteringen, det är en implementationsdetalj.
G34: Funktioner går enbart en abstraktionsnivå ner
Denna har en del med G6 att göra och jag återanvänder bilexemplet. Bilen står still i en korsning och ska svänga vänster. Metoden för att hantera denna situaion ska öka farten och styra åt vänster och sedan när tillräklig riktningsförändring skett sluta svänga. Det är alltså tre anrop till tre andra metoder som tar hand om detaljerna med att bestämma bränsleblandning, mängd bränsle som ska sprutas in i cylindrarna och annat som faktiskt behövs för att bilen ska fungera. En enkel regel för att se när man går ner en abstraktionsnivå i koden är att man använder val eller snurror. Det som händer inne i valet eller snurran är oftast på en annan abstraktionsnivå och hör där med hemma i en egen metod och håller där med den första metoden kvar på "sin" abstraktionsnivå. Detta tillsammans med G30 gör det betydligt svårare att skriva komplicerad kod.
G35: Håll konfigurerbart data på hög abstraktionsnivå
Konfiguration är viktigt, att gömma den typen av information långt ner är inte helt lyckat. Den ska vara enkel att hitta och enkel att förändra. Att gömma konfigurerbart data där det faktiskt används innebär att läsaren får leta länge efter hur man ändrar tex en ipadress till en webservice.
G36: Undvik beroenden på beroende
I normalfallet vill vi att vår kod ska känna till så lite som möjligt om annan kod. Betänk följande metod i en klass:
public void aMethod(){
a.getB().getC.doSomething()
}
Den här klassen har en medlemsvariabel som heter a. Vår klass känner till att a har en metod som heter getB och det är helt normalt. Men att klassen känner till att det som returneras från getB() också har en metod som heter getC() och att man på den kan anropa doSomething() är ganska illa. Varför det är dåligt. Om man nu skulle vilja förändra getA() så att den i stället för att returnera ett objekt som har en metod som heter getB() returnerar ett objekt som inte har den metoden så måste du även ändra på alla ställen där man gjort en anropskedja som ovan. Hur löser man problemet då? I första steget använder vi G19 (förklarande variabler) så att det går att se vad getA() och getB() och getC() returnerar för något. Nästa steg är att undvika duplicering och bryta ut anropskedjan så att den bara existerar på ett ställe. Det tredje steget är lite svårare men det går ut på är att steg för steg korta ner kedjan genom att i stället för att anropa getA() anropa doSomething() i som i sin tur anropar den del av kedjan som är kvar och sedan upprepar man tills kedjan inte längre existerar. Ser att texten blir knölig att läsa så jag skriver ett kodexempel:
// i vår klass
public void aMethod()
a.doSomething();
}
// i klassen som som objektet a kommer ifrån
public void doSomething(){
b.getC().doSomething();
}
Här har jag tagit bort ett steg ur anropskedjan i första exemplet. Det kan dock bli många metoder i en del klasser om de ska ta hand om anrop den här vägen så det kan kan ibland vara idé att implementera speciella klasser som får vara en punkt i applikationen som kanske har lite mer kunskap om ett antal klasser än de borde ha.
Prenumerera på:
Inlägg (Atom)