Gör dina klasser oberoende av omgivningen
Förord
För att få en riktigt bra och flexibelt system måste en vettig arkitektur ligga i grund och botten. Jag har själv för inte länge sedan fått upp ögonen mer för design patterns och arkitektur i det hela och tänkte dela med mig av mina nyvunna erfarenheter till de mindre vetande. Denna artikel kommer att vara på en väldigt grundläggande nivå, men förhoppningsvis kan den ge dig en lika fin "aha" upplevelse som det givit mig.Innehåll
»»
»
»
Bakgrund
Då du ska skapa ett system, litet som stort, är det viktigt att tanken bakom lösningarna är enkel och tydlig. Maximal kodlängd som ska finnas i varje funktion ska helst ligga kring maximalt 1 skärmhöjd, kan du inte hålla koden så kort i en funktion är det aktuellt att dela upp funktionen i mindre delar för att förtydliga det hela. Denna artikel kommer dock inte att gå igenom detta ämne, utan tar upp hur man skapar klasser som inte har ett sådant beroende att man måste ändra eller lägga till funktionalitet i en befintlig klass. Kodexempel som visas kommer att vara i C#, men vilket objektorienterat språk som helst kan användas med dessa principer.
Fungerande men dålig design
Dålig design består av till exempel nedanstående kod.
namespace BadDesign
{
public class CarList
{
private List cars;
public enum CarSource
{
X, Y
}
private List getCarsFromSourceX()
{
// Läs in Car från källa x
}
private List getCarsFromSourceY()
{
// Läs in Car från källa y
}
public CarList(CarSource fromSource)
{
switch (fromSource)
{
case CarSource.X:
this.cars = this.getCarsFromSourceX();
break;
case CarSource.Y:
this.cars = this.getCarsFromSourceY();
break;
}
}
}
public class Car
{
public string Model {get; set;}
}
}
Även om koden är ren och tydlig blir det snabbt väldigt svårt att överblicka. Med tiden kanske det bestäms att Car klassen ska kunna läsas in från källan Z också, och med ovanstående design måste man göra följande:
- Lägga till en extra funktion, getCarsFromSourceZ.
- Ändra på CarSource och lägga till Z.
- Utöka switch satsen i konstruktorn till att ta ta i beaktande Z.
Det blir alltså flera ställen att ändra på, bara för att lägga till en enda källa. Tänk er sedan att man är flera som ska lägga till varsin funktion, då blir det direkt svårare att samarbeta samtidigt. För att inte tala om testning, kanske en ny funktion som läggs till bryter någon existerande funktionalitet, vilket skapar buggar.
En klar förbättring
Genom att skapa lite fler klasser och en interface kan man göra detta system väldigt flexibelt. I grund och botten ska klassen CarList ovan inte behöva bry sig om hur den ska få in listan med Car objekten. Klassen CarList vill bara veta varifrån den ska begära att få en lista med Car objekt, inte hur den ska läsa in det från underliggande medium och konvertera det till en lista med Car objekt.Lösningen ligger i att skapa en interface som definierar vilka funktioner CarList klassen kan använda sig av. Därefter gör man de klasser man behöver, som implementerar detta interface. Här följer en förbättrad version.
namespace GoodDesign
{
interface ICarSource
{
List getCars();
}
public class CarSourceX : ICarSource
{
public List getCars()
{
// Denna klass kan vara en testklass som alltid returnerar samma lista
}
}
public class CarSourceY : ICarSource
{
public string Path { get; set; }
public List getCars()
{
// Denna klass kan läsa in listan från en fil
}
public CarSourceY(string path)
{
this.Path = path;
}
}
public class CarList
{
private List cars;
public CarList(ICarSource carsource)
{
this.cars = carsource.getCars();
}
}
public class Car
{
public string Model {get; set;}
}
}
Vad händer i den nya lösningen? Jo, all implementation om hur man ska läsa in listorna med Car objekt har flyttats ut ur CarList klassen och in i egna fristående klasser, som också kan befinna sig i egna filer. Visst, i .NET kan man ha partial class som tillåter att implementationen i en klass sprids över flera filer, men detta är inte fallet i andra objektorienterade språk, och inte heller bra design i detta fall.
Några av fördelarna med den nya designen är:
- Du kan lätt lägga till nya datakällor utan att ändra på din CarList klass.
- Testning blir lättare, du kan testa varje klass för sig för att se till att den verkligen gör det den ska.
- Renare kod, du får inte milslånga klasser med oändligt många funktioner att hålla reda på.
- Du kan lätt sprida ut implementationen av de olika klasserna till flera olika utvecklare.
- Mycket lättare underhåll av systemet, lättare att sätta sig in i mindre delar som ska fixas.
- Klasserna blir återanvändbara och kan ha valfria parametrar, som bara koden som skapar CarList objektet behöver känna till.
Det finns garanterat fler fördelar, och nackdelarna är faktiskt, om inte helt, nästan obefintliga.
Slutsats
Att dela upp din kod i flera klasser är bra ur många aspekter, men främst får du ett system där varje klass är mindre och lättare att underhålla, samtidigt som den är lättare att testa och återanvända. Du får helt enkelt klasser som är specialiserade på sin uppgift och lämnar andra uppgifter åt andra klasser som är specialiserade på sina saker.Har du kopplat isär din design kan du också lätt lägga till ny funktionalitet utan att ändra en enda befintlig kodrad, utan behöver bara skapa en klass för just den nya implementationen, skapa en instans av den klassen och skicka in den i ett annat objekt, som CarList objektet ovan.
0 Kommentarer