En praktisk veiledning for refaktorering av eldre kode, som dekker identifisering, prioritering, teknikker og beste praksis for modernisering og vedlikehold.
Slik temmer du udyret: Refaktoreringstrategier for eldre kode
Eldre kode. Selve begrepet fremkaller ofte bilder av sprikende, udokumenterte systemer, skjøre avhengigheter og en overveldende følelse av frykt. Mange utviklere over hele verden står overfor utfordringen med å vedlikeholde og utvikle disse systemene, som ofte er kritiske for forretningsdriften. Denne omfattende guiden gir praktiske strategier for refaktorering av eldre kode, og gjør en kilde til frustrasjon om til en mulighet for modernisering og forbedring.
Hva er eldre kode?
Før vi dykker ned i refaktoreringsteknikker, er det viktig å definere hva vi mener med "eldre kode". Selv om begrepet rett og slett kan referere til eldre kode, fokuserer en mer nyansert definisjon på vedlikeholdbarheten. Michael Feathers, i sin banebrytende bok "Working Effectively with Legacy Code", definerer eldre kode som kode uten tester. Denne mangelen på tester gjør det vanskelig å trygt modifisere koden uten å introdusere regresjoner. Eldre kode kan imidlertid også ha andre kjennetegn:
- Mangel på dokumentasjon: De opprinnelige utviklerne kan ha sluttet, og etterlatt seg lite eller ingen dokumentasjon som forklarer systemets arkitektur, designbeslutninger eller til og med grunnleggende funksjonalitet.
- Komplekse avhengigheter: Koden kan være tett koblet, noe som gjør det vanskelig å isolere og endre individuelle komponenter uten å påvirke andre deler av systemet.
- Utdaterte teknologier: Koden kan være skrevet med eldre programmeringsspråk, rammeverk eller biblioteker som ikke lenger aktivt støttes, noe som utgjør sikkerhetsrisikoer og begrenser tilgangen til moderne verktøy.
- Dårlig kodekvalitet: Koden kan inneholde duplisert kode, lange metoder og andre kodelukter som gjør den vanskelig å forstå og vedlikeholde.
- Skjør design: Tilsynelatende små endringer kan få uforutsette og vidtrekkende konsekvenser.
Det er viktig å merke seg at eldre kode ikke er iboende dårlig. Den representerer ofte en betydelig investering og inneholder verdifull domenekunnskap. Målet med refaktorering er å bevare denne verdien samtidig som man forbedrer kodens vedlikeholdbarhet, pålitelighet og ytelse.
Hvorfor refaktorere eldre kode?
Refaktorering av eldre kode kan være en skremmende oppgave, men fordelene veier ofte opp for utfordringene. Her er noen sentrale grunner til å investere i refaktorering:
- Forbedret vedlikeholdbarhet: Refaktorering gjør koden enklere å forstå, endre og feilsøke, noe som reduserer kostnadene og innsatsen som kreves for løpende vedlikehold. For globale team er dette spesielt viktig, da det reduserer avhengigheten av enkeltpersoner og fremmer kunnskapsdeling.
- Redusert teknisk gjeld: Teknisk gjeld refererer til den implisitte kostnaden av omarbeid forårsaket av å velge en enkel løsning nå i stedet for å bruke en bedre tilnærming som ville tatt lengre tid. Refaktorering hjelper til med å betale ned denne gjelden, og forbedrer den generelle helsen til kodebasen.
- Forbedret pålitelighet: Ved å adressere kodelukter og forbedre kodens struktur, kan refaktorering redusere risikoen for feil og forbedre systemets generelle pålitelighet.
- Økt ytelse: Refaktorering kan identifisere og adressere ytelsesflaskehalser, noe som resulterer i raskere kjøretider og forbedret respons.
- Enklere integrasjon: Refaktorering kan gjøre det enklere å integrere det eldre systemet med nye systemer og teknologier, noe som muliggjør innovasjon og modernisering. For eksempel kan en europeisk e-handelsplattform trenge å integrere med en ny betalingsgateway som bruker et annet API.
- Forbedret utviklermoral: Å jobbe med ren, velstrukturert kode er mer givende og produktivt for utviklere. Refaktorering kan øke moralen og tiltrekke seg talenter.
Identifisering av refaktoreringkandidater
Ikke all eldre kode trenger å refaktoreres. Det er viktig å prioritere refaktoreringstiltak basert på følgende faktorer:
- Endringsfrekvens: Kode som ofte endres er en førsteklasses kandidat for refaktorering, da forbedringer i vedlikeholdbarhet vil ha en betydelig innvirkning på utviklingsproduktiviteten.
- Kompleksitet: Kode som er kompleks og vanskelig å forstå har større sannsynlighet for å inneholde feil og er vanskeligere å endre på en trygg måte.
- Konsekvens av feil: Kode som er kritisk for forretningsdriften eller som har høy risiko for å forårsake kostbare feil, bør prioriteres for refaktorering.
- Ytelsesflaskehalser: Kode som er identifisert som en ytelsesflaskehals bør refaktoreres for å forbedre ytelsen.
- Kodelukter: Vær på utkikk etter vanlige kodelukter som lange metoder, store klasser, duplisert kode og "feature envy". Dette er indikatorer på områder som kan dra nytte av refaktorering.
Eksempel: Se for deg et globalt logistikkselskap med et eldre system for håndtering av forsendelser. Modulen som er ansvarlig for å beregne fraktkostnader oppdateres ofte på grunn av endrede regelverk og drivstoffpriser. Denne modulen er en førsteklasses kandidat for refaktorering.
Refaktoreringsteknikker
Det finnes en rekke refaktoreringsteknikker, hver utformet for å adressere spesifikke kodelukter eller forbedre spesifikke aspekter av koden. Her er noen vanlige teknikker:
Komponering av metoder
Disse teknikkene fokuserer på å bryte ned store, komplekse metoder til mindre, mer håndterbare metoder. Dette forbedrer lesbarheten, reduserer duplisering og gjør koden enklere å teste.
- Extract Method: Dette innebærer å identifisere en kodeblokk som utfører en spesifikk oppgave og flytte den inn i en ny metode.
- Inline Method: Dette innebærer å erstatte et metodekall med metodens kropp. Bruk dette når en metodes navn er like tydelig som dens kropp, eller når du er i ferd med å bruke Extract Method, men den eksisterende metoden er for kort.
- Replace Temp with Query: Dette innebærer å erstatte en midlertidig variabel med et metodekall som beregner variabelens verdi ved behov.
- Introduce Explaining Variable: Bruk dette for å tilordne resultatet av et uttrykk til en variabel med et beskrivende navn, for å tydeliggjøre formålet.
Flytting av funksjoner mellom objekter
Disse teknikkene fokuserer på å forbedre designet av klasser og objekter ved å flytte ansvarsområder dit de hører hjemme.
- Move Method: Dette innebærer å flytte en metode fra en klasse til en annen klasse der den logisk hører hjemme.
- Move Field: Dette innebærer å flytte et felt fra en klasse til en annen klasse der det logisk hører hjemme.
- Extract Class: Dette innebærer å lage en ny klasse fra et sammenhengende sett med ansvarsområder hentet fra en eksisterende klasse.
- Inline Class: Bruk dette for å slå sammen en klasse med en annen når den ikke lenger gjør nok til å rettferdiggjøre sin eksistens.
- Hide Delegate: Dette innebærer å lage metoder i serveren for å skjule delegeringslogikk fra klienten, og redusere koblingen mellom klienten og delegaten.
- Remove Middle Man: Hvis en klasse delegerer nesten alt sitt arbeid, hjelper dette med å kutte ut mellommannen.
- Introduce Foreign Method: Legger til en metode i en klientklasse for å betjene klienten med funksjoner som egentlig trengs fra en serverklasse, men som ikke kan endres på grunn av manglende tilgang eller planlagte endringer i serverklassen.
- Introduce Local Extension: Oppretter en ny klasse som inneholder de nye metodene. Nyttig når du ikke kontrollerer kilden til klassen og ikke kan legge til atferd direkte.
Organisering av data
Disse teknikkene fokuserer på å forbedre måten data lagres og aksesseres på, noe som gjør det enklere å forstå og endre.
- Replace Data Value with Object: Dette innebærer å erstatte en enkel dataverdi med et objekt som innkapsler relaterte data og atferd.
- Change Value to Reference: Dette innebærer å endre et verdiobjekt til et referanseobjekt, når flere objekter deler samme verdi.
- Change Unidirectional Association to Bidirectional: Oppretter en toveis kobling mellom to klasser der det bare finnes en enveiskobling.
- Change Bidirectional Association to Unidirectional: Forenkler assosiasjoner ved å gjøre et toveisforhold enveis.
- Replace Magic Number with Symbolic Constant: Dette innebærer å erstatte bokstavelige verdier med navngitte konstanter, noe som gjør koden enklere å forstå og vedlikeholde.
- Encapsulate Field: Tilbyr en getter- og setter-metode for å få tilgang til feltet.
- Encapsulate Collection: Sikrer at alle endringer i samlingen skjer gjennom nøye kontrollerte metoder i eierklassen.
- Replace Record with Data Class: Oppretter en ny klasse med felt som samsvarer med postens struktur og tilgangsmetoder.
- Replace Type Code with Class: Opprett en ny klasse når typekoden har et begrenset, kjent sett med mulige verdier.
- Replace Type Code with Subclasses: For når typekodeverdien påvirker atferden til klassen.
- Replace Type Code with State/Strategy: For når typekodeverdien påvirker atferden til klassen, men subklassing ikke er hensiktsmessig.
- Replace Subclass with Fields: Fjerner en subklasse og legger til felt i superklassen som representerer subklassens distinkte egenskaper.
Forenkling av betingede uttrykk
Betinget logikk kan raskt bli komplisert. Disse teknikkene tar sikte på å klargjøre og forenkle.
- Decompose Conditional: Dette innebærer å bryte ned en kompleks betinget setning i mindre, mer håndterbare deler.
- Consolidate Conditional Expression: Dette innebærer å kombinere flere betingede setninger til en enkelt, mer konsis setning.
- Consolidate Duplicate Conditional Fragments: Dette innebærer å flytte kode som er duplisert i flere grener av en betinget setning utenfor betingelsen.
- Remove Control Flag: Eliminer boolske variabler som brukes til å kontrollere logikkflyten.
- Replace Nested Conditional with Guard Clauses: Gjør koden mer lesbar ved å plassere alle spesialtilfeller øverst og stoppe behandlingen hvis noen av dem er sanne.
- Replace Conditional with Polymorphism: Dette innebærer å erstatte betinget logikk med polymorfisme, slik at forskjellige objekter kan håndtere forskjellige tilfeller.
- Introduce Null Object: I stedet for å sjekke for en nullverdi, opprett et standardobjekt som gir standardatferd.
- Introduce Assertion: Dokumenter forventninger eksplisitt ved å lage en test som sjekker for dem.
Forenkling av metodekall
- Rename Method: Dette virker åpenbart, men er utrolig nyttig for å gjøre koden tydelig.
- Add Parameter: Å legge til informasjon i en metodesignatur gjør at metoden kan bli mer fleksibel og gjenbrukbar.
- Remove Parameter: Hvis en parameter ikke brukes, kvitt deg med den for å forenkle grensesnittet.
- Separate Query from Modifier: Hvis en metode både endrer og returnerer en verdi, skill den i to distinkte metoder.
- Parameterize Method: Bruk dette for å konsolidere lignende metoder til en enkelt metode med en parameter som varierer atferden.
- Replace Parameter with Explicit Methods: Gjør det motsatte av parameterisering - del en enkelt metode opp i flere metoder som hver representerer en spesifikk verdi av parameteren.
- Preserve Whole Object: I stedet for å sende noen få spesifikke dataelementer til en metode, send hele objektet slik at metoden har tilgang til alle dataene.
- Replace Parameter with Method: Hvis en metode alltid kalles med den samme verdien avledet fra et felt, bør du vurdere å utlede parameterverdien inne i metoden.
- Introduce Parameter Object: Grupper flere parametere sammen i et objekt når de naturlig hører sammen.
- Remove Setting Method: Unngå settere hvis et felt bare skal initialiseres, men ikke endres etter konstruksjon.
- Hide Method: Reduser synligheten til en metode hvis den bare brukes innenfor en enkelt klasse.
- Replace Constructor with Factory Method: Et mer beskrivende alternativ til konstruktører.
- Replace Exception with Test: Hvis unntak brukes som flytkontroll, erstatt dem med betinget logikk for å forbedre ytelsen.
Håndtering av generalisering
- Pull Up Field: Flytt et felt fra en subklasse til dens superklasse.
- Pull Up Method: Flytt en metode fra en subklasse til dens superklasse.
- Pull Up Constructor Body: Flytt kroppen til en konstruktør fra en subklasse til dens superklasse.
- Push Down Method: Flytt en metode fra en superklasse til dens subklasser.
- Push Down Field: Flytt et felt fra en superklasse til dens subklasser.
- Extract Interface: Oppretter et grensesnitt fra de offentlige metodene til en klasse.
- Extract Superclass: Flytt felles funksjonalitet fra to klasser til en ny superklasse.
- Collapse Hierarchy: Kombiner en superklasse og subklasse til en enkelt klasse.
- Form Template Method: Opprett en malmetode i en superklasse som definerer trinnene i en algoritme, slik at subklasser kan overstyre spesifikke trinn.
- Replace Inheritance with Delegation: Opprett et felt i klassen som refererer til funksjonaliteten, i stedet for å arve den.
- Replace Delegation with Inheritance: Når delegering er for kompleks, bytt til arv.
Dette er bare noen få eksempler på de mange refaktoreringsteknikkene som er tilgjengelige. Valget av hvilken teknikk man skal bruke, avhenger av den spesifikke kodelukten og det ønskede resultatet.
Eksempel: En stor metode i en Java-applikasjon brukt av en global bank beregner rentesatser. Ved å bruke Extract Method for å lage mindre, mer fokuserte metoder, forbedres lesbarheten, og det blir enklere å oppdatere rentesatsberegningslogikken uten å påvirke andre deler av metoden.
Refaktoreringprosessen
Refaktorering bør tilnærmes systematisk for å minimere risiko og maksimere sjansene for suksess. Her er en anbefalt prosess:
- Identifiser refaktoreringkandidater: Bruk kriteriene nevnt tidligere for å identifisere områder av koden som vil ha størst nytte av refaktorering.
- Lag tester: Før du gjør noen endringer, skriv automatiserte tester for å verifisere den eksisterende atferden til koden. Dette er avgjørende for å sikre at refaktorering ikke introduserer regresjoner. Verktøy som JUnit (Java), pytest (Python) eller Jest (JavaScript) kan brukes til å skrive enhetstester.
- Refaktorer inkrementelt: Gjør små, inkrementelle endringer og kjør testene etter hver endring. Dette gjør det lettere å identifisere og fikse eventuelle feil som introduseres.
- Commit ofte: Commit endringene dine til versjonskontroll ofte. Dette gjør at du enkelt kan gå tilbake til en tidligere versjon hvis noe går galt.
- Gjennomgå koden: Få koden din gjennomgått av en annen utvikler. Dette kan bidra til å identifisere potensielle problemer og sikre at refaktoreringen gjøres riktig.
- Overvåk ytelsen: Etter refaktorering, overvåk ytelsen til systemet for å sikre at endringene ikke har introdusert noen ytelsesregresjoner.
Eksempel: Et team som refaktorerer en Python-modul i en global e-handelsplattform bruker `pytest` for å lage enhetstester for den eksisterende funksjonaliteten. Deretter bruker de Extract Class-refaktoreringen for å skille ansvarsområder og forbedre modulens struktur. Etter hver liten endring kjører de testene for å sikre at funksjonaliteten forblir uendret.
Strategier for å introdusere tester i eldre kode
Som Michael Feathers så treffende sa, er eldre kode kode uten tester. Å introdusere tester i eksisterende kodebaser kan føles som en massiv oppgave, men det er avgjørende for trygg refaktorering. Her er flere strategier for å nærme seg denne oppgaven:
Karakteriseringstester (også kjent som Golden Master-tester)
Når du har å gjøre med kode som er vanskelig å forstå, kan karakteriseringstester hjelpe deg med å fange opp dens eksisterende atferd før du begynner å gjøre endringer. Ideen er å skrive tester som bekrefter den nåværende utdataen fra koden for et gitt sett med inndata. Disse testene verifiserer ikke nødvendigvis korrekthet; de dokumenterer bare hva koden *for øyeblikket* gjør.
Fremgangsmåte:
- Identifiser en kodeenhet du vil karakterisere (f.eks. en funksjon eller metode).
- Lag et sett med inndataverdier som representerer en rekke vanlige og ekstreme scenarier.
- Kjør koden med disse inndataene og fang opp de resulterende utdataene.
- Skriv tester som bekrefter at koden produserer nøyaktig disse utdataene for disse inndataene.
Advarsel: Karakteriseringstester kan være skjøre hvis den underliggende logikken er kompleks eller dataavhengig. Vær forberedt på å oppdatere dem hvis du må endre kodens atferd senere.
Sprout-metode og Sprout-klasse
Disse teknikkene, også beskrevet av Michael Feathers, tar sikte på å introdusere ny funksjonalitet i et eldre system samtidig som risikoen for å ødelegge eksisterende kode minimeres.
Sprout-metode: Når du trenger å legge til en ny funksjon som krever endring av en eksisterende metode, lag en ny metode som inneholder den nye logikken. Kall deretter denne nye metoden fra den eksisterende metoden. Dette lar deg isolere den nye koden og teste den uavhengig.
Sprout-klasse: Ligner på Sprout-metode, men for klasser. Opprett en ny klasse som implementerer den nye funksjonaliteten, og integrer den deretter i det eksisterende systemet.
Sandkasse-testing (Sandboxing)
Sandkasse-testing innebærer å isolere den eldre koden fra resten av systemet, slik at du kan teste den i et kontrollert miljø. Dette kan gjøres ved å lage mocker eller stubber for avhengigheter eller ved å kjøre koden i en virtuell maskin.
Mikado-metoden
Mikado-metoden er en visuell problemløsningstilnærming for å takle komplekse refaktoreringsoppgaver. Det innebærer å lage et diagram som representerer avhengighetene mellom forskjellige deler av koden og deretter refaktorere koden på en måte som minimerer innvirkningen på andre deler av systemet. Hovedprinsippet er å "prøve" endringen og se hva som går i stykker. Hvis det går i stykker, gå tilbake til den siste fungerende tilstanden og registrer problemet. Adresser deretter det problemet før du prøver den opprinnelige endringen på nytt.
Verktøy for refaktorering
Flere verktøy kan hjelpe til med refaktorering, automatisere repetitive oppgaver og gi veiledning om beste praksis. Disse verktøyene er ofte integrert i integrerte utviklingsmiljøer (IDE-er):
- IDE-er (f.eks. IntelliJ IDEA, Eclipse, Visual Studio): IDE-er tilbyr innebygde refaktoreringverktøy som automatisk kan utføre oppgaver som å gi nytt navn til variabler, trekke ut metoder og flytte klasser.
- Statiske analyseverktøy (f.eks. SonarQube, Checkstyle, PMD): Disse verktøyene analyserer kode for kodelukter, potensielle feil og sikkerhetssårbarheter. De kan hjelpe med å identifisere områder av koden som vil ha nytte av refaktorering.
- Kodedekningsverktøy (f.eks. JaCoCo, Cobertura): Disse verktøyene måler prosentandelen av kode som dekkes av tester. De kan hjelpe med å identifisere områder av koden som ikke er tilstrekkelig testet.
- Refaktorering-nettlesere (f.eks. Smalltalk Refactoring Browser): Spesialiserte verktøy som hjelper til med større restruktureringsaktiviteter.
Eksempel: Et utviklingsteam som jobber med en C#-applikasjon for et globalt forsikringsselskap bruker Visual Studios innebygde refaktoreringverktøy for å automatisk gi nytt navn til variabler og trekke ut metoder. De bruker også SonarQube for å identifisere kodelukter og potensielle sårbarheter.
Utfordringer og risikoer
Refaktorering av eldre kode er ikke uten utfordringer og risikoer:
- Innføring av regresjoner: Den største risikoen er å introdusere feil under refaktoreringprosessen. Dette kan reduseres ved å skrive omfattende tester og refaktorere inkrementelt.
- Mangel på domenekunnskap: Hvis de opprinnelige utviklerne har sluttet, kan det være vanskelig å forstå koden og dens formål. Dette kan føre til feilaktige refaktoreringbeslutninger.
- Tett kobling: Tett koblet kode er vanskeligere å refaktorere, da endringer i en del av koden kan ha utilsiktede konsekvenser for andre deler av koden.
- Tidsbegrensninger: Refaktorering kan ta tid, og det kan være vanskelig å rettferdiggjøre investeringen overfor interessenter som er fokusert på å levere nye funksjoner.
- Motstand mot endring: Noen utviklere kan være motvillige til refaktorering, spesielt hvis de ikke er kjent med teknikkene som er involvert.
Beste praksis
For å redusere utfordringene og risikoene forbundet med refaktorering av eldre kode, følg disse beste praksisene:
- Få aksept fra interessenter: Sørg for at interessenter forstår fordelene med refaktorering og er villige til å investere den tiden og de ressursene som kreves.
- Start i det små: Begynn med å refaktorere små, isolerte deler av koden. Dette vil bidra til å bygge selvtillit og demonstrere verdien av refaktorering.
- Refaktorer inkrementelt: Gjør små, inkrementelle endringer og test ofte. Dette vil gjøre det lettere å identifisere og fikse eventuelle feil som introduseres.
- Automatiser tester: Skriv omfattende automatiserte tester for å verifisere atferden til koden før og etter refaktorering.
- Bruk refaktoreringverktøy: Utnytt refaktoreringverktøyene som er tilgjengelige i IDE-en din eller andre verktøy for å automatisere repetitive oppgaver og gi veiledning om beste praksis.
- Dokumenter endringene dine: Dokumenter endringene du gjør under refaktorering. Dette vil hjelpe andre utviklere med å forstå koden og unngå å introdusere regresjoner i fremtiden.
- Kontinuerlig refaktorering: Gjør refaktorering til en kontinuerlig del av utviklingsprosessen, i stedet for en engangshendelse. Dette vil bidra til å holde kodebasen ren og vedlikeholdbar.
Konklusjon
Refaktorering av eldre kode er en utfordrende, men givende oppgave. Ved å følge strategiene og beste praksisene som er beskrevet i denne guiden, kan du temme udyret og transformere dine eldre systemer til vedlikeholdbare, pålitelige og høytytende ressurser. Husk å tilnærme deg refaktorering systematisk, teste ofte og kommunisere effektivt med teamet ditt. Med nøye planlegging og utførelse kan du låse opp det skjulte potensialet i din eldre kode og legge grunnlaget for fremtidig innovasjon.