neděle 27. února 2011

Ivan Pilný - Příště spadni líp!

Několik nedávných večerů jsem strávil nad knihou Ivana Pilného, prvního šéfa českého Microsoftu. Kniha má podtitul Riskujte a neztrácejte chuť podnikat, který napoví, jaké bude stěžejní poselství celé knihy. Pokud váháte a stále ještě sbíráte odvahu, zda-li přestoupit do podnikatelského světa, tahle kniha Vás trochu popostrčí. Zároveň získáte cenné rady, které se Vám mohou hodit. Nebojte se, že si nabijete ústa, nabila si je i většina nyní úspěšných podnikatelů. Důležité je, abyste se ze svých chyb poučili a příště spadli líp.

Možná se ptáte, proč se recenze na takovou knihu objevuje na vývojářském blogu. Pan Pilný sice glosuje a píše o obecnějších podnikatelských tématech, ale textem prostupuje jeho prvotní specializace - svět počítačů a IT. Já osobně jsem si knihu koupil kvůli zvědavosti nakouknout do zákulisí české pobočky Microsoftu. Dostal jsem sice výživnou porci zákulisních informací, ale nejvíce se mi líbila část, ve které pan Pilný komentuje současný životní styl ovlivněný počítači, mobily, emaily, sociálními sítěmi a dalšími "výdobytky" moderní doby. S nadhledem je komentována "doba stálého vyrušování". Pokud se necháváte cíleně vyrušovat emaily, tweety, esemeskami a dalšími "neodkladnými" informacemi, počítejte s tím, že se Vám prudce snižuje produktivita a dochází u Vás k poruchám soustředění. Možná máte doma BlackBerry sirotky nebo máte vědecky popsanou poruchu dýchání při práci e emaily nebo patříte mezi 15% lidí, kteří (v Americe) čtou nové emaily i v kostele.

Kniha je psána formou krátkých a svěžích dvoustránkových fejetonů. Každý fejeton je autorovým osobním pohledem na problém a je napěchován zajímavými informacemi. Pan Pilný působí v IT oboru už dlouho a je proto nutné brát jeho pohled na svět velice vážně. Můžete se nechat inspirovat člověkem, který si už nevybírá projekty kvůli vidině materiálního zisku, ale podle toho, zda-li je pro něj projekt něčím inspirativní a přínosný. Do tohoto stádia by se asi chtěla dostat většina z nás :-).

Chcete vědět, kterou Vaši mozkovou hemisféru již počítače předběhly ve výkonnosti? Platí na Vás citát "Narodil jsem se jako originál a zemřel jako kopie"? Píšete občas nadřízenému za tepla konfrontační emaily? Máte dostatečnou emoční inteligenci? Jak jste na tom se soft-skills? Nejste postiženi "affluenzou"? Hodilo by se Vám neurolingvistické programování? Víte jak oddálit návštěvu pana Alzheimera?

Zkuste se na svůj soukromý i profesní život podívat očima pana Pilného, určitě nebudete litovat času stráveného čtením. A zkuste si při čtení vypnout Vaše PDA ;-)

Kniha na e-shopu Cpress.

Jaký je Váš názor na tuto knihu?

pondělí 21. února 2011

Kapitální úlovek - pravidla kladená na metody

Kapitální úlovek - maximetoda

Dnes se mi podařilo objevit skutečného maxikapra. Plaval si ukrytý ve stojatých vodách našich objektů a snažil se tvářit nenápadně. Objevil jsem ho čistě náhodou při dohledávání jedné konstrukce. Posuďte sami, zda-li se jedná o vzácný kousek nebo se to ve Vašich projektech takovými chlapáky jenom hemží:

  • Délka metody: 1524 řádků
  • Počet vstupních argumentů: 12
  • Počet lokálních proměnných: více jak 100
  • Počet if konstrukcí: 235
  • Počet switch konstrukcí: 14

Chvilku jsem tento nechtěný úlovek rozdýchával a přemýšlel kolik doporučení a pravidel tato metoda nebohého objektu porušuje. Věřte mi, že hodně. Pojďme se na ně postupně podívat.

Malé metody a ještě menší

Metody by měly být v rozsahu pouze několika málo řádků. Rozumná horní hranice délky metody je 10 řádků. Delší metody ztrácí na přehlednosti a obvykle porušují pravidlo jedné věci - metoda toho dělá příliš mnoho.

V jedné metodě by měla být nejvýše jedna řídící struktura if, switch, while nebo podobná. V každé větvi takové struktury by pak měl být nejvýše jeden řádek, který je zřejmě opět voláním další metody nebo se jedná o přiřazení. Úroveň odsazení ve funkci je jedna maximálně dvě. Autor metody zde prokázal více než velkou oblíbenost if větvení.

Dělejte jen jednu věc

Každá metoda by měla dělat pouze jednu věc. Pokud jich dělá více, je to špatně. Obvykle pak název neodpovídá očekávané funkcionalitě metody a to už je pouze krůček k jejímu chybnému použití a vyvolání problémového aplikačního stavu.

Důležité je umět řešený problém rozložit na různé úrovně abstrakce. Pokud metoda řeší posloupnost kroků na stejné úrovni abstrakce, je to v pořádku a v našem chápání problému řeší pouze jednu věc. Pokud však zamíchá více úrovní abstrakce je to špatně.

Šetřete s argumenty

Jak tvrdí Robert C. Martin, ideální počet argumentů je 0 (12 argumentů si určitě ani nedokáže představit ;-). Tři a víc argumentů již obvykle indikují chybný návrh a měl by to pro Vás být impuls k refaktorování. Metoda pravděpodobně dělá více věcí najednou a míchá úrovně abstrakcí.

Zkušení vývojáři také varují před předáváním logických argumentů (bool hodnot). Obvykle jsou použity uvnitř metody pro rozhodování mezi dvěmi cestami zpracování. To samo o sobě porušuje pravidlo jedné věci. Vytvořte raději metody dvě a volejte v if konstrukci nadřízené úrovně abstrakce konkrétní metodu.

Pokud metoda přesto vyžaduje více argumentů než dva, zauvažujte o sloučení argumentů do objektu typu struktura, kterou pak předáte jako jeden argument.

Výstupní argumenty (ne návratové hodnoty) nepoužívejte. Pokud by měla naše metoda v rámci sebe něco změnit, ať změní pouze vnitřní stav svého objektu.

Vypilujte názvosloví

Přesný a výstižný název metody je velmi důležitý a pro klienta metody je samodokumentující. Klient této metody bude přesně vědět, co od ní může očekávat. Ušetříte mu čas, který by v případě pochybností ztratil tím, že se musí podívat dovnitř metody, jak je implementována. Někdy se ani nemá kam podívat, pokud se jedná o veřejnou metodu nějaké překompilované assembly.

Na druhou stranu by pro Vás měl být název zavazující a neměli byste v metodě dělat nic navíc než jejím názvem deklarujete. V opačném případě zavádíte do metody vedlejší efekty a bude docházet k neočekávanému způsobu použití.

Název je samozřejmě důležitý i u argumentů. Používejte terminologii z domény, kterou metoda řeší. Nepoužívejte více různých termínů pro stejnou věc. Udržujte si doménový slovník.

Chybové stavy a výjimky

Pro indikaci problému při provádění metody nepoužívejte návratovou hodnotu (např. oblíbená -1). Použijte raději výjimky, která jsou pro tyto účely určeny k uchování kontextu chyby. Návratová hodnota je určena k něčemu jinému.

Měli byste mít samostatnou metodu na zachycení výjimky a samostatnou metodu na její zpracování.

Testovatelnost

Zajímalo by mě, jak se taková metoda s 12 argumenty a více než stovkou lokálních proměnnných testuje? Tuny funkcionality, které by měly být refaktorovány do mnoha malých samostatně testovatelných tříd, jsou ukryty v jedné metodě. Testovatelnost je prakticky nulová. Vývoj této metody musel stát extrémně mnoho času.

Robustnost

V tomto ohledu nás zajímá, jak je snadné upravit část kódu tak, abychom splnili požadavek na změnu a zároveň nezanesli chybu na jiné místo algoritmu. Pokud máme problémovou testovatelnost, bude zřejmě nízká i robustnost systému. V algoritmu metody, který je na 1500 řádcích bude velmi problematické provést izolovanou změnu a domyslet všechny její důsledky. Proto se autor rozhodl, že stejný algoritmus v dalším roce zkopíruje do nové metody a tu pouze mírně modifikuje tak, aby původní funkcionalita zůstala zachována. Tím pádem nám vznikl bráška původní metody, který podvýživou také zrovna netrpí. Nešvary mají tendenci se znásobovat!

Neopakujte se

Rozpoznejte zdvojení kódu a refaktorujte ho do samostatné metody. Nedělám si iluze, že naše maximetoda neobsahuje množství zdvojeného kódu.

Závěrem

Dávejte si na podobné programové zrůdky pozor. Mohou se objevit v každém systému, pokud nemáte dostatečné zajišťovací sítě a neprovádíte pravidelnou deratizaci. Zkuste použít statickou analýzu kódu, která Vám pomůže odhalit podobné defektní záležitosti. Používejte a vyhodnocujte metriky kódu, které Vám mnohé napoví o zdraví Vašeho projektu. Dělejte Code Review a upozorňujte se vzájemně na problémy, které může někdo z Vás přehlédnout. Refaktorujte tak často, jak je to jen potřeba.

A hlavně se snažte ze svých chyb poučit. Chybovati je lidské, ale opakující se chyby jsou obvykle drahé a zbytečné a budou Vás strašit tak dlouho, dokud je neopravíte nebo nedáte výpověď ;-)

neděle 20. února 2011

Princip oddělení rozhraní - méně je více

  • Úvod, definice
  • Ukázka porušení principu ISP
  • Návrh s využitím oddělených rozhraní
  • Příklad oddělení rozhraní v .Net frameworku
  • Další zdroje a ukázky
  • Úvod a definice

    Princip oddělení rozhraní (ISP - Interface Segregation Principle) je čtvrtým základním principem objektové návrhové metodologie SOLID. Jeho definice zní:

    Klienti by neměli být nuceni do závislostí na rozhraních, která nepoužívají.

    Princip ISP definuje požadavky kladené na veřejné rozhraní tříd. Doporučuje vyhýbat se návrhu příliš "tlustých" rozhraní. Třída poskytující "tlusté" rozhraní obvykle není soudržná. Třída samozřejmě může nabízet rozhraní pro více typů klientských přístupů, neměli byste však nutit klienta využívat rozsáhlá rozhraní, která jsou pro něj užitečná pouze z části. Rozsáhlá rozhraní raději rozdělte do více menších rozhraní podle typu obsluhovaných klientů.

    Ukázka porušení principu ISP

    Navrhujeme rozhraní abstraktní třídy reprezentující zámek (například u dveří). Zámek můžeme odemykat, zamykat a zjišťovat jeho stav.

    abstract class Zamek
    {
        public abstract void Zamknout();
        public abstract void Odemknout();
        public abstract Boolean JeOdemceno { get; }
    }
    

    V systému máme implementován časovač - třída Casovac a jeho klientské rozhraní - třídu KlientCasovace.

    class Casovac
    {
        public void Registrovat(int casovyLimitVMilisekundach, KlientCasovace klient) 
        { 
            // Implementace časovače.
        }
    }
    
    abstract class KlientCasovace
    {
        public abstract void ObslouzitVyprseniCasovehoLimitu();
    }
    

    Nyní chceme implementovat časový zámek - třída CasovyZamek. Vytvoříme podtřídu abstraktní třídy Zamek. Potřebujeme však využít funkcionalitu třídy KlientCasovace. Zkusíme třídu Zamek nadefinovat jako podtřídu třídy KlientCasovace. Výsledná dědičnost vypadá takto:

    Jak zřejmě tušíte, něco tady nehraje. Tato dědičnost je problematická. Třída KlientCasovace je bázovou třídou třídy Zamek. Všechny podtřídy bázové třídy Zamek jsou v tomto případě nuceni používat rozhraní, vynucené třídou KlientCasovace. Rozhraní třídy Zamek je "znečištěno" rozhraním třídy KlientCasovace. Oddělení funkcionality do samostatných tříd (Zamek, KlientCasovace) ztrácí v tomto případě na užitečnosti.

    Dalším problémem je to, že abstraktní třída Zamek nepotřebuje podporu časovače, přesto je jí vnucena. Měla by tedy implementovat abstraktní metodu ObslouzitVyprseniCasovehoLimitu(). To je samo o sobě problematické. Nemůže totiž implementovat funkcionalitu, kterou potřebuje až její podtřída CasovyZamek. Došlo by k porušení Liskovové principu zaměnitelnosti se všemi problémy, které tento prohřešek přináší.

    Návrh s využitím oddělených rozhraní

    Naším úkolem bude navrhnout třídy tak, abychom se zbavili přímé závislosti mezi třídou Zamek a třídou KlientCasovace. Pomůžeme si návrhovým vzorem Adapter. Vytvoříme třídu CasovacZamkuAdapter, která nám bude adaptovat funkcionalitu abstraktní třídy KlientCasovace. Zároveň bude mít referenci na instanci třídy CasovyZamek. Třída CasovyZamek si vytvoří instanci adaptéru CasovacZamkuAdapter, který převezme zodpovědnost za komunikaci s rozhraním třídy KlientCasovace. Názornější by měl být obrázek:

    Pokud dojde ke změně rozhraní třídy KlientCasovace, bude ovlivněna pouze třída CasovacZamkuAdapter a možná i třída CasovyZamek. Ale nebude ovlivněna třída Zamek ani její případné podtřídy (s vyjímkou třídy CasovyZamek). Implementace tříd by pak mohla vypadat například takto:

    abstract class Zamek
    {
        public abstract void Zamknout();
        public abstract void Odemknout();
        public abstract Boolean JeOdemceno { get; }
    }
    
    class Casovac
    {
        public void Registrovat(int casovyLimitVMilisekundach, KlientCasovace klient) 
        { 
            // Implementace funkcionality časovače.
        }
    }
    
    abstract class KlientCasovace
    {
        public abstract void ObslouzitVyprseniCasovehoLimitu();
    }
    
    class CasovacZamkuAdapter : KlientCasovace
    {
        private CasovyZamek casovyZamek;
    
        public CasovacZamkuAdapter(CasovyZamek casovyZamek)
        {
            this.casovyZamek = casovyZamek;
        }
    
        public void ZapnoutCasovac(int casovyLimitVMilisekundach)
        {
            Casovac casovac = new Casovac();
            casovac.Registrovat(casovyLimitVMilisekundach, this);
        }
    
        public override void ObslouzitVyprseniCasovehoLimitu()
        {
            casovyZamek.Zamknout();
        }
    }
    
    class CasovyZamek : Zamek 
    {
        private Boolean jeOdemceno;
        private int casovyLimitVMilisekundach;
        private CasovacZamkuAdapter casovac;
    
        public CasovyZamek(int casovyLimitVMilisekundach)
        {
            this.casovyLimitVMilisekundach = casovyLimitVMilisekundach;
            casovac = new CasovacZamkuAdapter(this);
        }
    
        public override void Zamknout()
        {
            jeOdemceno = false;
        }
    
        public override void Odemknout()
        {
            jeOdemceno = true;
            casovac.ZapnoutCasovac(casovyLimitVMilisekundach);
        }
    
        public override bool JeOdemceno
        {
            get { return jeOdemceno; }
        }
    }
    

    Třída CasovacZamkuAdapter si v metodě ZapnoutCasovac() vytvoří instanci třídy Casovac a zaregistruje se v něm. Časovač po uplynutí časového limitu vyvolá metodu ObslouzitVyprseniCasovehoLimitu(). V této metodě je vyvolána metoda Zamknout() instance třídy CasovyZamek.

    Alternativním řešením je použít vícenásobnou dědičnost, která však v programovacích jazycích typu C#, VB nebo Java není z objektivních důvodů podporována. V tomto případě by třída CasovyZamek byla podtřídou třídy Zamek a zárověň podtřídou třídy KlientCasovace. Před vícenásobnou dědičností dostává přednost použití více rozhraní, tak jak je to ukázáno na dalším příkladě z .Net frameworku.

    Příklad oddělení rozhraní v .Net frameworku

    Na mnoha místech .Net frameworku je použito oddělení funkcionality do malých soudržných rozhraní. Pokud se například podíváme na deklaraci třídy System.Collections.Hashtable, zjistíme, že implementuje několik relativně malých rozhraní:

    public class Hashtable : IDictionary, ICollection, IEnumerable, 
        ISerializable, IDeserializationCallback, ICloneable
    

    Každé rozhraní je určeno pro klienty s jinou potřebou přístupu k třídě Hashtable. Pro sekvenční procházení příkazem foreach je nutné rozhraní typu IEnumerable, pro serializaci objektu zase ISerializable, pro vytvoření identické kopie instance využijete IClonable. Tento způsob rozložení funkcionality do malých soudržných rozhraní je určitě výhodný a umožňuje třídy používat v různých souvislostech vždy správným způsobem (přes správné rozhraní).

    Další zdroje a ukázky

    Máte nápad, připomínku, našli jste chybu? Přidejte prosím komentář k tomuto článku.

    neděle 6. února 2011

    Liskovové princip zaměnitelnosti - podmínky kladené na dědičnost

    Úvod, definice

    Liskovové princip zaměnitelnosti (Liskov Substitution Principle - LSP) je třetím základním principem objektové návrhové metodologie SOLID. Je definován takto:

    Funkce (metody), které používají ukazatele nebo reference na bázovou třídu, musí být schopny použít objekty jejich podtříd bez jejich znalostí.

    Tento princip nám klade zásadní podmínky na vlastnosti bázových tříd a jejich podtříd (potomků). Definuje, za jakých podmínek je možné vytvářet podtřídy bázových tříd a využívat výhody plynoucí z dědičnosti. Při porušení principu LSP se dostáváme do problémů, jak bude ukázáno v dalším textu.

    Porušení principu LSP - nutná znalost podtříd

    Navrhujeme reprezentaci základních geometrických tvarů. Bázovou třídou je třída Tvar, jejími potomky jsou třídy Ctverec a Kruznice. Metoda VykreslitTvar() zajišťuje vykreslení tvarů různého typu:

    class Tvar { }
    
    class Ctverec : Tvar { }
    
    class Kruznice : Tvar { }
    
    ...
    
    void VykreslitTvar(Tvar tvar)
    {
        if (tvar is Ctverec)
        {
            VykreslitCtverec(tvar as Ctverec);
        }
        else if (tvar is Kruznice)
        {
            VykreslitKruznici(tvar as Kruznice);
        }
    }
    

    Takto definová metoda VykreslitTvar() porušuje LSP, neboť není splněna druhá část definice. Metodě nestačí znalost pouze bázové třídy, ale musí znát i její potomky. Musí vědět, že v systému existují třídy Ctverec a Kruznice a musí je umět přetypovat. Metoda zároveň porušuje princip otevřenosti a uzavřenosti OCP. Pokud vznikne další potomek třídy Tvar například Trojuhelnik, je nutné metodu otevřít a doplnit znalost nové podtřídy.

    Porušení principu LSP - nezaměnitelnost podtříd

    Navrhujeme reprezentaci základních geometrických tvarů. Vznikne bázová třída Tvar a její podtřída pro obdélníky Obdelnik. U obdélníku evidujeme rozměry pro výšku a šířku, které zpřístupníme přes veřejné settery:

    class Tvar { }
    
    class Obdelnik : Tvar
    {
        private double sirka;
        private double vyska;
    
        public double Sirka 
        {
            get { return sirka; }
            set { sirka = value; } 
        }
    
        public double Vyska 
        {
            get { return vyska; }
            set { vyska = value; } 
        }
    }
    

    Požadujeme také třídu Ctverec. Vyjdeme z předpokladu, že čtverec je speciální případ obdélníka a třídu Ctverec nadefinujeme jako podtřídu třídy Obdelnik. Musíme zajistit, aby vždy platila rovnost rozměrů šířky a výšky čtverce. Nabízí se využít settery pro Vysku a Sirku a přidat nastavení i druhé vlastnosti. Dostáváme definici třídy Ctverec:

    class Ctverec : Obdelnik
    {
        public double Sirka
        {
            set
            {
                base.Sirka = value;
                base.Vyska = value;
            }
        }
    
        public double Vyska
        {
            set
            {
                base.Vyska = value;
                base.Sirka = value;
            }
        }
    }
    

    Na první pohled vypadají naše třídy v pořádku a dostavuje se pocit uspokojení. Zkušenější návrhář však začíná pochybovat a hledá na řešení problémová místa.

    Toto přiřazení ještě dopadne podle očekávání:

    Ctverec ctverec = new Ctverec();
    ctverec.Vyska = 1;  // Vyska == 1, Sirka == 1
    

    ... ovšem, pokud logiku nastavení vlastnosti Vyska umístíme do třídy, která předpokládá na vstupu obecnější třídu Obdelnik, dostáváme "neočekávané" chování:

    void F(Obdelnik obdelnik)
    {
        obdelnik.Vyska = 1;
    }
    
    Obdelnik obdelnik = new Obdelnik();
    F(obdelnik);     // Vyska == 1, Sirka == 0, ok
    Ctverec ctverec = new Ctverec();
    F(ctverec);     // Vyska == 1, Sirka == 0 (předpokládali jsme, že bude 1)
    

    K čemu tady došlo? Proč je šířka čtverce 0, přestože v setteru Ctverec.Vyska je i nastavení šířky na stejnou hodnotu? Metoda F() předpokládá na vstupu objekt typu Obdelnik a volání setteru vlastnosti Vyska se tedy omezí na úroveň této třídy. Setter Ctverec.Vyska se vůbec nevyvolá.

    V C# je v tomto případě možné použít modifikátor override, kterým označíme metodu nebo vlastnost podtřídy jako přepsanou. Přepisovat můžeme pouze metody a vlastnosti, které jsou v bázové třídě označené jako virtuální (virtual), abstraktní (abstract) nebo přepsané (override). Přepsané metody nebo vlastnosti budou prováděny až na úroveň příslušné podtřídy i přesto, že se pracuje s bázovou třídou. V našem případě přepíšeme definice tříd Obdelnik a Ctverec takto:

    class Obdelnik : Tvar
    {
        private double sirka;
        private double vyska;
    
        public virtual double Sirka 
        {
            get { return sirka; }
            set { sirka = value; } 
        }
    
        public virtual double Vyska 
        {
            get { return vyska; }
            set { vyska = value; } 
        }
    }
    
    class Ctverec : Obdelnik
    {
        public override double Sirka
        {
            set
            {
                base.Sirka = value;
                base.Vyska = value;
            }
        }
    
        public override double Vyska
        {
            set
            {
                base.Vyska = value;
                base.Sirka = value;
            }
        }
    }
    

    ... a metoda F() se bude chovat očekávaným způsobem i pro čtverce:

    Ctverec ctverec = new Ctverec();
    F(ctverec);     // Vyska == 1, Sirka == 1, ok
    

    Pokud si v tento okamžik pomyslíte, že veškeré problémy s návrhem jsou vyřešeny, zkuste si napsat třeba tuto metodu:

    void G(Obdelnik obdelnik)
    {
        obdelnik.Sirka = 4;
        obdelnik.Vyska = 5;
    
        if (obdelnik.Sirka * obdelnik.Vyska != 20)
        {
            // Chyba
        }
    }
    

    Pokud je na vstupu objekt typu Ctverec, nastane chyba. Přiřazení obdelnik.Vyska = 5 totiž způsobí (pro autora metody G()) nechtěné nastavení šířky čtverce na 5 a podmínka nebude splněna (5 * 5 == 25). Autor metody G() omezil správně funkcionalitu pouze na objekty typu obdélník. To že nadefinujeme podtřídu Ctverec jako potomka bázové třídy Obdelnik a využijeme tuto metodu, nemohl předpokládat. Při chybném návrhu třídy Ctverec jsme porušili Liskovové princip zaměnitelnosti.

    Tento příklad by Vás měl varovat. Vždy pečlivě zvažte, zda-li vztah mezi dvěma třídami opravdu splňuje podmínky kladené na dědičnost. Problémy se mohou projevit později, kdy je pracné chybný návrh přepracovat.

    Podmínky typu předpoklady, následné podmínky a invarianty

    Omezující podmínky jsou základními stavebními kameny v návrhové metodologii nazvané Návrh řízený podmínkami (dohodami) (Design by Contract - DbC). Většina vyšších programovacích jazyků obsahuje syntaktické konstrukty pro zajištění DbC. Např. v .Net Fx 4.0 jsou k dispozici třídy ve jmenném prostoru System.Diagnostics.Contracts.

    Podmínky mohou být trojího typu:

    • Předpoklady (Preconditions) - podmínky, které musí platit před provedením kódu metody nebo vlastnosti.
    • Následné podmínky (Postconditions) - podmínky, které musí platit po provedení kódu metody nebo vlastnosti.
    • Invarianty (Invariants) - podmínky, které musí platit po celou dobu existence objektu.

    Princip LSP definuje požadavky na chování, které musí podtřídy splňovat:

    • Předpoklady nesmí být zesilovány v podtřídách.
    • Následné podmínky nesmí být oslabovány v podtřídách.
    • Invarianty nadřízené třídy musí být zachovány v podtřídách.

    Kovariance a kontravariance

    Pro pochopení vzájemné zaměnitelnosti objektových typů, které jsou v hierarchii dědičnosti, je nutná znalost pojmů kovariance a kontravariance.

    Kovariance (covariance) vyjadřuje možnost do návratové proměnné typu bázové třídy vložit objekt libovolné podtřídy. Třída DemoKovarinace má metody Funkce1() a Funkce2(), které pro delegáta funkce s návratovým typem Obdelnik splňují podmínku kovariance. Funkce2() vrací objekt typu Ctverec, který je podtřídou požadované třídy Obdelnik.

    class DemoKovariance
    {
        public delegate Obdelnik VratitObdelnik();
    
        public static Obdelnik Funkce1()
        {
            return new Obdelnik();
        }
    
        public static Ctverec Funkce2()
        {
            return new Ctverec();
        }
    
        static void Main()
        {
            VratitObdelnik funkce1 = Funkce1;
            Obdelnik obdelnik1 = funkce1();
            VratitObdelnik funkce2 = Funkce2;
            Obdelnik obdelnik2 = funkce2();
        }
    }
    

    Kontravariance (contravariance) vyjadřuje možnost objektovou hodnotu předávanou jako parametr vložit do objektového typu bázové třídy (nadtřídy). Jak ukazuje příklad, delegát funkce PredatCtverec() má v signatuře parametr typu Ctverec. Funkce Funkce2() má na vstupu parametr typu Obdelnik, který je bázovou třídou třídy Ctverec. Kontravariance je tedy splněna, program bude fungovat korektně.

    class DemoKontravariance
    {
        public delegate void PredatCtverec(Ctverec ctverec);
    
        public static void Funkce1(Ctverec ctverec)
        {
        }
    
        public static void Funkce2(Obdelnik obdelnik)
        {
        }
    
        static void Main()
        {
            PredatCtverec funkce1 = Funkce1;
            funkce1(new Ctverec());
            PredatCtverec funkce2 = Funkce2;
            funkce2(new Ctverec());
        }
    }
    

    Princip LSP definuje požadavky na metody podtříd:

    • Kontravariance argumentů metod v podtřídách.
    • Kovariance návratových typů metod v podtřídách.
    • Podtřídy nemohou vyvolat jiné výjimky než výjimky použité v bázové třídě nebo podtřídy těchto výjimek.

    Další zdroje a ukázky

    úterý 1. února 2011

    Princip otevřenosti a uzavřenosti - Open Closed Principle

    Otevřený nebo uzavřený?

    Princip otevřenosti a uzavřenosti (Open Closed Principle - OCP) je druhým základním principem definovaným objektovou návrhovou metodologií SOLID. Jeho definice zní:

    Programové entity (třídy, moduly, funkce, apod.) by měly být otevřeny pro rozšiřování, ale uzavřeny pro modifikaci.

    Pojďme si definici rozložit na jednotlivé podčásti:

    • Otevřený pro rozšiřování. Chování entity může být dále rozšiřováno. Tím je zajištěna reakce na změnu požadavků v aplikaci.
    • Uzavřený pro modifikaci. Zdrojový kód entity je nedotknutelný. Nikdo nemá povoleno kód změnit.
    Možná se Vám teď začínají protáčet mozkošrouby a snažíte se přijít na to, jak tento, na první pohled protichůdný princip, aplikovat v praxi. Nebojte se, rozumím Vám. Vydržte a čtěte dál.

    Klíčem je abstrakce

    Pokud použijeme přímou vazbu mezi konkrétními třídami, které jsou ve vztahu klient - server, stává se klientská třída přímo závislá na serverové. Pokud potřebujeme použít jinou serverou třídu, musíme otevřít klientskou třídu a změnit název serverové třídy. Takový klient je uzavřený vůči změnám, viz. Obrázek 1. Tento návrh porušuje princip OCP.

    Na Obrázku 2. je návrh pozměněn tak, že mezi klientskou a serverovou třídu je vložena abstraktní třída. Klient nepracuje přímo se serverovou třídou, ale s abstrakcí. Za abstraktní třídou je ukryta konkrétní podtřída. Případnou změnou podtřídy není ovlivněn klient, není jej potřeba otevírat a měnit. Klient je otevřený. Návrh respektuje princip OCP.

    V programovacích jazycích se pro zajištění abstrakce využívá buď abstraktní třída (abstract class) nebo rozhraní (interface). Velké množství návrhových vzorů také vzniklo s primární motivací zajištění principu OCP.

    Zkušený objektový návrhář dokáže predikovat chování systému natolik, že odhadne pevné body systému a pro ně navrhne dostatečně abstraktní entity. U abstraktních entit se nepředpokládají časté změny v budoucnu (uzavřenost), naopak se nechává prostor pro různé implementace ukryté za abstraktní vrstvou (otevřenost).

    Příklad - ověřování uživatelů

    Vývojář řeší požadavek na vytvoření variabilního mechanismu ověřování uživatelů. Sledujme jeho myšlenkové pochody a následné prozření. Není úplným nováčkem v objektovém návrhu, proto se rozhodne každý typ ověření uživatele naimplementovat do samostatné třídy. Aktuálně je požadováno ověřování vůči databázi a vůči Active Directory. Vytvoří tedy třídy OvereniUzivateleVuciDatabazi a OvereniUzivateleActiveDirectory:

    public class OvereniUzivateleVuciDatabazi
    {
        public void Overit(String pripojovaciRetezec, String uzivatelskeJmeno, String heslo)
        {
            // ... implementace databázového ověření
        }
    }
    
    public class OvereniUzivateleActiveDirectory
    {
        public void Overit(String nazevDomeny, String uzivatelskeJmeno, String heslo)
        {
            // ... implementace doménového ověření přes Active Directory
        }
    }
    

    Tyto ověřovací moduly budou využívány ve třídě SpravceUzivatelskychOvereni. SpravceUzivatelskychOvereni je klientskou třídou vůči třídám OvereniUzivateleVuciDatabazi a OvereniUzivateleActiveDirectory. Třída pro správu uživatelských ověření zvěřejňuje metodu OveritUzivatele(), která vytváří instanci příslušné ověřovací třídy a následně vyvolá její metodu pro ověření:

    public class SpravceUzivatelskychOvereni
    {
        public void OveritUzivatele(String uzivatelskeJmeno, String heslo)
        {
            TypOvereniUzivatele typOverovani = 
                (TypOvereniUzivatele)Aplikace.Nastaveni["TypOvereniUzivatele"];
    
            switch (typOverovani)
            {
                case TypOvereniUzivatele.VuciDatabazi:
                    OvereniUzivateleVuciDatabazi overeniDatabaze = new OvereniUzivateleVuciDatabazi();
                    overeniDatabaze.Overit(Aplikace.Nastaveni["PripojovaciRetezec"] as String, 
                        uzivatelskeJmeno, heslo);
                    break;
    
                case TypOvereniUzivatele.ActiveDirectory:
                    OvereniUzivateleActiveDirectory overeniActiveDirectory = new OvereniUzivateleActiveDirectory();
                    overeniActiveDirectory.Overit(Aplikace.Nastaveni["NazevDomeny"] as String, 
                        uzivatelskeJmeno, heslo);
                    break;
            }
        }
    }
    
    public enum TypOvereniUzivatele 
    {
        VuciDatabazi,
        ActiveDirectory
    }
    

    Jak asi tušíte, došlo tady k porušení principu OCP. Problematická je metoda OveritUzivatele() třídy SpravceUzivatelskychOvereni, která používá konkrétní třídy OvereniUzivateleVuciDatabazi a OvereniUzivateleActiveDirectory. Je pravděpodobné, že v budoucnu vyvstane požadavek na další typ ověřovacího modulu. Třídu SpravceUzivatelskychOvereni bude nutné otevřít a dopsat další větev příkazu switch. Každá podobná změna vynutí modifikaci kódu, který ovlivní i kód související s klientským použitím třídy OvereniUzivateleVuciDatabazi.

    Příkaz switch je obvykle indikátorem chybného návrhu. Na jednom místě se autor pokouší sloučit rozhodovací a výkonnou logiku. Výkonná logika by měla být řešena v příslušné podtřídě a rozhodovací při vzniku objektu příslušné třídy.

    Příklad - ověřování uživatelů s dodržením OCP

    Pojďme se podívat na návrh očima návrháře, který zná a respektuje OCP. Musíme si uvědomit, co jsou v případě ověřování uživatelů pevné body a co variabilní části. Jak jsme uvedli výše, pro zajištění principu OCP se využívá přidání vrstvy s abstrakcí. Pevným bodem je to, že každý ověřovací modul musí mít metodu, která provede vlastní ověření. Pro tyto účely vytvoříme abstraktní třídu, která bude obsahovat pouze jednu abstraktní metodu Overit():

    public abstract class OvereniUzivateleAbstract
    {
        public abstract void Overit(IParametryOvereniUzivatele parametry);
    }
    

    Metoda Overit() je sice nezávislá na způsobu ověřování, ale ve své implementaci vyžaduje různé vstupní argumenty. Pro ověření vůči databázi potřebuje připojovací řetězec, uživatelské jméno a heslo. Pro ověření vůči Active Directory zase název domény, uživatelské jméno a heslo. Musíme proto využít obecnější mechanismus pro předávání variabilních vstupů. Potřebujeme další abstrakci. Tentokrát zvolíme rozhraní, pojmenujeme jej IParametryOvereniUzivatele a nadefinujeme jednoduše takto:

    public interface IParametryOvereniUzivatele
    {
    }
    

    Parametry ověřování vůči databázi mohou být implementovány například:

    public class ParametryOvereniUzivateleVuciDatabazi : IParametryOvereniUzivatele
    {
        private String pripojovaciRetezec;
        private String uzivatelskeJmeno;
        private String heslo;
    
        public ParametryOvereniUzivateleVuciDatabazi(String pripojovaciRetezec, String uzivatelskeJmeno, String heslo)
        {
            this.pripojovaciRetezec = pripojovaciRetezec;
            this.uzivatelskeJmeno = uzivatelskeJmeno;
            this.heslo = heslo;
        }
    
        public string PripojovaciRetezec
        {
            get { return pripojovaciRetezec; }
        }
    
        public string UzivatelskeJmeno
        {
            get { return uzivatelskeJmeno; }
        }
    
        public string Heslo
        {
            get { return heslo; }
        }
    }
    

    Pokud máme abstraktní třídu OvereniUzivateleAbstract a implementaci rozhraní s parametry pro ověřování vůči databázi, můžeme naimplementovat vlastní ověřování vůči databázi:

    public class OvereniUzivateleVuciDatabazi : OvereniUzivateleAbstract
    {
        public override void Overit(IParametryOvereniUzivatele parametry)
        {
            if (!(parametry is ParametryOvereniUzivateleVuciDatabazi))
            {
                throw new ArgumentException("Parametr není typu ParametryOvereniUzivateleVuciDatabazi.");
            }
    
            ParametryOvereniUzivateleVuciDatabazi parametryOvereni = 
                parametry as ParametryOvereniUzivateleVuciDatabazi;
    
            // ... implementace databázového ověření
        }
    }
    

    Všimněte si, že na vstupu metody Overit() je kvůli abstrakci argument typu IParametryOvereniUzivatele, který si ihned po validaci přetypujeme na ParametryOvereniUzivateleVuciDatabazi. V tomto okamžiku máme vše potřebné pro ověření uživatele přes databázi. Podobným způsobem by se naimplementovalo ověření vůči ActiveDirectory nebo jiné, které má vlastní specifika.

    Nakonec se dostáváme ke třídě SpravceUzivatelskychOvereni, která nám vynutila tuto refaktorizaci. Třída se v našem případě zjednodušila na absolutní minimum. Metoda OveritUzivatele() dostane dvě abstrakce - první pro ověřovací třídu a druhou pro parametry příslušného ověření. Nad těmito abstrakcemi provede vyvolání ověření:

    public class SpravceUzivatelskychOvereni
    {
        public void OveritUzivatele(OvereniUzivateleAbstract overeniUzivatele, IParametryOvereniUzivatele parametry)
        {
            overeniUzivatele.Overit(parametry);
        }
    }
    

    Přidání dalšího ověřovacího modulu v tomto návrhu znamená, že není ovlivněn žádný z existujících modulů ani třída SpravceUzivatelskychOvereni.

    Můžete se ptát, kde a jak vznikají instance ověřovacích tříd a jejich parametrů? Je vhodné využít některý z návrhových vzorů pro vytváření objektů. Konfigurace výběru třídy pro ověření uživatelů může být například v konfiguračním souboru aplikace.

    Princip OCP jako základ pro další mechanismy

    Princip OCP má ve světě objektového návrhu zcela zásadní význam. Velké množství souvisejících mechanismů bylo motivováno právě tímto principem:

    • Princip jednotného přístupu (Uniform Access Principle). Všechny služby poskytované modulem (třídou) by měly být dostupné přes jednotnou notaci. Notace je nezávislá na tom, zda-li je služba implementována nad úložištěm nebo jako vypočítavaná hodnota. V notaci C# se jedná o zpřístupnění obsahu objektu přes property.
    • Návrhy řízené daty (Data-Driven Designs). Pokrývají širokou rodinu technik jako je parametrizace systému z externích zdrojů, metadata pro mapování ORM, skiny oken, apod. Systém je chráněn proti vlivu dat, metadat a deklarativních variant tím, že se odloučí variabilní části ven ze systému a samotný systém je uzavřený.
    • Interpretem řízený návrh (Interpreter-Driven Design). V návrzích tohoto typu čte interpreter pravidla z externího zdroje (skriptu) a provádí je. Používá se v souvislosti s virtuálními stroji, neuronovými sítěmi, množinami omezujících podmínek, apod. Takto lze parametrizovat systém pomocí externí logiky. Systém je opět chráněn externalizací logiky a čtením skrze interpret.

    Někdy se pro OCP používá ekvivalent Protected Variations.

    Další zdroje a ukázky

    Generická modifikace příkladu

    Kolega Radek mi navrhl, abych zkusil použít v příkladě generiku. Upravený kód přikládám. Konstrukce ve třídě SpravceUzivatelskychOvereni nyní vypadají méně přehledně, ale zůstává otázkou, zda-li nově tuto třídu vůbec potřebujeme. Ale to už je předmětem diskuze nad rámec principu OCP.

    public interface IParametryOvereniUzivatele
    {
    }
    
    public abstract class OvereniUzivateleAbstract<TParametryOvereniUzivatele>
        where TParametryOvereniUzivatele : IParametryOvereniUzivatele
    {
        public abstract void Overit(TParametryOvereniUzivatele parametry);
    }
    
    public class ParametryOvereniUzivateleVuciDatabazi : IParametryOvereniUzivatele
    {
        private String pripojovaciRetezec;
        private String uzivatelskeJmeno;
        private String heslo;
    
        public ParametryOvereniUzivateleVuciDatabazi(String pripojovaciRetezec, String uzivatelskeJmeno, String heslo)
        {
            this.pripojovaciRetezec = pripojovaciRetezec;
            this.uzivatelskeJmeno = uzivatelskeJmeno;
            this.heslo = heslo;
        }
    
        public string PripojovaciRetezec
        {
            get { return pripojovaciRetezec; }
        }
    
        public string UzivatelskeJmeno
        {
            get { return uzivatelskeJmeno; }
        }
    
        public string Heslo
        {
            get { return heslo; }
        }
    }
    
    public class OvereniUzivateleVuciDatabazi : OvereniUzivateleAbstract<ParametryOvereniUzivateleVuciDatabazi>
    {
        public override void Overit(ParametryOvereniUzivateleVuciDatabazi parametry)
        {            
            // ... implementace databázového ověření
        }
    }
    
    public class SpravceUzivatelskychOvereni
    {
        public void OveritUzivatele<TParametryOvereniUzivatele>(
            OvereniUzivateleAbstract<TParametryOvereniUzivatele> overeniUzivatele, TParametryOvereniUzivatele parametry)
            where TParametryOvereniUzivatele : IParametryOvereniUzivatele
        {
            overeniUzivatele.Overit(parametry);
        }
    }