A SOLID egy tervezési elvek nevéből képzett mozaikszó, amit Robert C. Martin talált ki azért, hogy a szoftverek könnyebben megérthetőek és karbantarthatók maradjanak. Az elvek:
Single Responsibility principle
Egy felelősség elve. Egy osztály csak egy felelősséggel rendelkezzen.
A kérdés már csak az, hogy mit nevezünk felelősségnek? Ezt egy példán keresztül könnyebben meg lehet érteni. Tételezzük fel, hogy van egy adatokat leíró osztályunk és az adatokat be kellene tudnunk olvasni valamilyen formátumban. Adódhat az elképzelés, hogy a fájlbeolvasást metódusként megvalósítjuk az osztályban.
Ez azért problémás, mert így a kód valójában két dolgot csinál: értékeket tárol és beolvas. Ha a későbbiekben ez a funkcionalitás bővül exportálási lehetőséggel, akkor már három dolog van egy helyen implementálva. Ez pedig előbb-utóbb elkerülhetetlenül azt eredményezi, hogy az egyik metódus módosítása ki fog hatni a másikra és szépen csendben akár képes eltörni a funkcionalitást, ami csak futásidőben derül ki.
Arról, hogy egy adott osztály teljesíti-e a Signgle Responsibility elvet úgy tudunk megbizonyosodni, hogy mikor magyarázzuk az osztály/metódus szerepét és kötőszavakat kell használnunk, (és, illetve, továbbá, meg még és szinonimáik), akkor biztos, hogy nem single responsible.
Open/Closed principle
Nyílt/zárt elv. Egy osztály legyen nyílt a kiterjesztésre, de zárt a módosításra.
Tételezzük fel, hogy van egy osztályunk, aminek definiáltuk a publikus részeit. Az osztályunk nyílt a kiterjesztésre, mert örököltethetünk belőle újabb funkciók beletételével, de csak akkor lesz zárt a módosításra, ha az eredeti osztály publikus tagjait később nem változtatjuk.
Nézzünk egy példát:
class Pelda
{
public void CsinalValamit()
{
//kód
}
}
class Gyerek : Pelda
{
public void CsinalValamit()
{
// Open closed sértés
// és egyben Liskov elvnek sem felel meg.
}
}
A fenti példában a CsinalValamit() metódus a Pelda osztályban nem virtuális metódusként van szerepeltetve, vagyis nem írhatnánk felül a működését. Azonban a polimorfizmusnak köszönhetően a Gyerek osztályban is definiálhatunk egy CsinalValamit() metódust. Ha ilyesmit csinálunk, akkor megsértjük az Open/Closed elvet, mivel az eredeti Pelda osztályban a metódus nem volt felülírhatónak jelölve.
Az elv eredetileg Java-ra lett kitalálva. Ez azért érdekes adalék, mivel Java esetén minden metódus virtuális, vagyis felülírható. C# esetén explicit jelölni kell, hogy egy metódus virtuális. Ha szó szerint vesszük az Open/Closed elvet, akkor ha egy alap osztály funkcionalitását bővítjük örököltetés helyett, akkor már sértjük az Open/Closed elvet. De mint az életben, ez a döntés sem csupán fekete és fehér kérdése: minden esetben mérlegeljük a döntésünket és ha lehet, akkor törekedjünk az öröklés észszerű és jó használatára.
Liskov substitution principle
Liskov helyettesítési elv. Minden osztály legyen helyettesíthető a leszármazott osztályával anélkül, hogy a program helyes működése megváltozna.
Ez röviden és tömören azt jelenti, hogy ha örököltetünk egy osztályból és felüldefiniáljuk az osztály bizonyos részeit, akkor ne implementáljunk az ősosztálytól radikálisan eltérő logikát a felüldefiniált tagokban.
Ez alatt azt értem, ha az ősosztály rendelkezik egy ToString() metódussal, akkor A leszármazott osztályban is csak azt csinálja, amit az ősosztályban elvárunk. Ne valósítson meg belső állapot módosítást és egyéb, a nevéből és az eredeti szándékából levezethető viselkedést.
Továbbá ebbe beleértendő az is, hogy ha egy, az osztályunk által megvalósított interfész egy metódust/tulajdonságot definiál, akkor a megvalósítást tartalmazó osztályban az implementációnak nem szabad NotImplementedException kivételt kiváltania. Ugyanebbe a szabályba értendő az is, ha az ősosztály definiál egy virtuális vagy absztrakt metódust/tulajdonságot, akkor a leszármazott osztályoknak nem szabad az öröklési láncot megszakítaniuk.
abstract class OsOsztaly
{
public virtual void Valami()
{
}
}
class Leszarmazott: OsOsztaly
{
public new void Valami()
{
//öröklés megszakítva! Ilyet ne csináljunk.
}
}
Szintén Liskov sértés:
interface IPelda
{
void PeldaMetodus();
}
class Implementacio: IPelda
{
public void PeldaMetodus()
{
//szintén Liskov sértés
throw new NotImplementedException();
}
}
A fenti interfész példában a NotImplementedException dobása az interfész által definiált metódusban Liskov sértés, mivel az Implementacio nem helyettesíthető a IPelda felületre probléma nélkül.
Szintén Liskov elv sértése, ha van egy metódusunk egy ősosztályban a következő szignatúrával: bool TryParse(string input, out SajatOsztaly parsed) Ebben az esetben a szignatúrából következik, hogy true értékkel kell visszatérni, ha sikeres volt a művelet és false értékkel, ha nem. Azonban ha a leszármaztatott osztály ilyen metódusában kivételt dobunk feldolgozás közben, akkor az ősosztályra nem helyettesíthető be a gyerek osztály. Fordító és nyelvi szinten igen, de viselkedés tekintetében nem, mivel 99% az esélye annak, hogy a komponens, ami az ősosztályra függ, nem számít arra, hogy majd egy implementáció kivételt dob.
Interface segregation principle
Interfész elválasztási elv. Több specifikus interfész jobb, mint egy általános.
Ha a kódunkat sok kicsi interfésszel valósítjuk meg, akkor elérhetjük azt, hogy a felületet felhasználó osztálynak ténylegesen csak ahhoz lesz hozzáférése, amire szüksége van. Ezzel csökkentjük a hibalehetőségeket.
Ennek az elvnek nem kicsit köze van a Single Responsibility-hez. A single responsibility elsősorban az implementációk (tényleges osztályok) kérdésével foglalkozik, míg az interface segregation az implementációk publikus felületével.
Tételezzük fel, hogy egy moduláris alkalmazást készítünk. Ebben a moduloknak szeretnénk egy közös csatolófelületet biztosítani, amin keresztül mondjuk nyomtathatnak és fájlokat kezelhetnek. Kézenfekvő lenne ilyen módon implementálni az interfészt:
interface API
{
void FajltMegnyit(string fajlnev);
void Ment(string fajlnev);
void Nyomtat();
void NyomtatasiElonezet();
}
Ha a single responsibility esetén tanultakat alkalmazzuk, akkor nyilvánvaló, hogy nem egy felelőssége van, mivel kell egy mágikus ÉS szót alkalmaznunk: nyomtat ÉS fájlokat kezel. Ugyanakkor Interface segregation-t is sértünk, mivel interfészekről beszélünk. Helyesebb megvalósítás:
interface FajlApi
{
void FajltMegnyit(string fajlnev);
void Ment(string fajlnev);
}
interface NyomtatApi
{
void Nyomtat();
void NyomtatasiElonezet();
}
Dependency inversion principle
Függőség megfordítási elv. A kódod függjön absztrakcióktól, ne konkrét implementációktól. Vagyis, ha az osztályunknak szüksége van egy másik osztályra a működéséhez, akkor ne a konkrét osztálytípust várja függőségnek, hanem egy interfészt, amit a függőségosztály megvalósít.
Nézzünk egy példát. Tételezzük fel, hogy van egy osztályunk Foo, ami működéséhez egy másik osztályt, a Bar-t használja fel. Kézenfekvő egy Façade-es megoldás:
class Foo
{
private Bar _bar;
public Foo()
{
_bar = new Bar();
}
}
A probléma ezzel a kódrészlettel az, hogy Foo osztály egyetlen példánya sem létezik Bar nélkül. Vagyis, ha a Bar osztály módosul, akkor az indirekt módon kihat a Foo osztály működésére is, ami azt eredményezheti, hogy mindkét osztály funkcionalitása eltörik. Éppen ezért szerencsésebb lenne a fenti példában, ha a Foo osztály nem közvetlenül függne Bar osztálytól, hanem mondjuk egy absztrakciójától:
interface IBar
{
void Publikus();
}
class Foo
{
private IBar _bar;
public Foo(IBar bar)
{
_bar = bar;
}
}
Ennek a megoldásnak az az előnye, hogy két komponens közötti interakció jól definiált egy interfészen (esetlegesen absztrakt osztályon) keresztül, illetve tesztelés esetén helyettesíthető az IBar tetszőleges implementációval, ami nagymértékben megkönnyíti a tesztek írását és a hibák feltárását. További előnye az ilyen fajta megoldásnak, hogy minimalizáljuk annak az esélyét, hogy Jenga1 kód alakuljon ki a szoftverünkben.
Természetesen ez sem fekete-fehér döntés, mivel a Façade egy létező tervezési minta. A dependency inversion bevezetése olyan részek esetén, amelyekben nincs rá szükség több problémát tud okozni, mint amit megold, így ezt is érdemes mérlegelni. Azonban ha tehetjük, akkor törekedjünk a dependency inversion használatára is.
-
A Jenga kód olyan kódrészlet, aminek a módosítása az egész program működését befolyásolja, rosszabb esetben el is törheti azt.↩