S.O.L.I.D. – Objektum orientált tervezési elvek 1. SRP

A sorozat részei:

SRP, OCP, LSP, ISP, DIP

 

Objektum orientáltan programozni könnyű. Egy kis osztály itt, egy kis öröklődés ott, interfészek, virtuális metódusok, mifene… Aztán néhány ezer sorral később nyakig ülünk a szarban, mert egy osztályt ki kellene egészíteni egy apró funkcióval, csakhogy szegényke úgy bele van betonozva a hierarchiába, hogy ember legyen a talpán aki egymáshoz igazítja a részleteket.

Kellene tehát valami, ami kifogja a szelet a káosz vitorlájából és alaposan seggbe rúgja az osztályszerkezetet, hogy úgy viselkedjen ahogyan azt elvárjuk. Szerencsére a világban sok okos ember van, közülük pedig Robert C. Martin a.k.a. Uncle Bob volt az aki felhúzta az acélbetétes bakancsot és a világra szabadította a S.O.L.I.D.–ot, ezt a kedves kis betűszót, amelynek minden karaktere egy-egy alapelvet jelképez (ez egyébként vicces: egy betűszó, ami betűszavakból áll).

A S.O.L.I.D. elvek meglepően egyszerűek, de éppen ebben rejlik a nagyszerűségük: öt apró szabályt kell betartanunk (ez néha nem is olyan könnyű), mégis hamar érezni fogjuk a különbséget.

S.R.P. – Single Responsibility Principle

Vágjunk hát bele, kezdjük az SRP-vel ami azt mondja ki, hogy egy osztálynak egy, és pontosan egy oka lehet arra, hogy megváltozzon. Virágnyelvről fordítva ez azt jelenti, hogy egy osztálynak egyetlen feladatot kell ellátnia és csak akkor kell megváltoztatnunk ha ennek a feladatnak a végrehajtási módja megváltozik. Miért jó ez nekünk?

1. Adott osztály kevesebb forráskódot tartalmaz, vagyis: kisebb az esélye, hogy hibát követünk el, de ha ezt mégis megtettük akkor könnyebb a hiba okát behatárolni és megszüntetni.

2. Kevesebb forráskódunk van, ez egyúttal azt is jelenti, hogy egy osztály kevesebb funkcióval rendelkezik, vagyis könnyebbé válik a tesztelés folyamata.

3. A forráskód fejlesztése is egyszerűsödik, hiszen az előző két pont miatt az osztályhierarchia felépítése is feleannyi fejfájást okoz.

Mielőtt továbbmegyünk és megnézzük, hogy mindez hogyan néz ki a gyakorlatban egy dolgot még tisztáznunk kell: a SOLID elvek valóban hasznosak, de nem kell hozzájuk körömszakadtáig ragaszkodni, adódhatnak olyan helyzetek amikor nem feltétlenül nyerünk a használatukkal. Későbbi részekben látunk majd olyan megoldásokat – akár magán a .NET-en belül – amelyek ugyan megszegik ezeket a “szabályokat”, de másképp kényelmetlenséget okoznak a fejlesztőknek (ez lenne az ún. anti-pattern fogalma amely olyan módszertant takar amely nem felel meg a konvencióknak (esetenként nagyon csúnya a megoldás), de szükséges).

Most már jöjjön a lényeg, egy első látásra nem túl életszerű példán fogunk dolgozni, készítsük el az óra osztályt:

class Clock
{
    public string Time { getset; }
}

Jó, nem egy nagy szám, nem is ez a lényeg. Na most, képzeljük el, hogy valamiképpen meg kellene jelenítenünk az aktuális időt valamilyen grafikus felületen. Kérdés: mit csináljunk?

Ötlet 1. :

class Clock
{
    public string Time { getset; }
}

class DisplayedClock : Clock
{
    public void Display() { }
}

Jónak tűnik, de ha valaki figyelt eddig, akkor gyorsan rájön, hogy ez nem fasza, mivel az eredeti osztályunk funkcióját szépen kibővítettük egy olyannal aminek abszolút semmi köze az időméréshez. Sőt, mutatok még mást is: mi van akkor, ha mutatós ÉS digitális óramegjelenítést is akarok? Valami ilyesmi:

class Clock
{
    public string Time { getset; }
}

abstract class DisplayedClock : Clock
{
    public abstract void Display();
}

class AnalogClock : DisplayedClock
{
    public override void Display() { } 
}

class DigitalClock : DisplayedClock
{
    public override void Display() { }
}

Ezen persze lehet csiszolni, de szép soha nem lesz, karbantartható meg pláne nem. Lássuk be: két olyan dolgot akarunk összeházasítani, amiknek a világon semmi közük egymáshoz (arról pedig egyáltalán nem beszéltünk, hogy mi van akkor ha különböző grafikus felületekhez akarjuk hozzáidomítani az osztályainkat, pl. WPF és WinForms).

OK, használjuk férfiagyunk hatalmas erejét és gondolkodjunk egy kicsit! Mit akarunk igazából?

1. Tárolni akarjuk az aktuális időt

2. Meg is akarjuk jeleníteni

3. Ezt többféleképpen is megtehessük

4. Lehetőleg úgy, hogy figyelembe vesszük azt, hogy több platformon is szükségünk van rá

Tehát: elsősorban vegyük észre, hogy az óra osztályból nekünk egyetlen dologra van szükségünk a megjelenítéshez, mégpedig magára az időre, másról senkinek nem kell tudnia. Azt is vegyük még észre, hogy a megjelenítést elvégző programrészről csak annyit kell tudnunk, hogy képes erre, vagyis a felületét – interfészét – ismerjük. Vagyis semmi szükség arra, hogy beleerőszakoljuk az óra osztályba a megjelenítésért felelős kódrészletet, helyette készítsünk inkább két különálló osztályt, sőt használjuk ki azt, hogy a C# ismeri az interfészek intézményét.

Beszéljen helyettem a következő forráskód:

class Clock
{
    public string Time { getset; }
}

public interface IClockDisplayer
{
    void Display(Clock clock);
}

class ClockWithDisplay
{
    private Clock _clock;
    private IClockDisplayer _displayer;

    public ClockWithDisplay(IClockDisplayer displayer)
    {
        this._clock = new Clock();
        this._displayer = displayer;
    }

    public void Display()
    {
        this._displayer.Display(_clock);
    }
}

Csodálatosan egyszerű a megoldás, és gyakorlatilag minden gondunk elszállt: a Clock osztály tesztelhető, mert nincs más dolga mint az időt mérni. Ugyanígy a megjelenítést végző osztályok is ellenőrizhetőek, mert csak egyetlen Clock objektumra van szüksége, az pedig egy rendkívül egyszerű osztály nem függ semmi mástól (nyilván az is egy lehetőség, hogy csak egy stringet adunk át a Display metódus paramétereként, az még inkább függetleníti az osztályt).

Megjegyzés: a megoldásunkba felhasználtunk egy másik S.O.L.I.D. elvet is, egészen pontosan az utolsót, erről majd később.

Látható, hogy egy ilyen apró dolog és egy kis gondolkodás csodákat tehet a forráskóddal. Végeztünk is, legközelebb az O betűvel ismerkedünk, vagyis az Open/Closed Principle-vel.

Reklámok
Tagged with:
Nincs kategorizálva kategória
6 comments on “S.O.L.I.D. – Objektum orientált tervezési elvek 1. SRP
  1. reiteristvan szerint:

    Elfelejtettem bekapcsolni a kommentek ellenőrzését, az eddig feltett kérdésekre most válaszolok:

    “Bocsi, hogy beleronditok, de lenne egy kerdesem a SOLID – SRP -s posthoz (csak ahhoz nem lehet hozzasozlni). Csak annyi hogy lathatnek egy egyszeru hasznalatat a megalkotott vegso osztalynak? Az az interface mint parameter atadas erdekelne… Ott at kell adni egy osztaly nevet, ami megvalositja az IClockDisplayer interfacet? Bocsi a lama kerdesert. Koszi”

    Pontosan, bármelyik osztály ami megvalósítja azt az interfészt megfelel paraméterként, pl.:

    class MyDisplayer : IClockDisplayer
    {

    public void Display(Clock clock)
    {
    //itt megjelenítjük amit kell
    }
    }

    Ezután így használjuk:

    MyDisplayer d = new MyDisplayer();

    ClockWithDisplay cwd = new ClockWithDisplay(d);
    cwd.Display();

  2. hurka szerint:

    Értem, hogy miről szólna a pattern, csak az előnyét nem látom.
    Ez nem egyszerűbb?:

    MyFDisplayer( d = new MyFDisplayer(Clock clock);
    d.Display();

    Minek a ClockWithDisplay osztály és az IClockDisplayer interfész?
    Illetve ha átírjuk pl silverlightos verzióra (ami egy nagyobb projekt esetén nagyon ritkán fordul elő) a displayert, az úgyis egy másik projektben fog szerepelni…

    Kíváncsian várom a folytatást mindenesetre, lehet még csak nem világosultam fel eléggé.

  3. hurka szerint:

    Kicsit elgépeltem a rohanásban:

    MyDisplayer d = new MyDisplayer(Clock clock);

    • reiteristvan szerint:

      “Minek a ClockWithDisplay osztály és az IClockDisplayer interfész?”

      Igen, ezt a devportálon is megkérdezték 🙂 Az igazság, hogy nem feltétlenül kellenek, ahogy azt írtam is egy másik elvet is felhasználtam, az utolsót a Dependency Inversiont, vagyis az interfész és a másik osztály lényegében megvalósít egy egyszerű dependency injection-t.
      Azért lett ilyen a példa, mert az utolsó részben erre (is) akartam hivatkozni, mint ismerős példa, de igazatok van, első ránézésre kicsit túlbonyolított.

      “Illetve ha átírjuk pl silverlightos verzióra (ami egy nagyobb projekt esetén nagyon ritkán fordul elő) a displayert, az úgyis egy másik projektben fog szerepelni…”

      Természetesen, ennyire azért nem életszerű a példa, inkább a koncepció a lényeg.

  4. inf3rno szerint:

    Jól írsz, bár talán ha a trágár szavakat mellőznénk, akkor még jobb lenne. 🙂

    Szerintem is sok volt elsőre felhozni a dependency injection-t, mert nem ehhez a fejezethez tartozik. Később visszautalhattál volna erre a példára, hogy megmutasd hogy mi változik a DI használatával.

    Ami kifejezetten tetszett, hogy a cikk egyszerű, rövid és lényegre törő. (Én is fogok írni egy összefoglaló cikket, amint elolvastam a Clean Code-ot meg a PPP-t, azt hiszem az nem lesz ennyire egyszerű, de azért igyekszem ihletet meríteni ebből is.)

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés /  Módosítás )

Google kép

Hozzászólhat a Google felhasználói fiók használatával. Kilépés /  Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés /  Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés /  Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: