Refaktorisering i praktiken Aron Kornhall D00, Lunds Tekniska Högskola d00ak@efd.lth.se 24 Februari 2004
Abstract Den här artikeln behandlar refaktorisering ur ett praktiskt perspektiv och riktar sig till programmerare som inte har någon större vana av refaktorisering sedan innan. Några vanliga och tämligen enkla refaktoriseringar beskrivs och exemplifieras tillsammans med någon enstaka mer komplicerad. Vidare ges några mönster för refaktorisering, speciellt för refaktorisering till designmönster. Slutligen testas och recenseras ett antal refaktoriseringsverktyg för Java. Innehåll Innehåll... 2 1. Inledning... 3 2. Allmänt om refaktorisering... 3 3. Att refaktorisera till Designmönster... 3 4. Mönster för refaktorisering:... 4 4.1 Mönster: Högre kodkvalitet med refaktorisering... 4 4.2 Mönster: Mönsterlösning... 4 4.3 Mönster: Mönstermedborgaren... 5 4.4 Mönster: Säker refaktorisering... 5 5. Några vanliga refaktoriseringar... 6 5.1 Rename symbol [Fowler04]... 6 5.2 Remove double negative [Fowler04]... 7 5.3 Extract method [Fowler04]... 7 5.4 Subclassing to Decorator... 8 6. Refaktoriseringsverktyg... 12 6.1 Eclipse... 12 6.2 RefactorIT... 12 6.3 JRefactory... 13 7. Sammanfattning... 14 8. Tack till... 14 9. Referenser... 14 2
1. Inledning En definition på refaktorisering är [TDDN01]: Förändring av kod med bibehållen funktionalitet. Definitionen lämnar alltså en hel del frågor obesvarade: Varför man refaktoriserar man? När bör man refaktorisera? Blir resultatet av en refaktorisering bättre än dess föregångare? Hur bär man sig åt? etc. Det är min tanke att den här artikeln skall belysa de praktiska aspekterna på refaktorisering snarare än de teoretiska. Jag kommer att ta upp och ge exempel på några vanliga typer av refaktorisering som jag tycker är intressanta. Dessa skall inte ses som något försök till en lista över de bästa refaktoriseringarna utan snarare som ett antal smakprov på hur några olika typer av refaktorisering kan se ut. 2. Allmänt om refaktorisering En väl utförd refaktorisering kan lösa mängder av olika typer av problem, medan en dåligt utförd sådan kan ställa till med minst lika många. Refaktorisering är alltså inte något som per automatik ger ett bra resultat. Syftet med en refaktorisering kan som tidigare antytts variera, men vanligast är nog att man vill göra designen bättre och underlätta läsbarheten av koden. Andra motiv kan vara att förbättra konfigurerbarheten eller optimera koden för att snabba upp exekveringen. Oavsett vad man vill åstadkomma finns det dock alltid en risk för att man introducerar nya buggar i koden när man refaktoriserar, hur försiktig man än är. Det bästa sättet att komma tillrätta med detta problem är att se till att ha många och välskrivna testfall som kontrollerar att koden verkligen fungerar som den ska, även efter refaktoriseringen. 3. Att refaktorisera till Designmönster Designmönster är en företeelse som alla programmerare förr eller senare kommer i kontakt med vare sig de vill eller inte. Det stora flertalet av oss är dessutom eniga om att det är ett enkelt och förhållandevis smärtfritt sätt att återanvända fiffiga designlösningar och därmed undvika att uppfinna hjulet på nytt varje gång man behöver lösa ett programmeringsproblem. Att uppfinna hjul är inte heller det vi vill lägga vår energi på när vi refaktoriserar, så visst vore det trevligt om man kunde använda sig av designmönster även när man refaktoriserar. Denna typ av refaktorisering är visserligen inte helt lätt att lyckas med och tar ofta förhållandevis lång tid att ta sig igenom, men å andra sidan får man i gengäld en väl beprövad lösning som nästan garanterat ger en vacker design som lön för allt slit. 3
4. Mönster för refaktorisering: 4.1 Mönster: Högre kodkvalitet med refaktorisering Problem: Koden är svårläst, duplicerad kod förekommer och designen är oklar. Kontext: Du programmerar. Krafter: Det kan ta emot att ändra i kod som fungerar. Man kanske inte vågar göra ändringar eftersom man är rädd att ha sönder någonting. Det upplevs ofta som roligare att skriva ny kod än att skriva om gammal. Lösning: Gör refaktorisering till en naturlig del i programmerandet. Efter varje färdigskriven metod försöker du få överblick över vad koden gör och tänk till: Vad skulle jag kunna göra för att förbättra läsbarheten och strukturen? 4.2 Mönster: Mönsterlösning Problem: Koden är svårläst, duplicerad kod förekommer och designen är oklar. Kontext: Du programmerar men tycker att koden blir allt mer svårförståelig och designen oklar. Du inser att du måste förändra designen radikalt för att kunna komma vidare. Krafter: Det kan ta emot att ändra i kod som fungerar. Man kanske inte vågar göra ändringar eftersom man är rädd att ha sönder någonting. Alla programmerare har inte den kunskap om designmönster som krävs. Att ändra designen radikalt tar ofta emot eftersom det kräver förhållandevis mycket arbete. Det kan vara svårt att inse vilket designmönster man bör använda. Det upplevs ofta som roligare att skriva ny kod än att skriva om gammal. Lösning: Se till att vara någorlunda väl inläst på åtminstone ett par vanligt förekommande designmönster. När du refaktoriserar försöker du hela tiden identifiera delar av koden som skulle kunna skrivas om som ett designmönster. När dessa delar är identifierade gör du en utvärdering av vilka fördelar respektive nackdelar det skulle innebära att skriva om koden. Tycker du att fördelarna uppväger nackdelarna är det bara att fatta tangentbordet och skriva om hela rasket så att designen överensstämmer med det aktuella mönstret. 4
Relaterade mönster: Mönstermedborgaren beskriver hur man bör bete sig när man refaktoriserar till designmönster. 4.3 Mönster: Mönstermedborgaren Problem: När man väl kommit in i designmönstersvängen tenderar man att använda sig av dem in absurdum och all kod man skriver blir tryfferad med allehanda upptänkliga och oupptänkliga mönster. Kontext: Du programmerar och har fallit för tjusningen i designmönstrens underbara värld. Krafter: Att använda många mönster imponerar på andra programmerare. Mönster innebär en viss trygghet. Man tror lätt att bara man använder sig av en massa mönster kommer resultatet att bli bra. Ofta är det bättre med en enkel lösning som är lätt att förstå än en avancerad designmönsterlösning. Lösning: Behärska dig. Det kan vara svårt ibland men är inte alls omöjligt. Det handlar helt enkelt om att väga fördelarna med en lösning mot nackdelarna. Att använda sig av mönster ger inte automagiskt ett bra resultat. En mönstermedborgare är alltså en programmerare som använder designmönster om och endast om det är motiverat. 4.4 Mönster: Säker refaktorisering Problem: Koden behöver refaktoriseras, men risken för att nya buggar uppkommer är ett problem. Kontext: Du refaktoriserar. Krafter: För att kunna bygga på systemet krävs refaktorisering. Risken för att introducera nya buggar finns alltid vid refaktorisering. Välskrivna testfall fångar upp större delen av de buggar som kan tänkas introduceras vid refaktorisering. Lösning: Innan varje refaktorisering kontrollerar man om det finns tillräckligt med tester av den kod som skall refaktoriseras. Gör det inte det skriver man nya, genomtänkta sådana enligt konstens alla regler. När alla gamla och nya tester går igenom refaktoriserar man och kör sedan alla testfall igen. Går dessa igenom har man sannolikt inte introducerat några nya buggar. 5
5. Några vanliga refaktoriseringar 5.1 Rename symbol [Fowler04] Rename symbol är nog den allra enklaste och vanligast förekommande refaktoriseringen. Som namnet antyder innebär denna refaktorisering att man byter namn på symboler dvs. variabler, metoder, klasser eller paket. Ett refaktoriseringsverktyg är till stor hjälp vid denna refaktorisering eftersom ett namnbyte ofta innebär att flera referenser måste uppdateras. Motivering Det hör till god sed att ge sina variabler, metoder mm bra namn som beskriver vilken funktion de har i koden, men ibland kan det vara svårt att komma på bra namn och då kan det bli lite si och så med namngivandet. Efter någon dag, när man sovit på saken, brukar det dock vara lättare att gå tillbaka till koden och byta ut de gamla och dåliga namnen mot nya och bra. Exempel Innan refaktorisering: public class Account { private double money; public Account() { money = 0.0; public double getmoney() { return money; public void setmoney(double b) { this.money = b; När denna klass som beskriver ett bankkonto skrevs döpte författaren variabeln som innehåller aktuellt saldo till money vilket dessvärre inte är helt lyckat eftersom det bara betyder pengar i största allmänhet. Detsamma gäller för metoderna set- och getmoney som dessutom är publika och visar på så sätt även utåt upp det dåliga variabelnamnet. Efter Refaktorisering: public class Account { private double balance; public Account() { balance = 0.0; public double getbalance() { return balance; public void setbalance(double balance) { this.balance = balance; Här har variabeln money bytt namn till balance (som ju betyder just saldo). Get- och setmetoderna har ändrats på motsvarande sätt så att vi även publikt talar om att det är saldot vi kan förändra istället för det något oklara pengar. 6
5.2 Remove double negative [Fowler04] Dubbla negationer är inte bra. Det fick vi lära oss redan på grundskolans svensklektioner, och detsamma gäller i programmeringssammanhang. Motivering Eftersom människans förmåga att parsra ett logiskt uttryck korrekt är ganska begränsad bör vi göra det så enkelt som möjligt för oss och då är det en bra idé börja med att ta bort alla dubbla negationer. Tänk t ex på meningen: Du skall inte låta bli att inte använda dubbla negationer. Visst blir det besvärligt? Exempel Innan refaktorisering:... if(!fueltank.isnotempty()) { // gör något public boolean isnotempty() { return fuel > 0; Efter Refaktorisering:... if(fueltank.isempty()) { // gör något public boolean isempty() { return!isnotempty(); public boolean isnotempty() { return fuel > 0; Här har vi lagt till en ny metod isempty() som gör negationen åt oss Enkelt och praktiskt om vi fortfarande behöver isnotempty() någon annanstans i koden. 5.3 Extract method [Fowler04] Att bryta ut ett stycke kod till en egen metod är en mycket vanlig typ av refaktorisering. Den har ett syskon vid namn Extract class som jag inte tar upp här, men det bör inte vara så svårt att föreställa sig vad den gör efter att ha läst om Extract method. Motivering Att en metod börjar bli för lång är nog en av de vanligaste dåliga lukterna inom programvaruutveckling. Lyckligtvis är denna lukt ganska lätt att bli av med med hjälp av Extract method. 7
Exempel Innan refaktorisering: public void LongMethod() { //många, långa, digra rader kod String name = ""; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.println("Hej!"); System.out.print("Vad heter du: "); try { name = br.readline(); catch (IOException e) { System.err.println(e); System.out.println("Hej " + name); //ännu fler, långa, digra rader kod Efter Refaktorisering: public void NotThatLongMethod() { //många, långa, digra rader kod System.out.println("Hej " + askforname()); //ännu fler, långa, digra rader kod private String askforname() { String name = ""; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.println("Hej!"); System.out.print("Vad heter du: "); try { name = br.readline(); catch (IOException e) { System.err.println(e); return name; Som synes blev det plötsligt betydligt lättare att se att vi frågar användaren om hans/hennes namn mitt i den långa metoden. 5.4 Subclassing to Decorator Att refakorisera till designmönster kan vara mycket effektivt, men är förhållandevis svårt eftersom det kräver en djupare förståelse för koden och måste göras i flera steg. Jag har valt att som exempel på en sådan refaktorisering visa hur man kan ersätta en hierarki av subklasser med designmönstret decorator. Decorator är ett mönster som är bra att använda om man enkelt vill kunna skapa objekt med olika egenskaper. Jag går inte igenom Decorator i detalj här men det bör inte vara så svårt att förstå principen genom att studera nedanstående kod. För den intresserade finns mer information om Decorator och designmönster i största allmänhet i en sökmotor nära dig. 8
Motivering Med hjälp av Decorator kan man skapa objekt med olika egenskaper genom att skriva en klass för varje egenskap och sedan på ett fiffigt sätt kombinera dessa klasser när objektet skapas. I en lösning där varje kombination av egenskaper representeras av en subklass växer antalet klasser exponentiellt när antalet egenskaper ökar. Innan refaktorisering: public class Person { protected String name; public Person(String name) { this.name = name; System.out.println("Namn: " + name); public class Female extends Person { public Female(String name) { super(name); System.out.println("Namn: " + name); System.out.println("Sex: female"); public class Male extends Person { public Male(String name) { super(name); System.out.println("Namn: " + name); System.out.println("Sex: male"); public class Man extends Male { public Man(String name) { super(name); System.out.println("Namn: " + name); System.out.println("Sex: male"); System.out.println("Age: Adult"); public class Woman extends Female { public Woman(String name) { super(name); 9
System.out.println("Namn: " + name); System.out.println("Sex: female"); System.out.println("Age: Adult"); public class Girl extends Female { public Girl(String name) { super(name); System.out.println("Namn: " + name); System.out.println("Sex: female"); System.out.println("Age: Not very old"); public class Boy extends Male { public Boy(String name) { super(name); System.out.println("Namn: " + name); System.out.println("Sex: male"); System.out.println("Age: Not very old"); Här vill vi kunna representera personer med alla kombinationer av egenskaperna kön och ålder. För enkelhets skull finns det bara två åldrar barn och vuxen och så har vi inte tagit hänsyn till att hermafroditer inte kan representeras i vår modell. Det behövs alltså sex subklasser till Person för att representera alla kombinationer av dessa två egenskaper. Detta kanske inte låter så farligt, men om vi antar att vi hade 10 egenskaper som alla kunde anta två värden så skulle denna artikel utskriven på papper väga ca två kg Efter Refaktorisering: public abstract class Person { public Person() { public abstract void printinformation(); public class NameHolder extends Person { protected String name; public NameHolder(String name) { this.name = name; System.out.println("Namn: " + name); 10
public abstract class AbstractDecorator extends Person { protected Person component; public AbstractDecorator(Person component) { super(); this.component = component; public abstract void printinformation(); public class SexDecorator extends AbstractDecorator { private boolean ismale; public SexDecorator(Person component, boolean ismale) { super(component); this.ismale = ismale; component.printinformation(); System.out.println("Sex: " + getsex()); private String getsex() { // (c: if(ismale) { return "male"; else { return "female"; public class AgeDecorator extends AbstractDecorator { private boolean ischild; public AgeDecorator(Person component, boolean ischild) { super(component); this.ischild = ischild; component.printinformation(); System.out.println("Age: " + getage()); private String getage() { if(ischild) { return "Not very old"; else { return "Adult"; Med den här lösningen behöver vi bara skriva en ny subklass till Decorator för varje egenskap vi lägger till. Om man sen vill skapa t.ex. en pojke som heter Erik skriver man såhär: Person p = new AgeDecorator(new SexDecorator( new NameHolder("Erik"),true),true); 11
6. Refaktoriseringsverktyg Refaktorisering blir inte riktigt njutbar utan verktygshjälp. Även om man kan göra mycket med bara sök/ersätt funktionaliteten som finns i nästan alla texteditorer så blir det aldrig lika smidigt som med ett specialskrivet verktyg. På senare år har det kommit ut en hel rad mer eller mindre bra sådana verktyg och jag kommer här att presentera tre sådana för Java. Jag har testat dessa verktyg genom att i ett större programmeringsprojekt med många välskrivna tester göra ett antal refaktoriseringar och sen kontrollera om testerna fortfarande går igenom. Denna metod är måhända inte så vetenskaplig, men ger ändå en viss uppfattning om hur bra verktyget är på att refaktorisera. Jag kommer hädanefter att kalla detta test för domedagstestet. 6.1 Eclipse Eclipse är egentligen en hel utvecklingsmiljö med stöd för bl.a. CVS och en mängd plugins för UML-ritning mm. Det är dock refaktoriseringsfunktionaliteten som jag kommer att recensera här. Så här kan det se ut när man gör Extract Method i Eclipse: figur 1 Refaktorisering i Eclipse Användargränssnittet är mycket intuitivt. Man bara markerar det man vill refaktorisera och högerklickar så får man upp en meny där man kan välja mellan en mängd olika refaktoriseringar. Eclipse klarar sig igenom domedagstestet även om vi får en del onödiga import-satser, men å andra sidan gör Eclipse oss uppmärksamma på problemet med hjälp av en liten gul markering i kanten på aktuella klasser, så det är en smal sak att rätta det felet. 6.2 RefactorIT RefactorIT är ett fristående program, men har stöd för att integreras med bl.a. JBuilder och Forte vilket är nödvändigt eftersom det inte innehåller någon egen editor. Upplägget är ganska likt det i Eclipse, men inte riktigt lika intuitivt. Extract method på samma kod som i figur 1 ser ut så här: 12
figur2 Refaktorisering i RefactorIT Även RefactorIT klarar domedagstestet och även här genereras en del onödig kod vid flyttning av klasser. Vi får ingen direkt information om detta som på samma sätt som i Eclipse, men det finns inbyggda analyseringsverktyg som hittar den typen av överflödig kod om vi använder dem. 6.3 JRefactory Både Eclipse och RefactorIT har en traditionell syn på hur refaktorisering görs där det är källkoden man ändrar och därmed ändras också designen. JRefactory har en ganska annorlunda approach på hela refaktoriseringskonceptet. Här är det designen som står i centrum och visas i form av UML-diagram, medan källkoden hålls i skymundan och faktiskt inte visas alls. Man skulle kunna säga att man refaktoriserar grafiskt. Så här kan det t.ex. se ut: figur 3 Refaktorisering i JRefactory 13
JRefactory känns lite instabilt redan från början och det är (förhållandevis) svårt att förstå hur man skall importera källkoden i programmet. Dessutom misslyckas programmet i domedagstestet så att fyra testfall inte längre går igenom. Trots detta vill jag inte såga JRefactory totalt eftersom det sina brister till trots är en intressant och nyskapande lösning som kanske i senare versioner kan bli riktigt bra, även om det i nuläget har en del irriterande buggar. 7. Sammanfattning Jag har i denna artikel efter bästa förmåga försökt belysa de praktiska aspekterna på refaktorisering. Mycket av det som finns skrivet har en mer teoretisk inriktning och vänder sig till den erfarne programmeraren som redan har så stor erfarenhet att hon direkt inser en refaktoriserings användningsområde när hon läser en beskrivning av den. Jag har istället försökt att beskriva förhållandevis enkla refaktoriseringar med förhållandevis enkla exempel och mönster samt testat ett par förhållandevis enkla refaktoriseringsverktyg och det är min förhoppning att du som läsare nu tycker att refaktorisering är förhållandevis enkelt. 8. Tack till Magnus Starseth, Miguel Ewezon, Fredrik Redgård och Staffan Åberg som alla kommit med värdefulla kommentarer på mitt arbete. 9. Referenser [Cooper98] Cooper, James W, The Design Patterns Java Companion Addison-Wesley, 1998 [TDDN01] Tichelaar, Sander; Ducasse, Stéphane; Demeyer, Serge; Niertrasz, Oscar, A Metamodel for Language-Independent Refactoring 2001 [Fowler04] Fowler, Martin, www.refactoring.com 2004 [Kerievsky02] Kerievsky, Joshua, Refactoring to Patterns 2002 14