En praktisk guide till refaktorering av befintlig kod, som täcker identifiering, prioritering, tekniker och bästa praxis för modernisering och underhåll.
Att tämja odjuret: Strategier för refaktorering av befintlig kod
Befintlig kod. Termen i sig frammanar ofta bilder av spretiga, odokumenterade system, bräckliga beroenden och en överväldigande känsla av fasa. Många utvecklare världen över står inför utmaningen att underhålla och utveckla dessa system, som ofta är kritiska för affärsverksamheten. Denna omfattande guide ger praktiska strategier för att refaktorera befintlig kod och förvandla en källa till frustration till en möjlighet för modernisering och förbättring.
Vad är befintlig kod?
Innan vi dyker in i refaktoreringstekniker är det viktigt att definiera vad vi menar med "befintlig kod". Medan termen helt enkelt kan hänvisa till äldre kod, fokuserar en mer nyanserad definition på dess underhållbarhet. Michael Feathers, i sin banbrytande bok "Working Effectively with Legacy Code", definierar befintlig kod som kod utan tester. Denna brist på tester gör det svårt att säkert modifiera koden utan att introducera regressioner. Befintlig kod kan dock också uppvisa andra egenskaper:
- Brist på dokumentation: De ursprungliga utvecklarna kan ha gått vidare och lämnat efter sig lite eller ingen dokumentation som förklarar systemets arkitektur, designbeslut eller ens grundläggande funktionalitet.
- Komplexa beroenden: Koden kan vara hårt kopplad, vilket gör det svårt att isolera och modifiera enskilda komponenter utan att påverka andra delar av systemet.
- Föråldrad teknik: Koden kan vara skriven med äldre programmeringsspråk, ramverk eller bibliotek som inte längre aktivt stöds, vilket utgör säkerhetsrisker och begränsar tillgången till moderna verktyg.
- Dålig kodkvalitet: Koden kan innehålla duplicerad kod, långa metoder och andra kodlukter som gör den svår att förstå och underhålla.
- Skör design: Tydligen små förändringar kan få oförutsedda och omfattande konsekvenser.
Det är viktigt att notera att befintlig kod inte är dålig i sig. Den representerar ofta en betydande investering och förkroppsligar värdefull domänkunskap. Målet med refaktorering är att bevara detta värde samtidigt som man förbättrar kodens underhållbarhet, tillförlitlighet och prestanda.
Varför refaktorera befintlig kod?
Att refaktorera befintlig kod kan vara en skrämmande uppgift, men fördelarna väger ofta tyngre än utmaningarna. Här är några viktiga anledningar att investera i refaktorering:
- Förbättrad underhållbarhet: Refaktorering gör koden lättare att förstå, modifiera och felsöka, vilket minskar kostnaden och ansträngningen som krävs för löpande underhåll. För globala team är detta särskilt viktigt, eftersom det minskar beroendet av specifika individer och främjar kunskapsdelning.
- Minskad teknisk skuld: Teknisk skuld avser den implicita kostnaden för omarbete som orsakas av att man väljer en enkel lösning nu istället för att använda en bättre metod som skulle ta längre tid. Refaktorering hjälper till att betala av denna skuld och förbättrar den övergripande hälsan i kodbasen.
- Förbättrad tillförlitlighet: Genom att åtgärda kodlukter och förbättra kodens struktur kan refaktorering minska risken för buggar och förbättra systemets övergripande tillförlitlighet.
- Ökad prestanda: Refaktorering kan identifiera och åtgärda prestandaflaskhalsar, vilket resulterar i snabbare exekveringstider och förbättrad responsivitet.
- Enklare integration: Refaktorering kan göra det lättare att integrera det befintliga systemet med nya system och tekniker, vilket möjliggör innovation och modernisering. Till exempel kan en europeisk e-handelsplattform behöva integreras med en ny betalningsgateway som använder ett annat API.
- Förbättrad utvecklarmoral: Att arbeta med ren, välstrukturerad kod är roligare och mer produktivt för utvecklare. Refaktorering kan höja moralen och locka talanger.
Identifiera kandidater för refaktorering
All befintlig kod behöver inte refaktoreras. Det är viktigt att prioritera refaktoreringinsatser baserat på följande faktorer:
- Ändringsfrekvens: Kod som ofta modifieras är en utmärkt kandidat för refaktorering, eftersom förbättringar i underhållbarhet kommer att ha en betydande inverkan på utvecklingsproduktiviteten.
- Komplexitet: Kod som är komplex och svår att förstå är mer benägen att innehålla buggar och är svårare att modifiera på ett säkert sätt.
- Inverkan av buggar: Kod som är kritisk för affärsverksamheten eller som har en hög risk att orsaka kostsamma fel bör prioriteras för refaktorering.
- Prestandaflaskhalsar: Kod som identifieras som en prestandaflaskhals bör refaktoreras för att förbättra prestandan.
- Kodlukter: Håll utkik efter vanliga kodlukter som långa metoder, stora klasser, duplicerad kod och "feature envy". Dessa är indikatorer på områden som kan dra nytta av refaktorering.
Exempel: Föreställ dig ett globalt logistikföretag med ett befintligt system för att hantera försändelser. Modulen som ansvarar för att beräkna fraktkostnader uppdateras ofta på grund av ändrade regler och bränslepriser. Denna modul är en utmärkt kandidat för refaktorering.
Refaktoreringstekniker
Det finns många tillgängliga refaktoreringstekniker, var och en utformad för att hantera specifika kodlukter eller förbättra specifika aspekter av koden. Här är några vanliga tekniker:
Komponera metoder
Dessa tekniker fokuserar på att bryta ner stora, komplexa metoder i mindre, mer hanterbara metoder. Detta förbättrar läsbarheten, minskar duplicering och gör koden lättare att testa.
- Extrahera metod: Detta innebär att identifiera ett kodblock som utför en specifik uppgift och flytta det till en ny metod.
- Infoga metod: Detta innebär att ersätta ett metodanrop med metodens kropp. Använd detta när en metods namn är lika tydligt som dess kropp, eller när du är på väg att använda Extrahera metod men den befintliga metoden är för kort.
- Ersätt temporär variabel med anrop: Detta innebär att ersätta en temporär variabel med ett metodanrop som beräknar variabelns värde vid behov.
- Introducera förklarande variabel: Använd detta för att tilldela resultatet av ett uttryck till en variabel med ett beskrivande namn, vilket klargör dess syfte.
Flytta funktionalitet mellan objekt
Dessa tekniker fokuserar på att förbättra designen av klasser och objekt genom att flytta ansvarsområden dit de hör hemma.
- Flytta metod: Detta innebär att flytta en metod från en klass till en annan klass där den logiskt hör hemma.
- Flytta fält: Detta innebär att flytta ett fält från en klass till en annan klass där det logiskt hör hemma.
- Extrahera klass: Detta innebär att skapa en ny klass från en sammanhängande uppsättning ansvarsområden som extraherats från en befintlig klass.
- Infoga klass: Använd detta för att slå ihop en klass med en annan när den inte längre gör tillräckligt för att motivera sin existens.
- Dölj delegat: Detta innebär att skapa metoder i servern för att dölja delegeringslogik från klienten, vilket minskar kopplingen mellan klienten och delegaten.
- Ta bort mellanhand: Om en klass delegerar nästan allt sitt arbete hjälper detta till att ta bort mellanhanden.
- Introducera främmande metod: Lägger till en metod i en klientklass för att betjäna klienten med funktioner som egentligen behövs från en serverklass, men som inte kan modifieras på grund av bristande åtkomst eller planerade ändringar i serverklassen.
- Introducera lokal utökning: Skapar en ny klass som innehåller de nya metoderna. Användbart när du inte kontrollerar källan till klassen och inte kan lägga till beteende direkt.
Organisera data
Dessa tekniker fokuserar på att förbättra hur data lagras och nås, vilket gör det lättare att förstå och modifiera.
- Ersätt datavärde med objekt: Detta innebär att ersätta ett enkelt datavärde med ett objekt som kapslar in relaterade data och beteende.
- Ändra värde till referens: Detta innebär att ändra ett värdeobjekt till ett referensobjekt, när flera objekt delar samma värde.
- Ändra enkelriktad association till dubbelriktad: Skapar en dubbelriktad länk mellan två klasser där endast en enkelriktad länk finns.
- Ändra dubbelriktad association till enkelriktad: Förenklar associationer genom att göra en dubbelriktad relation enkelriktad.
- Ersätt magiskt tal med symbolisk konstant: Detta innebär att ersätta literala värden med namngivna konstanter, vilket gör koden lättare att förstå och underhålla.
- Inkapsla fält: Tillhandahåller en getter- och setter-metod för att komma åt fältet.
- Inkapsla samling: Säkerställer att alla ändringar i samlingen sker genom noggrant kontrollerade metoder i ägarklassen.
- Ersätt post med dataklass: Skapar en ny klass med fält som matchar postens struktur och accessormetoder.
- Ersätt typkod med klass: Skapa en ny klass när typkoden har en begränsad, känd uppsättning möjliga värden.
- Ersätt typkod med subklasser: För när typkodens värde påverkar klassens beteende.
- Ersätt typkod med State/Strategy: För när typkodens värde påverkar klassens beteende, men subklasser inte är lämpligt.
- Ersätt subklass med fält: Tar bort en subklass och lägger till fält i superklassen som representerar subklassens distinkta egenskaper.
Förenkla villkorsuttryck
Villkorslogik kan snabbt bli invecklad. Dessa tekniker syftar till att klargöra och förenkla.
- Dela upp villkorssats: Detta innebär att bryta ner en komplex villkorssats i mindre, mer hanterbara delar.
- Konsolidera villkorsuttryck: Detta innebär att kombinera flera villkorssatser till en enda, mer koncis sats.
- Konsolidera duplicerade villkorsfragment: Detta innebär att flytta kod som är duplicerad i flera grenar av en villkorssats utanför villkoret.
- Ta bort kontrollflagga: Eliminera booleska variabler som används för att styra logikflödet.
- Ersätt nästlade villkor med skyddsklausuler: Gör koden mer läsbar genom att placera alla specialfall överst och stoppa bearbetningen om något av dem är sant.
- Ersätt villkorssats med polymorfism: Detta innebär att ersätta villkorslogik med polymorfism, vilket gör att olika objekt kan hantera olika fall.
- Introducera nullojekt: Istället för att kontrollera för ett null-värde, skapa ett standardobjekt som ger standardbeteende.
- Introducera försäkran: Dokumentera uttryckligen förväntningar genom att skapa ett test som kontrollerar dem.
Förenkla metodanrop
- Byt namn på metod: Detta verkar uppenbart, men är otroligt hjälpsamt för att göra koden tydlig.
- Lägg till parameter: Att lägga till information i en metodsignatur gör att metoden kan bli mer flexibel och återanvändbar.
- Ta bort parameter: Om en parameter inte används, ta bort den för att förenkla gränssnittet.
- Separera anrop från modifierare: Om en metod både ändrar och returnerar ett värde, separera den i två distinkta metoder.
- Parametrisera metod: Använd detta för att konsolidera liknande metoder till en enda metod med en parameter som varierar beteendet.
- Ersätt parameter med explicita metoder: Gör motsatsen till parametrisering - dela upp en enda metod i flera metoder som var och en representerar ett specifikt värde på parametern.
- Bevara hela objektet: Istället för att skicka några specifika dataobjekt till en metod, skicka hela objektet så att metoden har tillgång till all dess data.
- Ersätt parameter med metod: Om en metod alltid anropas med samma värde som härleds från ett fält, överväg att härleda parametervärdet inuti metoden.
- Introducera parameterobjekt: Gruppera flera parametrar i ett objekt när de naturligt hör ihop.
- Ta bort setter-metod: Undvik setters om ett fält endast ska initieras, men inte modifieras efter konstruktion.
- Dölj metod: Minska synligheten för en metod om den bara används inom en enda klass.
- Ersätt konstruktor med fabriksmetod: Ett mer beskrivande alternativ till konstruktorer.
- Ersätt undantag med test: Om undantag används som flödeskontroll, ersätt dem med villkorslogik för att förbättra prestandan.
Hantera generalisering
- Flytta upp fält: Flytta ett fält från en subklass till dess superklass.
- Flytta upp metod: Flytta en metod från en subklass till dess superklass.
- Flytta upp konstruktorkropp: Flytta kroppen av en konstruktor från en subklass till dess superklass.
- Flytta ner metod: Flytta en metod från en superklass till dess subklasser.
- Flytta ner fält: Flytta ett fält från en superklass till dess subklasser.
- Extrahera gränssnitt: Skapar ett gränssnitt från de publika metoderna i en klass.
- Extrahera superklass: Flytta gemensam funktionalitet från två klasser till en ny superklass.
- Kollapsa hierarki: Kombinera en superklass och subklass till en enda klass.
- Forma mallmetod: Skapa en mallmetod i en superklass som definierar stegen i en algoritm, vilket gör att subklasser kan åsidosätta specifika steg.
- Ersätt arv med delegering: Skapa ett fält i klassen som refererar till funktionaliteten, istället för att ärva den.
- Ersätt delegering med arv: När delegering är för komplex, byt till arv.
Dessa är bara några exempel på de många tillgängliga refaktoreringsteknikerna. Valet av vilken teknik som ska användas beror på den specifika kodlukten och det önskade resultatet.
Exempel: En stor metod i en Java-applikation som används av en global bank beräknar räntor. Genom att tillämpa Extrahera metod för att skapa mindre, mer fokuserade metoder förbättras läsbarheten och det blir lättare att uppdatera ränteberäkningslogiken utan att påverka andra delar av metoden.
Refaktoreringsprocessen
Refaktorering bör hanteras systematiskt för att minimera risker och maximera chanserna till framgång. Här är en rekommenderad process:
- Identifiera kandidater för refaktorering: Använd de tidigare nämnda kriterierna för att identifiera områden i koden som skulle dra mest nytta av refaktorering.
- Skapa tester: Innan du gör några ändringar, skriv automatiserade tester för att verifiera kodens befintliga beteende. Detta är avgörande för att säkerställa att refaktorering inte introducerar regressioner. Verktyg som JUnit (Java), pytest (Python) eller Jest (JavaScript) kan användas för att skriva enhetstester.
- Refaktorera inkrementellt: Gör små, inkrementella ändringar och kör testerna efter varje ändring. Detta gör det lättare att identifiera och åtgärda eventuella fel som introduceras.
- Committa ofta: Committa dina ändringar till versionskontroll ofta. Detta gör att du enkelt kan återgå till en tidigare version om något går fel.
- Granska koden: Låt din kod granskas av en annan utvecklare. Detta kan hjälpa till att identifiera potentiella problem och säkerställa att refaktoreringen görs korrekt.
- Övervaka prestanda: Efter refaktorering, övervaka systemets prestanda för att säkerställa att ändringarna inte har introducerat några prestandaregressioner.
Exempel: Ett team som refaktorerar en Python-modul i en global e-handelsplattform använder `pytest` för att skapa enhetstester för den befintliga funktionaliteten. De tillämpar sedan refaktoreringen Extrahera klass för att separera ansvarsområden och förbättra modulens struktur. Efter varje liten ändring kör de testerna för att säkerställa att funktionaliteten förblir oförändrad.
Strategier för att introducera tester i befintlig kod
Som Michael Feathers träffande uttryckte det är befintlig kod kod utan tester. Att introducera tester i befintliga kodbaser kan kännas som ett enormt åtagande, men det är avgörande för säker refaktorering. Här är flera strategier för att närma sig denna uppgift:
Karakteriseringstester (även kända som Golden Master-tester)
När du har att göra med kod som är svår att förstå kan karakteriseringstester hjälpa dig att fånga dess befintliga beteende innan du börjar göra ändringar. Idén är att skriva tester som försäkrar kodens nuvarande utdata för en given uppsättning indata. Dessa tester verifierar inte nödvändigtvis korrekthet; de dokumenterar helt enkelt vad koden *för närvarande* gör.
Steg:
- Identifiera en enhet kod du vill karakterisera (t.ex. en funktion eller metod).
- Skapa en uppsättning indatavärden som representerar ett spektrum av vanliga och udda scenarier.
- Kör koden med dessa indata och fånga de resulterande utdata.
- Skriv tester som försäkrar att koden producerar exakt dessa utdata för dessa indata.
Varning: Karakteriseringstester kan vara bräckliga om den underliggande logiken är komplex eller databeroende. Var beredd på att uppdatera dem om du behöver ändra kodens beteende senare.
Sprout Method och Sprout Class
Dessa tekniker, som också beskrivs av Michael Feathers, syftar till att introducera ny funktionalitet i ett befintligt system samtidigt som risken för att bryta befintlig kod minimeras.
Sprout Method: När du behöver lägga till en ny funktion som kräver modifiering av en befintlig metod, skapa en ny metod som innehåller den nya logiken. Anropa sedan denna nya metod från den befintliga metoden. Detta gör att du kan isolera den nya koden och testa den oberoende.
Sprout Class: Liknar Sprout Method, men för klasser. Skapa en ny klass som implementerar den nya funktionaliteten och integrera den sedan i det befintliga systemet.
Sandlåda (Sandboxing)
Sandlåda innebär att isolera den befintliga koden från resten av systemet, vilket gör att du kan testa den i en kontrollerad miljö. Detta kan göras genom att skapa mock-objekt eller stubbar för beroenden eller genom att köra koden i en virtuell maskin.
Mikado-metoden
Mikado-metoden är en visuell problemlösningsmetod för att hantera komplexa refaktoreringuppgifter. Det innebär att skapa ett diagram som representerar beroendena mellan olika delar av koden och sedan refaktorera koden på ett sätt som minimerar påverkan på andra delar av systemet. Kärnprincipen är att "försöka" ändringen och se vad som går sönder. Om det går sönder, återgå till det senaste fungerande tillståndet och registrera problemet. Åtgärda sedan det problemet innan du försöker den ursprungliga ändringen igen.
Verktyg för refaktorering
Flera verktyg kan hjälpa till med refaktorering, automatisera repetitiva uppgifter och ge vägledning om bästa praxis. Dessa verktyg är ofta integrerade i utvecklingsmiljöer (IDE:er):
- IDE:er (t.ex. IntelliJ IDEA, Eclipse, Visual Studio): IDE:er tillhandahåller inbyggda refaktoreringverktyg som automatiskt kan utföra uppgifter som att byta namn på variabler, extrahera metoder och flytta klasser.
- Statiska analysverktyg (t.ex. SonarQube, Checkstyle, PMD): Dessa verktyg analyserar kod för kodlukter, potentiella buggar och säkerhetssårbarheter. De kan hjälpa till att identifiera områden i koden som skulle dra nytta av refaktorering.
- Kodtäckningsverktyg (t.ex. JaCoCo, Cobertura): Dessa verktyg mäter procentandelen av kod som täcks av tester. De kan hjälpa till att identifiera områden i koden som inte är tillräckligt testade.
- Refactoring Browsers (t.ex. Smalltalk Refactoring Browser): Specialiserade verktyg som hjälper till med större omstruktureringsaktiviteter.
Exempel: Ett utvecklingsteam som arbetar med en C#-applikation för ett globalt försäkringsbolag använder Visual Studios inbyggda refaktoreringverktyg för att automatiskt byta namn på variabler och extrahera metoder. De använder också SonarQube för att identifiera kodlukter och potentiella sårbarheter.
Utmaningar och risker
Att refaktorera befintlig kod är inte utan sina utmaningar och risker:
- Introducera regressioner: Den största risken är att introducera buggar under refaktoreringprocessen. Detta kan mildras genom att skriva omfattande tester och refaktorera inkrementellt.
- Brist på domänkunskap: Om de ursprungliga utvecklarna har gått vidare kan det vara svårt att förstå koden och dess syfte. Detta kan leda till felaktiga refaktoreringbeslut.
- Hård koppling: Hårt kopplad kod är svårare att refaktorera, eftersom ändringar i en del av koden kan få oavsiktliga konsekvenser för andra delar av koden.
- Tidsbegränsningar: Refaktorering kan ta tid, och det kan vara svårt att motivera investeringen för intressenter som är fokuserade på att leverera nya funktioner.
- Motstånd mot förändring: Vissa utvecklare kan vara motståndskraftiga mot refaktorering, särskilt om de inte är bekanta med de involverade teknikerna.
Bästa praxis
För att mildra utmaningarna och riskerna med att refaktorera befintlig kod, följ dessa bästa praxis:
- Få acceptans: Se till att intressenter förstår fördelarna med refaktorering och är villiga att investera den tid och de resurser som krävs.
- Börja i liten skala: Börja med att refaktorera små, isolerade kodstycken. Detta hjälper till att bygga förtroende och visa värdet av refaktorering.
- Refaktorera inkrementellt: Gör små, inkrementella ändringar och testa ofta. Detta gör det lättare att identifiera och åtgärda eventuella fel som introduceras.
- Automatisera tester: Skriv omfattande automatiserade tester för att verifiera kodens beteende före och efter refaktorering.
- Använd refaktoreringverktyg: Utnyttja de refaktoreringverktyg som finns tillgängliga i din IDE eller andra verktyg för att automatisera repetitiva uppgifter och ge vägledning om bästa praxis.
- Dokumentera dina ändringar: Dokumentera de ändringar du gör under refaktoreringen. Detta hjälper andra utvecklare att förstå koden och undvika att introducera regressioner i framtiden.
- Kontinuerlig refaktorering: Gör refaktorering till en kontinuerlig del av utvecklingsprocessen, snarare än en engångshändelse. Detta hjälper till att hålla kodbasen ren och underhållbar.
Slutsats
Att refaktorera befintlig kod är ett utmanande men givande åtagande. Genom att följa strategierna och bästa praxis som beskrivs i denna guide kan du tämja odjuret och förvandla dina befintliga system till underhållbara, tillförlitliga och högpresterande tillgångar. Kom ihåg att närma dig refaktorering systematiskt, testa ofta och kommunicera effektivt med ditt team. Med noggrann planering och genomförande kan du låsa upp den dolda potentialen i din befintliga kod och bana väg för framtida innovation.