”Jag har haft sömnlösa nätter när jag har försökt lägga till funktioner i den kod som vi har köpt från ett annat företag. Jag har att göra med den renaste formen av Legacy Code”
”Jag har verkligen svårt att hantera trasslig, ostrukturerad kod som jag måste arbeta med men som jag inte förstår ett dugg. Legacy Code !”
Legacy code är ett begrepp som förmodligen har många olika definitioner, t.ex. kod som förvärvats från någon annan, kod som skrivits av någon annan, kod som är svår att förstå eller kod som skrivits med föråldrad teknik. Oavsett vilken definition som används anser de flesta av oss att Legacy Code är skrämmande.
Fråga> Hur skulle du definiera legacy code?
Defining Legacy Code
Michael Feathers definierar i sin bok ”Working Effectively with Legacy Code” legacy code som, kod utan tester.
Kod utan tester är en dålig kod. Det spelar ingen roll hur välskriven den är; hur välstrukturerad den är; hur väl inkapslad den är. utan tester finns det inget sätt att avgöra om vår kod blir bättre eller sämre.
Nja, en något modifierad version av denna definition är ”kod utan enhetstester kallas för legacy code”. Det är alltid bättre att ha tester så nära koden som möjligt (enhetstester > integrationstester > UI-tester). Så det skulle inte vara orättvist att kalla en kod utan enhetstester för en äldre kod.
Arbeta med äldre kod
Fråga> Vilket tillvägagångssätt kommer du att använda om du skulle göra en ändring i äldre kod?
De flesta av oss kanske säger: ”Jag kommer att göra ändringen och sluta med det, varför bry sig om att förbättra koden”. Rationalen bakom denna tankeprocess kan vara –
- Jag har inte tillräckligt med tid för att göra om koden, Jag föredrar att göra en ändring och slutföra min berättelse
- Varför riskera att ändra strukturen på koden som har körts i produktion under lång tid
- Vad är den övergripande fördelen med att refaktorisera äldre kod
Michael Feathers kallar den här stilen att göra en ändring för Edit and Pray. Man planerar och gör sina ändringar och när man är klar ber man och ber hårdare för att få ändringarna rätt.
Med den här stilen kan man bara bidra till att öka Legacy code.
Det finns en annan stil att göra ändringar som är Cover and Modify. Bygg ett skyddsnät, gör ändringar i systemet, låt skyddsnätet ge återkoppling och arbeta med dessa återkopplingar.
Det kan säkert antas att Cover and Modify är en väg att gå för att hantera äldre kod.
Fråga> Men, ska man ens lägga tid på att skriva tester i äldre kod eller ens fundera på att refaktorisera en äldre kod?
Pojkscoutregeln
Idén bakom pojkscoutregeln är, enligt farbror Bob, ganska enkel: Lämna koden renare än du hittade den! När du rör en gammal kod ska du rengöra den ordentligt. Tillämpa inte bara en genvägslösning som gör koden svårare att förstå utan behandla den med omsorg. Det räcker inte att skriva koden bra, koden måste hållas ren över tid.
Vi får ett mycket starkt budskap när pojkscoutregeln tillämpas på äldre kod ”Lämna ett spår av förståelse bakom dig så att andra kan följa dig”, vilket innebär att vi ska refaktorisera koden för att göra den mer begriplig. Och för att kunna omarbeta koden kommer vi att bygga ett skyddsnät runt den.
När vi nu förstår att vi inte kan ta genvägar är det enda alternativet som återstår att skriva några tester, omarbeta koden och fortsätta med utvecklingen. Frågor>
- Vilka tester ska vi skriva?
- Hur mycket ska vi refaktorisera?
Vilka tester ska vi skriva
I nästan alla äldre system är det viktigare vad systemet gör än vad det är tänkt att göra.
Karaktäriseringstester, de tester som vi behöver när vi vill bevara beteende kallas för karaktäriseringstester. Ett karaktäriseringstest är ett test som karaktäriserar det faktiska beteendet hos ett kodstycke. Det finns inget ”Ja, den borde göra så här” eller ”Jag tror att den gör så här”. Testerna dokumenterar systemets faktiska nuvarande beteende.
Skrivning av karaktäriseringstest
Ett karaktäriseringstest dokumenterar per definition systemets faktiska nuvarande beteende på exakt samma sätt som det körs i produktionsmiljön.
Låt oss skriva ett karaktäriseringstest för ett Customer-objekt som genererar textutlåtande för några filmer som hyrts av en kund.
import static com.code.legacy.movie.MovieType.CHILDREN;
import static org.junit.Assert.assertEquals;public void shouldGenerateTextStatement(){ Customer john = new Customer("John");
Movie childrenMovie = new Movie("Toy Story", CHILDREN);
int daysRented = 3;
Rental rental = new Rental(childrenMovie, daysRented); john.addRental(rental); String statement = john.generateTextStatement();
assertEquals("", statement);
}
Detta test försöker förstå (eller karaktärisera) genereringen av ”Textutlåtande” för en kund som fått en barnfilm som hyrts i tre dagar. Eftersom vi inte förstår systemet (åtminstone inte nu) förväntar vi oss att uttalandet ska vara tomt eller innehålla något dummyvärde.
Låt oss köra testet och låta det misslyckas. När det gör det har vi tagit reda på vad koden faktiskt gör under det villkoret.
java.lang.AssertionError:
Expected :""
Actual :Rental Record for John, Total amount owed = 12.5. You earned 4 frequent renter points.
Nu när vi vet hur koden beter sig kan vi gå vidare och ändra testet.
import static com.code.legacy.movie.MovieType.CHILDREN;
import static org.junit.Assert.assertEquals;public void shouldGenerateTextStatement(){
String expectedStatement = "Rental Record for John, Total amount owed = 12.5. You earned 4 frequent renter points"; Customer john = new Customer("John");
Movie childrenMovie = new Movie("Toy Story", CHILDREN);
int daysRented = 3;
Rental rental = new Rental(childrenMovie, daysRented);
john.addRental(rental); Sting statement = john.generateTextStatement();
assertEquals(expectedStatement, statement);
}
Vänta lite, kopierade vi bara den utdata som genereras av koden och placeras i vårt test. Ja, det är precis vad vi gjorde.
Vi försöker inte hitta fel just nu. Vi försöker sätta in en mekanism för att hitta fel senare, fel som visar sig som skillnader från systemets nuvarande beteende. När vi antar detta perspektiv blir vår syn på tester annorlunda: De har ingen moralisk auktoritet; de sitter bara där och dokumenterar vad systemet verkligen gör. I detta skede är det mycket viktigt att ha den kunskapen om vad systemet faktiskt gör någonstans.
Fråga> Vad är det totala antalet tester som vi skriver för att karakterisera ett system?
Svar> Det är oändligt. Vi skulle kunna ägna en stor del av våra liv åt att skriva fall efter fall för varje klass i en äldre kod.
Fråga> När slutar vi då? Finns det något sätt att veta vilka fall som är viktigare än andra?
Svar> Titta på den kod vi karakteriserar. Koden i sig kan ge oss idéer om vad den gör, och om vi har frågor är tester ett idealiskt sätt att ställa dem. Skriv då ett eller flera test som täcker en tillräckligt stor del av koden.
Fråga> Täcker det allt i koden?
Svar> Det kanske inte gör det. Men då gör vi nästa steg. Vi tänker på de ändringar som vi vill göra i koden och försöker ta reda på om de tester som vi har kommer att känna av eventuella problem som vi kan orsaka. Om de inte gör det, lägger vi till fler tester tills vi känner oss säkra på att de gör det.
Hur mycket ska refaktoriseras?
Det finns så mycket att refaktorisera i äldre kod och vi kan inte refaktorisera allt. För att svara på detta måste vi gå tillbaka till att förstå vårt syfte med att refaktorisera den gamla koden.
Vi vill refaktorisera den gamla koden för att lämna den renare än vad den var när den kom till oss och för att göra den begriplig för andra.
Med det sagt vill vi göra systemet bättre och hålla fokus på uppgiften. Vi vill inte bli galna med refaktorisering och försöka skriva om hela systemet på några dagar. Vad vi vill göra är att ”refaktorisera den kod som kommer i vägen för att genomföra en ny förändring”. Vi kommer att försöka förstå detta bättre med ett exempel i nästa artikel.