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.

    Žádné komentáře:

    Okomentovat