OOP prakticky: Zapouzdřenost

…aneb nikdo nezvaný nesmí k nám do bytu!
24. 12. 2008

V tuto chvíli navrhujeme třídu Proměnná. Co musí třída obsahovat? Tak předně nějaký slot pro ukládání názvu proměnné, to je jasné jako facka v pravé poledne. Ještě něco? Asi ne, to by mohlo stačit. Substituci vyřešíme později. Takže nejprve vytvoříme třídu, která bude obsahovat slot pro uchování názvu proměnné. to bude obyčejný String:

   1: class Promenna
   2: {
   3:     public string Nazev;
   4:  
   5:     public Promenna() { }
   6:  
   7:     public Promenna(string Nazev)
   8:     {
   9:         this.Nazev = Nazev;
  10:     }
  11: }

Připíšeme metodu ToString(), která bude prostě vracet Nazev, nic víc. Pokud se proměnná jmenuje „x1“, asi budeme chtít ve výpisu vidět „x1“.

   1: public override string ToString()
   2: {
   3:     return Nazev;
   4: }

Opět vyzkoušíme:

   1: static void Main(string[] args)
   2: {
   3:     Promenna x1 = new Promenna("x1");
   4:     Promenna x2 = new Promenna("x2");
   5:     Console.WriteLine("{0}, {1}", x1, x2);
   6:     //Výpis bude: x1, x2
   7:  
   8:     Console.Read();
   9: }

Kdo našel chybu? Pokud si někdo všiml jisté nedokonalosti implementace této třídy, má bod. Pro ty méně bystré: my nikde nekontrolujeme, jaký má ta proměnná název. A už jste někdy viděli třeba proměnnou „123456“? Asi ne, protože není moc zvykem, aby se proměnná skládala jen z čísel či aby vůbec začínala číslem. Proto bychom se měli nějak postarat o to, aby se do Názvu uložila jen platná hodnota, což může být například jen slovo, které začíná písmenem a následují opět písmena nebo i čísla. Tedy aby vyhovovaly názvy jako „x1“, „sila10“ nebo jen „rychlost“, ale aby už nevyhověly názvy typu „12x“ nebo „666“. To samozřejmě zařídit lze.

Vzpomínáte si na privátní metody/vlastnosti? Privátní vlastnost je ta vlastnost, která může být modifikována pouze z vnitřku třídy, nikoli zvenčí, pomocí instance. Pokud si vytvoříme instanci pomocí klíčového slova new, nemůže již přes tuto instanci přistupovat k privátním vlastnostem. Ukázka to jistí:

   1: // Ukázková třída s private a public vlastností
   2: class Ukazka
   3: {
   4:     public int verejnaVlastnost;
   5:     private int privatniVlastnost;
   6:  
   7:     public void setPriv(int cislo)
   8:     {
   9:         privatniVlastnost = cislo;
  10:     }
  11:  
  12:     public int getPriv()
  13:     {
  14:         return privatniVlastnost;
  15:     }
  16: }
  17:  
  18: class Program
  19: {
  20:     static void Main(string[] args)
  21:     {
  22:         Ukazka ukazka = new Ukazka();
  23:  
  24:         // To je v pořádku, verejnaVlastnost je public
  25:         ukazka.verejnaVlastnost = 10;
  26:  
  27:         // Kompilátor vyhodí chybu,
  28:         // privatniVlastnost je private, nemůžeme
  29:         // k ní přistupovat přes instanci.
  30:         ukazka.privatniVlastnost = 20;
  31:  
  32:         // To už je OK, k samotné privátní vlastnosti
  33:         // přistupuje vnitřní metoda, která k tomu
  34:         // má opravávnění.
  35:         ukazka.setPriv(10);
  36:  
  37:         Console.Read();
  38:     }
  39: }

Vidíme, že jsme schopni schovat určité vlastnosti tak, aby nebyly přístupné zvenčí tím, že je označíme za private. Nastavování a čtení z této proměnné můžeme udělat pomocí přídavných metod, v našem případě getPriv() a setPriv(). A tady už vidíme postup, jak docílit toho, abychom ve třídě Proměnná před uložením hodnoty zkontrolovali, jestli je daný Název platný. Samotnou vlastnost Název necháme private a sepíšeme si obslužné metody. Přesněji řečeno využijeme vymoženosti C#, který nám dovoluje napsat si vlastní get a set bloky k vlastnostem. Ostatní jazyky mají většinou něco podobného, takže by vás to nemělo nijak překvapit:

   1: private string _Nazev;
   2: public string Nazev
   3: {
   4:     get { return _Nazev; }
   5:     set
   6:     {
   7:         if(platnaPromenna(value))
   8:             _Nazev = value;
   9:         else
  10:             throw new Exception("Neplatný název proměnné.");
  11:     }
  12: }
  13:  
  14: public static bool platnaPromenna(string Nazev)
  15: {
  16:     Regex re = new Regex(@"^[a-zA-Z][a-zA-Z0-9]*$");
  17:  
  18:     return re.IsMatch(Nazev);
  19: }

Ještě jednou přesně popíši, co se bude dít, když se pokusíme do Názvu něco uložit. Jako první se zavolá set blok, ve kterém ověříme, jestli daný Název odpovídá regulárnímu výrazu (ten – jak doufám :-) – ověřuje, jestli daný String začíná písmenem a pak je tvořen jen písmeny nebo čísly. Pokud tomuto reguláru odpovídá, uloží ho proměnné _Nazev předanou hodnotu. V opačném případě vyvoláme výjimku, že se snažíme vytvořit nesmyslnou proměnnou. Hezké, ne?

A teď se zkusme zamyslet, čeho jsme vlastně dosáhli a proč bylo nutné toto dělat. Pokud jsme napsali správné sety a gety, můžeme si být jisti, že se objekt nikdy nedostane do nějakého pofiderního stavu. Ať se budeme snažit sebevíc, nikdy se nám nepovede změnit objekt tak, aby byl nějak nesmyslný. A to je fajn, ne? To je přesně zapouzdřenost. Zapouzdřenost objektu tedy není nic jiného, než důsledná kontrola vstupů a výstupů a obecně celkového stavu objektu. Dodržíme-li pravidla zapouzdřenosti, budeme moci snadněji odlaďovat celý program. Pokud máme chybu v nějaké jiné třídě a ta se bude pokoušet vytvářet proměnnou „111“, okamžitě přijdeme na to, že se někde stala chyba. Typický příklad zapouzdřenosti může být u objektu, který má reprezentovat datum. Představte si, že by vám takový objekt dovolil přiřadit jako číslo měsíce 67. To by asi nebylo moc hezké, že ne? Takovou třídu je ideální vyhodit woknem až do koše. Správně zapouzdřená třída pro datum musí samozřejmě kontrolovat, zda vložené vstupy odpovídají logice našich datumů (já vím, píše se správně dat, ale ono se to pak plete s daty třeba v počítači). Ve chvíli, kdy třída nebude pořádně zapouzdřená, budete nuceni při objevení chyby prohledávat všechny třídy a kontrolovat, kde se vlastně stala chyba. Při správném zapouzdření se okruh hledání značně zúží.

Teď už máme naši třídu Proměnná zčásti hotovou. Umí vrátit svou textovou reprezentaci a je odolná vůči neplatným vstupům. To by nám pro tuto chvíli mohlo stačit. Pojďme se podívat na druhý klíč OOP: