SOLID principles – udržitelnost a rozšiřitelnost

SOLIDVelmi často se setkávám se špatně napsanými projekty. Jejich společným znakem je špatná rozšiřitelnost a náročná údržba. Často se nejedná ani o „archivní kousek“ softwaru, ale o úplně nové aplikace. Většina neduhů se na špatně napsaném projektu začne objevovat brzo. Ve chvíli, kdy trávíte zbytečně mnoho času na údržbě systému a přidání „maličkostí“ zabírá neúměrně mnoho času, nastala chvíle na trochu sebereflexe. Je projekt napsán dobře? A jak to poznám? Vývojových technik je sice mnoho, ale snad všechny mají jedno společné. SOLID principles.

SOLID je akronym pro prvních (a nejdůležitějších) pět pravidel objektově orientovaného návrhu. Název poskládal Robert C. Martin (Uncle Bob), nicméně pravidla samotná vznikala jednotlivě a stojí za nimi různí lidé. Tato pravidla se stala základem OOP a jsou synonymem pro snadno udržitelný rozšiřitelný systém. Na jejich základu stojí krom OOP i další známé techniky programování, jako je clean code (čistý kód) nebo extremely defensive programming (defenzivní programování).

  • S – Single-responsiblity principle (princip jedné odpovědnosti)
  • O – Open-closed principle (princip otevřenosti a uzavřenosti)
  • L – Liskov substitution principle (liskovové princip zaměnitelnosti)
  • I – Interface segregation principle (princip oddělení rozhraní)
  • D – Dependency Inversion Principle (princip obrácení závislostí)

Single-responsibility Principle

„Třídy by měly mít jednu zodpovědnost, jeden důvod ke změně.“

single reponsibility principle
Jen proto, že můžete, neznamená, že by jste měli…

Dle mého názoru klíčové a zároveň nejčastěji porušované pravidlo. Není nic horšího, než velké třídy plné nesouvisejících funkcí. Univerzální třídy „na všechno“ prostě nefungují. Třídy píšeme malé, vhodně pojmenované a konkrétní. Jak malá by měla třída být? Čím menší, tím lepší. Pokud by se z vaší třídy dal kus její zodpovědnosti přesunout do jiné, udělejte to. Často se může zdát, že psaní velkého počtu malých tříd znamená více psaní kódu. Popravdě, někdy to tak je. Výhody malého počtu konkrétních tříd se ukážou později, ve chvíli kdy budete kód přidávat nebo měnit. Díky všem možnostem, které nám objektové programování nabízí, pak ušetříme mnoho řádků kódu a mnoho času při hledání, kde že to vlastně je třeba něco změnit.

class File {

    protected $content;

    public function __construct($content) {
        $this->content = $content;
    }

    public function output() {
        echo $this->content;
    } 
}

V ukázce třída File představuje soubor. Co je ale špatně? Funkce output() sem nepatří. Co když budu chtít vypsat content binárně nebo jako json? Odpovědnost za výstup patří jiné třídě. Pokud budu chtít změnit způsob, jakým vypisuji data ze souborů, nechci kvůli tomu měnit třídu File.

Open-closed principle

„Třídy by měly být otevřené pro rozšiřování, ale uzavřené pro změny.“

Open-closed principle
Operace hrudníku není třeba, když si chcete nasadit kabát

Další pravidlo nám říká, že máme psát třídy tak, aby se daly snadno rozšířit bez nutnosti modifikace. Co to vlastně znamená? Když chci do auta přidat radio, nechci kvůli tomu měnit motor. Nové funkcionality by měly jít přidávat do třídy tak, aby nebylo nutné měnit stávající kód. Každá taková změna totiž stojí čas a riziko, že se něco pokazí. Nechme věci, které jsou dobře napsané a fungují tak, jak jsou.

Liskov substitution principle

„Objekty mající stejného předka by měly být zaměnitelné bez nutnosti úpravy kódu.“

Liskov substitution principle
Pokud to vypadá jako kachna, kváká to jako kachna, ale potřebuje to baterky, pravděpodobně máte špatnou abstrakci

Zní to složitě, ale není. Pokud mám třídy mající společného předka, musí být našemu programu jedno, s jakou konkrétní instancí zrovna pracuje. Mám-li objekt Automobil, a jeho potomky Škoda a Fiat, náš program nesmí vůbec zajímat, s jakým autem zrovna pracuje. Vše musí fungovat ať už chceme jet ve škodovce nebo ve Fiatu.

Interface Segregation Principle

„Více specifických rozhraní je lepší než jedno univerzální.“

Interface Segregation Principle
Konkrétní rozhraní je lepší než univerzální.

Stejně jako třídy, tak i abstrakce a interface píšeme malé a konkrétní. Nikdy by jsme se neměli dostat do situace, kdy musíme implementovat nějakou funkci jen proto, že nám to říká rozhraní, ale doopravdy ji nepotřebujeme. Pokud definujeme jeden velký interface, ztrácíme tím přehled, kdo vlastně kterou jeho část používá. V tu chvíli naše rozhraní vlastně ztrácí smysl a stává se spíše břemenem, podle kterého je třeba tvarovat náš kód.

Dependency Inversion Principle

„Závislost je vždy směrem k abstrakci, ne konkretizaci“

Dependency Inversion Principle
Chtěli by jste pájet lampu přímo do zásuvky?

Poslední pravidlo nám říká, že by abstrakce nikdy neměla být závislá na konkretizaci. Pokud si představíme strom našich tříd, kdy vždy třídu nad naší třídou, tedy předka, nazveme jako její abstrakci, a třídu pod naší třídou, tedy potomka, nazveme jako její konkretizaci, nikdy by neměla mít žádná třída závislost v tomto stromu na svém potomku, tedy směrem dolů. Logicky pak lze říct, že potomek by měl být závislý jen směrem nahoru, tedy na svých abstrakcích. Jedná se o často porušované pravidlo, které vede ke zbytečným závislostem. Dalším dobrým důvodem dodržovat toto pravidlo je, že konkretní implementace se mění mnohem častěji, než abstrakce. Obecně není dobré záviset na něčem, co se často mění.

Závěr

SOLID je základem v objektovém programování. Chcete-li vytvářet dobré, snadno rozšiřitelné a dlouhodobě udržitelné aplikace, není použití těchto pravidel otázkou výběru.

2 comments on “SOLID principles – udržitelnost a rozšiřitelnost

  1. Petr

    No, jenže Liskovové princip jste zřejmě nepochopil.

    Příklad čtverce a obdélníku je nejtypičtější ukázkou porušení jeho porušení:

    class Rectangle
    {
    public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
    private:
    double itsWidth;
    double itsHeight;
    };

    a teď:

    void f(Rectangle& r)
    {
    r.SetWidth(32); // calls Rectangle::SetWidth // pro čtverec máte problém, změnila se jen jedna strana
    }

    Liskovové princip má několik pravidel, např. – vstupní podmínky nesmí být přísnější v podtypu, výstupní nesmí být slabší v podtypu, kovariance a kontravariance návratových typů a argumentů…

Napsat komentář: Josef Jadrný Zrušit odpověď na komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *