pondělí 14. března 2011

Princip obrácení závislostí - oddělte rozhraní od implementace

  • Špatný návrh systému
  • Princip obrácení závislostí - definice
  • Příklad aplikace DIP
  • DIP a Separated Interface Pattern
  • DIP a Dependency Injection
  • DIP a související problematiky
  • Další zdroje a ukázky
  • Špatný návrh systému

    Abychom pochopili důležitost principu popisovaného v tomto příspěvku, pokusme se nejdříve zadefinovat charakteristiky špatně navrženého systému. V případě, že rozpoznáme typické chyby v návrhu, budeme schopni těmto chybám lépe předcházet. Vyjdeme z definice Roberta C. Martina, který poukazuje na tři základní charakteristiky chybného návrhu:

    • Nepružnost (rigidity). Je obtížné provést změnu, neboť ta se projeví na spoustě jiných míst v systému. Jednoduchou změnu není možné provést bez nutnosti upravit závislé části systému. To může vynutit kaskádový efekt změn. Důsledky změny je pak těžké odhadnout a je tedy těžké odhadnout i skutečné náklady na změnu. Pokud systém netrpí nepružností je flexibilní.
    • Křehkost (fragility). Při provedení změny se nepředpokládaně projeví problémy na jiných místech systému. Změny se mohou projevit i na místech, které nemají přímou souvislost s měněnou oblastí. Uživatelé a manažeři systému nejsou schopni predikovat kvalitu systému. Opravy takových problémů mohou zanést další problémy. Pokud systém netrpí křehkostí je robustní.
    • Imobilita (immobility). Je obtížné část systému osamostatnit a použít v jiném systému. Důvodem je vysoká závislost a provázanost mezi částmi systému. Návrháři jsou zastrašeni množstvím práce, které obnáší osamostatnění části systému a uchylují se k duplicitám a znovuvyvíjení podobných částí. Pokud systém netrpí imobilitou je znovupoužitelný.

    Co je příčinou takto definovaného chybného návrhu? Je to vysoký stupeň závislostí mezi částmi (komponentami) systému. Pokud se nám podaří snížit stupeň provázanosti jednotlivých částí systému, náš systém se stane flexibilnějším, robustnějším a znovupoužitelnějším. Princip obrácení závislostí nám dává doporučení, pomocí kterých je tento cíl lépe dosažitelný.

    Princip obrácení závislostí - definice

    Princip obrácení závislostí (DIP - Dependency Inversion Principle) je pátým základním principem objektové návrhové metodologie SOLID. Jeho definice zní:

    A. Moduly vyšší úrovně by neměly být závislé na modulech nižších úrovní. Obojí by měly být závislé na abstrakci.
    B. Abstrakce by neměla záviset na detailech. Detaily by měly záviset na abstrakcích.

    V "tradiční" (naivní) architektuře mohou být komponenty na různých vrstvách ve vztahu závislostí jak je uvedeno na obrázku. Komponenta A je na vyšší úrovni než komponenta B a je na ní závislá. Komponenta B je závislá na komponentě C. Transitivita v tomto případě neplatí, tzn. že komponenta A nezávisí na komponentě C.

    DIP nám radí odstranit přímou závislost komponent tím, že použijeme abstrakci (v tomto případě rozhraní). Konkrétně komponenta A zadefinuje rozhraní, které požaduje pro svoji funkcionalitu ("Požadované rozhraní komponentou A"). Toto rozhraní je ve veřejné části komponenty A. Komponenta A nic neví o komponentě B. Komponenta B zná z balíčku komponenty A pouze veřejné rozhraní "Požadované rozhraní komponentou A" a toto rozhraní realizuje. Tím se stane komponenta B "kompatibilní" pro komponentu A, aniž by se implementace obou komponent "znaly". Rozhraní "Požadované rozhraní komponentou A" reprezentuje funkční dohodu mezi komponentou A a komponentou B a obecně mezi dalšími komponentami, které rozhraní realizují. Situace by měla být zřejmá z obrázku:

    Někdy se můžete setkat s UML notací (viz. další obrázek), ve které jsou zachycena "vystavená" rozhraní komponent. Komponenta A vyžaduje rozhraní (otevřený půlkruh) "Požadované rozhraní komponentou A". Komponenta B stejné rozhraní nabízí (uzavřený kruh). Proto jsou schopny vzájemně komunikovat.

    Z předchozích obrázků je zřejmé, proč se princip nazývá principem "obrácení závislostí". Komponenta A byla původně závislá na komponentě B. Po aplikování principu již komponenta A nezávisí na komponentě B, ale ta závisí na veřejném rozhraní komponenty A. Závislost se obrátila.

    Příklad aplikace DIP

    Je požadován systém, který bude kopírovat znaky zadané z klávesnice na tiskárnu. Program by mohl vypadat třeba takto:

    namespace KopirovaniZnaku
    {
        public class SpravceKopirovani
        {
            void KopirovatZnaky()
            {
                char znak;
                while ((znak = NacistZKlavesnice()) >= 0)
                {
                    VypsatNaTiskarnu(znak);
                }
            }
            
            ...
        }
    }
    

    Základem je metoda KopirovatZnaky(), která postupně načítá znaky z klávesnice a vypisuje je na tiskárnu. Implementace privátních metod NacistZKlavesnice() a VypsatNaTiskarnu() není důležitá. Důležitá je však závislost třídy SpravceKopirovani na implementaci čtení z klávesnice a zápisu na tiskárnu.

    Problém našeho řešení nastane v momentě, kdy se objeví nový požadavek, aby bylo možné znaky vypisovat nejen na tiskárnu, ale také na monitor. V tento okamžik musíme přidat rozhodování na místo v programu, kde se implementuje výpis výstupu. Přidáme také výčtový typ VystupniZarizeni pro reprezentaci typu výstupního zařízení.

    namespace KopirovaniZnaku
    {
        public enum VystupniZarizeni
        {
            Tiskarna,
            Monitor
        }
    
        public class SpravceKopirovani
        {
            void KopirovatZnaky(VystupniZarizeni vystup)
            {
                char znak;
                while ((znak = NacistZKlavesnice()) >= 0)
                {
                    switch (vystup)
                    {
                        case VystupniZarizeni.Tiskarna:
                            VypsatNaTiskarnu(znak);
                            break;
                        case VystupniZarizeni.Monitor:
                            VypsatNaMonitor(znak);
                            break;
                    }
                }
            }
            
            ...
        }
    }
    

    Program se nám komplikuje a začínáme tušit, že narůstá počet přímých závislostí mezi třídou SpravceKopirovani a implementací čtení a zápisu pro různá vstupně-výstupní zařízení. Vzpomeňme si na princip obrácení závislostí a odstraňme tyto přímé vazby. Zadefinujeme rozhraní IVstupniZarizeni, které nám bude reprezentovat vstupní zařízení a bude nabízet metodu pro čtení znaku. Podobné rozhraní bude potřeba pro výstupní zařízení (IVystupniZarizeni), které bude nabízet metodu pro zápis znaku.

    namespace KopirovaniZnaku
    {
        interface IVstupniZarizeni 
        {
            char NacistZnak();
        }
    
        interface IVystupniZarizeni 
        {
            void VypsatZnak(char znak);
        }
    
        public class SpravceKopirovani
        {
            void KopirovatZnaky(IVstupniZarizeni vstup, IVystupniZarizeni vystup)
            {
                char znak;
                while ((znak = vstup.NacistZnak()) >= 0)
                {
                    vystup.VypsatZnak(znak);
                }
            }
        }
    }
    
    namespace Klavesnice
    {
        public class Klavesnice : KopirovaniZnaku.IVstupniZarizeni
        {
            public char NacistZnak()
            {
                // Implementace načítání znaků z klávesnice
            }
        }
    }
    
    namespace Tiskarna
    {
        public class Tiskarna : KopirovaniZnaku.IVystupniZarizeni
        {
            public void VypsatZnak(char znak)
            {
                // Implementace výpisu znaku na tiskárnu
            }
        }
    }
    

    Vytvořili jsme programové komponenty KopirovaniZnaku, Klavesnice a Tiskarna. Ty spolu komunikují přes rozhraní IVstupniZarizeni a IVystupniZarizeni, která zadefinovala komponenta KopirovaniZnaku. Tato komponenta je na vyšší úrovni než komponenty Klavesnice a Tiskarna. Podle principu DIP nemáme v systému žádnou přímou závislost mezi implementacemi. Závislost je pouze na rozhraní. Systém nyní zřejmě splňuje požadavky na dobrý návrh, viz. úvodní kapitola příspěvku.

    DIP a Separated Interface Pattern

    Z principu DIP plyne, že bychom se měli ve fázi návrhu systému soustředit spíše na definici rozhraní mezi komponentami než na úvahy o vlastní implementaci. A vlastní rozhraní pak logicky oddělit od implementačních detailů. Tato programovací technika bývá označována jako "Programování vůči rozhraní, ne vůči implementaci".

    Jeden z návrhových vzorů je mírnou modifikací původního DIP a řeší způsob uložení veřejného rozhraní komponenty. Vzor se nazývá Separated Interface Pattern a doporučuje oddělit veřejné rozhraní komponenty a její vlastní implementaci do samostatných balíčků. Toto oddělení přináší výhody při referencování mezi balíčky. V případě DIP bylo rozhraní součástí balíčku komponenty vyšší úrovně. Bylo proto úzce spjato s klientskou stranou rozhraní, která nesla za rozhraní zodpovědnost. V případě SIP je rozhraní odděleno i od této komponenty a tím je zajištěno, že za vývoj rozhraní není zodpovědná pouze klientská strana.

    Na obrázku je vidět, že komponenta A závisí na rozhraní komponenty B. Rozhraní komponenty B je uloženo v samostatném balíčku. Implementace komponenty B je v samostatném balíčku a realizuje toto rozhraní.

    DIP a Dependency Injection

    Dependency Injection (DI) je množina návrhových technik souvisejících s DIP. Řeší zodpovědnost za zajištění závislostí komponent na externích zdrojích. Cílem Dependency Injection je oddělit problematiku získání závislostí od vlastních komponent.

    Jedna z technik DI, nazývaná Constructor Injection, definuje závislosti mezi komponentami s využitím předávání závislých komponent pomocí argumentů konstruktorů. Tzn. závislosti jsou ustanoveny v době vzniku komponenty.

    Pro uplatnění principů DI se používají různé frameworky označované jako Inversion of Control frameworks.

    DIP a související problematiky

    Jak je patrné z předchozího textu, princip DIP vnáší do návrhu systému prvky flexibility a snižuje vzájemné propojení komponent. Díky těmto vlastnostem mohou být systémy otevřené pro použití zásuvných modulů - plug-inů (návrhový vzor Plug-in). Jedná se o externí implementace (třetích stran), které respektují a realizují požadované rozhraní.

    Můžete se také seznámit s návrhovým vzorem Service Locator, který je určen pro registraci a lokalizaci služeb za běhu programu. Není zodpovědný za vznik instance služby, proto se často kombinuje se vzory typu Factory Pattern a (nebo) Dependency Injection.

    Příjemným důsledkem oddělení rozhraní a implementace je možnost použití mockovacích frameworků při testování komponent. S využitím rozhraní můžete snadno vytvořit příslušnou mock implementaci. Vytvoříte falešnou implementaci, která provádí to, co v dané situaci vyžadujete. Více o použití mocků v některém z budoucích příspěvků.

    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.

    pátek 4. března 2011

    Tři, dva, jedna, Android, start

    Patříte mezi fanoušky platformy Android a chcete si vyzkoušet napsat nějakou aplikaci? Zajímá Vás, co všechno k vývoji aplikací na platformě Android potřebujete? Hledáte stručný úvod do problematiky? Pak určitě pokračujte ve čtení.

    Firma Google je se svojí platformou pro mobilní zařízení velice úspěšná. Denně se celosvětově aktivuje více než 300 000 nových zařízení s Androidem. Jedná se aktuálně o nejrychleji rostoucí platformu na trhu mobilních zařízení. Potenciál trhu s Android aplikacemi je tedy značný. Možná, že se nakonec prosadí aplikace, kterou napíšete právě Vy!

    Základy architektury platformy Android

    Android je softwarová platforma sestávající z operačního systému, middleware vstvy a vlastních koncových aplikací. Operační systém je založen na Linuxu verze 2.6 a zajišťuje základní systémové služby jako zabezpečení, správu paměti, správu procesů, řízení sítě a další. Middleware vrstva nabízí služby a knihovny, které využívají koncové aplikace.

    Aplikace jsou napsané v Javě a jsou spouštěny v Android Runtime. Každá aplikace běží v samostatném procesu a má k dispozici vlastní instanci Dalvik Virtual Machine. Tím je zaručena vzájemná autonomita a ochrana při současném běhu více různých aplikací.

    Popis architektury na stránkách Androidu.

    Co potřebuji k vývoji aplikací?

    • Java Development Kit (JDK). Vývojový framework pro Java aplikace do firmy Sun, kterou nedávno koupil Oracle. Je to základní množina nástrojů a knihoven pro vývoj Java aplikací. JDK instalujete jako první. Aktuální verze je 1.6.0_24 (Stažení JDK). Pozor, ať si nenainstalujete Java Runtime Environment (JRE). Jedná se o framework určený pouze ke spouštění Java aplikací.
    • IDE Eclipse. Pro Android je doporučeno integrované vývojové prostředí Eclipse ve verzi 3.6. (Helios) a vyšší. Samozřejmě lze použít i jiná IDE, ale pro Eclipse existuje nejlepší podpora ze strany autorů Android platformy. Stáhněte si verzi Eclipse v edici Classic a vyšší. Viz. http://www.eclipse.org/downloads/.
    • Android SDK. Jedná se o sadu nástrojů a middleware knihoven pro podporu vývoje Android aplikací. Nejprve nainstalujete správce Android SDK, ke stažení zde. Správce Vám pak pomůže s instalací jednotlivých komponent SDK. Platforma Android se stále vyvíjí a od počáteční verze 1.1 se přes (významné) verze 1.6, 2.1, 2.2, 2.3 probojovala až k aktuální verzi 3.0. Můžete si nainstalovat pouze některé z verzí API nebo všechny.
    • ADT Plugin pro Eclipse. Zásuvný modul pro vývoj Android aplikací v Eclipse. Díky tomuto rozšíření máte v Eclipse k dispozici podporu pro Android projekty, pro vytváření UI, pro spouštění a debuggování ve virtuálních nebo skutečných Android zařízeních. Plugin se instaluje v Eclipse jako ostatní rozšíření, více na stránce s popisem instalace.
    • Testovací zařízení. Budete potřebovat na něčem vyvíjené aplikace spouštět pro účely otestování a ladění. K dispozici máte podporu virtuálních zařízení (AVD - Android Virtual Devices). Jedná se o virtuální emulátory, u kterých můžete nastavit verzi API, rozlišení v pixelech a další vlastnosti, které mají skutečná mobilní zařízení. AVD můžete mít v systému několik. AVD jsou spravovány Správcem Android SDK. Aplikaci vyvíjenou z Eclipse můžete samozřejě spouštět oproti skutečnému zařízení, které je připojeno k počítači přes USB port. V tomto případě musíte doinstalovat USB ovladač, který je specifický pro příslušný typ zařízení (HTC, Sony, ...).

    Popis přípravy vývojového počítače je k dispozici na webu Android Developers.

    Čím začít?

    Určitě si vyzkoušejte vytvořit jednoduchou Hello World aplikaci podle tohoto návodu. Seznámíte se se základními pojmy a principy vývoje Android aplikací.

    Až se trochu rozkoukáte a pozdravíte svět Androidu, pokračujte v dalších tutoriálech. Autoři Vás za ruku provedou problematikami návrhu UI, lokalizací aplikací, testováním i složitější aplikací na vkládání poznámek.

    Jak pokračovat?

    Pokud jste na tom podobně jako já, budete si muset zopakovat syntaxi jazyka Java. Sice jsem v Javě kdysi napsal jednu (stále ještě živou) aplikaci, ale od té doby jsem výrazně zdotnetil. Každopádně dobrá znalost syntaxe a principů jazyka Java je pro vývoj Android aplikací nezbytná.

    Budete se muset detailně seznámit s API Androidu a na příkladech je pochopit a osahat si je v praxi.

    Bude se Vám také hodit obecnější znalost principů programování a navrhování aplikací. Aplikace pro Android mají sice svá specifika, ale spousta obecných principů se samozřejmě dá aplikovat i zde. Můžete se nechat inspirovat příspěvky na tomto blogu ;-).

    Navštěvujte a aktivně přispívejte do vývojářských diskuzí a fór (i fórkem). I na českém Androidím rybníku to začíná ožívat. K dispozici je portál www.svetandroida.cz a spřátelené diskuzní fórum www.androidforum.cz.

    Podařilo se Vám naistalovat prostředí pro vývoj Android aplikací? Na jaké konfiguraci vyvíjíte? Máte nějaké tipy nebo rady?