Zapúzdrenie, viditeľnosť a úvod do polymorfizmu

Accendo > Publikované > OOP 4

Autor:                  Libor Bešenyi

Dátum:                2011/05/10

Zmena:                                2011/05/10

 

Zapúzdrenie a viditeľnosť (private / protected / public)

                Čo si predstaviť pod pojmom zapúzdrenie? Skrytie niečoho pred konzumentom triedy. Napríklad také auto, sa nepohne bez motora. Konzument je šofér, ktorý ale nekupuje motor, ale auto. Ako funguje auto ho nezaujíma. Presne na tomto princípúe funguje zapúzdrenie. Ak konzument nechce (nemal by alebo nepotrebuje) vedieť ako funguje trieda, len ju potrebuje používať – môžeme komplexnú logiku zapúzdriť do nového obalu. Potom je ale dôležité vytvoriť také rozhranie, aby vyhovovalo konzumentovi bez toho, aby musel začať upravovať zapúzdrenú logiku. Teda musíme vytvoriť konfortné rozhranie.

                Zapúzdrením teda získame vrstvu „naviac“ – no vhodným využitím zapúzdrenia vieme získať „pomocníka“ v prípade, že je nutné „motor“ v budúcnosti vymeniť. Zapuzdrovať sa zvykne aj business logika (tu sa už nejedná o OOP zapúzdrenie ale princíp je podobný). Všetky podnikové systémy využívajú databázové systémy na ukladanie dát v databázach. Nezapuzdrený prístup môže byť taký, že v celej aplikácií, kde potrebujeme dáta uložiť, alebo načítať, priamo pristupime k databáze. Takto sa nám logika môže „rozutekať“ po celom programe – nehovoriac o duplicite, alebo o zásahu iného člena o ktorom sme nevedeli. Napríklad vznikné nové pravidlo, že keď sa uloží faktúra, musí sa zapísať informácia do logu. Jeden programátor to naprogramuje v module „PridatFakturu“. Iný ale dostane za úlohu napísať nový modul „ImportFaktúr“. Pri importe sa samozrejme vytvárajú nové faktúry – ale keďže to robí iný programátor a nevedel o požiadavke logovať záznam, pri priamom prístupe ku databáze by logovanie v tomto prípade chýbalo (na čo by sa prišlo až pri živej prevádzke a zistovanie chýb / oprava by bola šialene nákladná).

Pri zapúzdrenom princípe (ignorujme teraz storované procedúry) by na komunikáciu s databázou bola vyčlenená jedná globálna trieda, ktorá by mala odprogramované metódy ako PridatFakturu(). V prípade požiadavky o logovaní, by sa to nepogramovalo pre konkrétny (každý jeden) modul, ale len v tejto metóde (lebo každý jeden modul by ukladal faktúry práve cez túto metódu). Tým sa zníži riziko práce v tíme (o nevedomosti toho, ako fungujú vnútorné procesy – každy nemôže vedieť v prípade robustných systémov všetko).

V tomto prípade je centralizácia a zapúzdrenie práce s databázou výhodou aj vtedy, ak sa rozhodneme napríklad zmeniť databázový systém. Vtedy by sme upravovali len „vnútra“ procedúr a nemuseli by sme zasahovať do už otestovaných modulov – tie by volali stále ten istý interface, čo by sa už dialo vo vnútri to moduly nemusí zaujímať.

                Čo sa týka zapúzdrenia v rámci OOP môžeme spomenúť napríklad fungovanie nejakých špeciálnych zariadení. Ak má program napríklad ovládať nejaké špeciálne zariadenie, vytvoríme triedu kde sa komunikácia so zariadením odprogramuje. To ako to funguje konzumenta triedy nemusí zaujímať. Predstavme si situáciu, že kedysi dávno vznikla banková spoločnosť. Tá mala jediného zamestnanca, ktorý viedol evidenciu na svojom PC. Obmedzme sa na bankové operácie vkladu a výberu z účtu (prostredníctvom zamestnanca v banke).

Na začiatku sa využival systém s dvoma vrstvami. Aplikačná vrstva zadávala dáta a dátová ich uchovávala v súbore. Konzumentom v tomto prípade je aplikačná vrstva, ktorá potrebuje zadávať dve operácie (výber a vklad). Ak by sme zapúzdrili logiku– trieda BankoveOperacie by mohla obsahovať dve metódy Vlozit(float ciastka, int uzivatelID) a Vyberat(float ciastka, int uzivatelID). Ako sa pracuje s údajmi v súbore už aplikácia nemusí (a nemá) riešiť – tá len potrebuje vykonávať tieto operácie a v prípade chyby majú tieto metódy vrátiť chybové hlasenie („Nie je možné vybrať 200e – účet nedisponuje takouto čiastkou!“). Ak by sa banka „rozrástla“ o nových zamestnancou, bolo by nutné prejsť na nejaký transakčný systém uchovávania dát (databázové systémy). Program by nebolo nutné meniť, len tela metód by sa nahradili z práce so súborom na prácu s databázou. V budúcnosti, ak by banka zakladala nové pobočky a systém by bolo nutné prevádzkovať na viacerých fizických miestach – metódy by sa mohli zmeniť z databáz na komunikáciu s centrálnym serverom, ktorý by riadil správu dát apod. Takto by sa stále menila komunikačná vrstva, ale nie aplikačná – čo môže ušetriť peniaze v prípade, že systém je robustný, či kritický (minimalizácia chýb pri rozširovaní systému).

                V tomto bode sa dostávame ku viditeľnosti členov. Ak trieda má zapúzdrovať nejakú komplexnejšiu logiku, pravdepodobne bude musieť pracovať so špecifickými „providermi“ – napr. komunikácia s databázou / soap komunikácia apod. Títo pomocníci, ale sú privátne určený pre loguku zapúzdrenia (napr. ak vieme, že databázu môžeme používať LEN cez databázovú vrstvu, musíme zabezpečiť, aby to tak bolo v celej aplikácii – inač naša snaha zapúzdrovať sa rozplynie v hybridnej architektúre).

                Ako teda skrývať jednotlivé členy? To určuje kľúčové slovo private / protected / public. Public je „najvyššia“ viditeľnosť. Private je viditeľná len pre danú triedu a protected len pre odvodené triedy (nie pre inštanciu). Takže príklad public člena sme si už uviedli:

// *************************************************************************

// *** ZakladnaTrieda ******************************************************

// *************************************************************************

public class ZakladnaTrieda

{

// ---------------------------------------------------------------------

private string textC;

protected string TextB;

public string TextA;

} // public class ZakladnaTrieda

 

// *************************************************************************

// *** OdvodenaTrieda ******************************************************

// *************************************************************************

public class OdvodenaTrieda : ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       public void RobNieco()

       {

              TextC = "nieco";

              TextB = "nieco";

              textA = "nieco"; ß CHYBA

       }

} // public class OdvodenaTrieda

 

...

 

// ---------------------------------------------------------------------

private void button1_Click(object sender, EventArgs e)

{

OdvodenaTrieda trieda = new OdvodenaTrieda();

trieda.TextC = "nieco";

trieda.TextB = "nieco"; ß CHYBA

trieda.textA = "nieco"; ß CHYBA

}

 

                Teda ako môžeme vidieť private je viditeľné jedine v rámci jednej triedy, protected vieme používať pri dedení medzi triedami, ale už je to skryté v inštancii triedy (pre konzumenta). Publi je viditeľné vždy. Samozrejme tieto pravidlá platia aj pre funkcie / metódy / konštanty...

Poznámka: Existuje ešte jeden typ viditeľnosti a to je „internal“. Ten sa správa ako public ale LEN v rámci konkrétneho projektu (*.VSPROJ). Teda je viditeľný v assembly, kde sa zadefinuje. Assemblies vieme medzi sebou prepájať (napr. cez DLL alebo SOAP). Internal členy však nebudú viditeľné pre integráciu šturkúr mimo projektu kde boli definované. Takto sa dá „zapuzdrovať“ logika v rámci DLL. Predstavme si DLL, ktorá slúži ako komunikačný modul pre aplikácie v rámci firmy. Táto DLL zapuzdruje širokú paletu služieb centrálneho servera. Aby sa nemusela komunikácia programovať stále pre každý firmený systém, zapúzdri sa do jedného DLL. To DLL samozrejme bude „prešpikované“ komunikáciou so serverom (napr. SOAP), no pomocné triedy / metódy NESMÚ byť publikované z tohto DLL (znova ten istý scénar, ak niečo zapúzdrime, musíme navrhnúť vhodné komunikačné rozhranie – ak sa konzument snaží komunikovať so zapúzdreným zariadením, znamená že je niečo zlé s rozhraním).

Polymorfizmus

                Toto je asi najdôležitejšia a najsilnejšia časť OOP! Polymorfizmus úzko súvisí s dedením – najprv si teda musíme vedieť predstaviť ako vyzerá dedený objekt v skutočnosti. Dedenie a odvodzovanie tried slúži programátorovi lepšie navrhnúť a opakujúce sa časti. Je to teda len „dizajnovacia“ pomôcka, ktorú nemusíme využívať a vieme dosiahnúť podobný efekt (až na polymorfizmus). Zoberme si jednoduchý príklad týchto vzájomne odvodených tried:

    // *************************************************************************

    // *** ZakladnaTrieda ******************************************************

    // *************************************************************************

    public class ZakladnaTrieda

    {

        // ---------------------------------------------------------------------

        public string TextA;

    } // public class ZakladnaTrieda

 

    // *************************************************************************

    // *** OdvodenaTrieda ******************************************************

    // *************************************************************************

    public class OdvodenaTrieda : ZakladnaTrieda

    {

        // ---------------------------------------------------------------------

        public string TextB;

    } // public class OdvodenaTrieda

 

                Ako už vieme, odvodená trieda je v skutočnosti prienikom oboch definícií a teda nová trieda, ktorá vyzerá takto je úplne jej ekvivalentom:

    // *************************************************************************

    // *** PrienikovaTrieda ****************************************************

    // *************************************************************************

    public class PrienikovaTrieda

    {

        // ---------------------------------------------------------------------

        public string TextA;

        public string TextB;

    } // public class PrienikovaTrieda

 

Ak je to tak, načo vlastne triedy dediť? To je správna otázka, ktorú by sme si mali klásť vždy, keď dizajnujeme nejakú hierarchiu tried! Veľa programátorov totiž používa dedenie na uľahčenie si práce (opakujúci sa kód), čo vždy vedie ku neprehľadnému kódu, do ktorého nikto z tímu nechce šahať, pretože každou zmenou sa môže pokaziť niečo úplne nečakane!

Ja som jednoznačne zastáncom využitia OOP hlavne ak má hierarchia polymorfistický význam. V ostatných prípadoch je prehľadnejšie držať duplicitný kód! Takže čo teda je ten polymorfizmus? V preklade sa jedná o mnohotvárnosť – čo nie je až také výstižné. Ku polymorfizmu sa viaže jedno kľúčové slovo override – čo je trocha výstižnejšie, keďže to môže znamenať aj pojem „nahradiť“.

Override: virtual & base

Teraz sa musíme pozrieť na príklad dedenia z príkladu zhora trocha abstraktnejšie. „Prienikom“ dvoch dedených tried je výsledná trieda – to si môžeme predstaviť aj tak, že každú triedu si napíšeme na priesvitnú fóliu a tieto fólie na seba poukladáme. Vo výsledku teda dostaneme prienik všetkých dedenych tried. O to sa vlastne stará dedenie. Pri použití triedy samozrejme bude záležať ešte na viditeľnosti čelnov o ktorom sme si už povedali v predchádzajúcej kapitole.

No zatiaľ vieme len to, ako sa „dedí“ rozhranie triedy. Trieda však je aj samotná logika, ktorá sa samozrejme dedí spolu s rozhraním. V tomto je OOP podľa mňa lepšie vystihnuté v Delphi (Pascal) – kde definícia triedy nie je prepojená s logikou (telom metód).

Vieme teda, že dedením dokážeme vytvoriť prienik dvoch tried, teda nielen premenných, ale aj metód:

// *************************************************************************

// *** ZakladnaTrieda ******************************************************

// *************************************************************************

public class ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       public void TextA()

       {

             MessageBox.Show("A");

       }

} // public class ZakladnaTrieda

 

// *************************************************************************

// *** OdvodenaTrieda ******************************************************

// *************************************************************************

public class OdvodenaTrieda : ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       public void TextB()

       {

             MessageBox.Show("B");

       }

} // public class OdvodenaTrieda

...

// ---------------------------------------------------------------------

private void button1_Click(object sender, EventArgs e)

{

 

       OdvodenaTrieda trieda = new OdvodenaTrieda();

       trieda.TextA();

       trieda.TextB();

}

 

Čo sa stane, keď ale potrebujeme „prekryť“ telo metódy? Najprv sa musíme zamyslieť, či dovolíme niekomu prepisovať „naše“ telo. Ak uznáme za vhodné, žeby to mohlo konzumentovi pomôcť, musíme metódu označiť ako virtuálnu:

// *************************************************************************

// *** ZakladnaTrieda ******************************************************

// *************************************************************************

public class ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       public virtual void Text()

       {

             MessageBox.Show("A");

       }

} // public class ZakladnaTrieda

 

Odvodená trieda, ktorá chce nahradiť telo tejto metódy ju musí prepísať. Na to slúži kľúčové slovo override:

 

// *************************************************************************

// *** OdvodenaTrieda ******************************************************

// *************************************************************************

public class OdvodenaTrieda : ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       public override void Text()

       {

             MessageBox.Show("B");

       }

} // public class OdvodenaTrieda

 

Výslerdok bude „B“ ak teraz vytvoríme inštanciu odovdenej triedy. Samotný fakt, že zavoláme metódu Text() definovanú v ZakladnaTrieda nič neznamená. Pretože jej telo sme nahradili v odvodenej triedy! Niekedy však potrebujeme zabezpečiť aj spúšťanie pôvodneho kódu. To sa volá vždy cez kľúčové slovo base. Base vlastne funguje tak, akoby sme aktuálnu „fóliu“  prevrátili a komunikovali sme opäť s BEZPROSTREDNÝM potomkom triedy:

 

// ---------------------------------------------------------------------

public override void Text()

{

       MessageBox.Show("B");

       base.Text();

}

 

Prečo s bezprostredným – vyskúšajme si „trojité“ dedenie, na tomto príklade pochopíme:

 

// *************************************************************************

// *** ZakladnaTrieda ******************************************************

// *************************************************************************

public class ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       public virtual void Text()

       {

             MessageBox.Show("A");

       }

} // public class ZakladnaTrieda

 

// *************************************************************************

// *** OdvodenaTrieda1 *****************************************************

// *************************************************************************

public class OdvodenaTrieda1 : ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       public override void Text()

       {

             MessageBox.Show("B");

       }

} // public class OdvodenaTrieda1

 

// *************************************************************************

// *** OdvodenaTrieda2 *****************************************************

// *************************************************************************

public class OdvodenaTrieda2 : OdvodenaTrieda1

{

       // ---------------------------------------------------------------------

       public override void Text()

       {

             MessageBox.Show("C");

             base.Text();

       }

} // public class OdvodenaTrieda2

 

Takže výsledkom bude najprv zobrazenie „C“ a potom „B“ ak vytvoríme inštanciu z triedy OdvodenaTrieda2. Pretože ta volá obsah zo svojej odvodenej triedy a tá zasa úplne prekrýva svojho predka (už ho nevolá).

 

Už sme si úkazali ako funguje konšturktor. Tu by som sa chcel zastaviť - konšturktor je automaticky virtuálny (vieme jeho obsah prepísať). V C# je ale dosť výrazné obmedzenie čo sa týka volania tela predka. Dá sa odignorovať, alebo volanie musí prebehnúť na KONCI. Pri virtuálnych metódach, ako vidno, vieme zavolať kód z predka na ľubovoľnom mieste – pri konstruktoroch to ale nie je možné, pretože sa volajú už pri definícii:

 

// *************************************************************************

// *** ZakladnaTrieda ******************************************************

// *************************************************************************

public class ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       private string zobrazovanyText;

 

       // ---------------------------------------------------------------------

       // --- ZakladnaTrieda Konstruktor --------------------------------------

       // ---------------------------------------------------------------------

       public ZakladnaTrieda(string text)

       {

             zobrazovanyText = text + " -> A";

       }

 

       // ---------------------------------------------------------------------

       // --- ZakladnaTrieda Metody -------------------------------------------

       // ---------------------------------------------------------------------

       public void Text()

       {

             MessageBox.Show(zobrazovanyText);

       }

} // public class ZakladnaTrieda

 

// *************************************************************************

// *** OdvodenaTrieda ******************************************************

// *************************************************************************

public class OdvodenaTrieda : ZakladnaTrieda

{

       // ---------------------------------------------------------------------

       // --- OdvodenaTrieda Konstruktor --------------------------------------

       // ---------------------------------------------------------------------

       public OdvodenaTrieda(string text)

             : base(text + " -> B")

       {

       }

} // public class OdvodenaTrieda

...

// ---------------------------------------------------------------------

private void button1_Click(object sender, EventArgs e)

{

       ZakladnaTrieda zakladnaTrieda = new ZakladnaTrieda("text");

       zakladnaTrieda.Text();

       // vysledok Text  -> A

 

       OdvodenaTrieda odvodenaTrieda = new OdvodenaTrieda("text");

       odvodenaTrieda.Text();

       // vysledok Text  -> B -> A

}