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.
Inga kommentarer:
Skicka en kommentar