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.↩