Close

Objektově orientované programování II. – abstrakce a dědičnost

Programovací jazyky - Wordcloud

V předchozím článku ze série věnované programování jsme se dostali k úplným základům objektově orientovaného programování. Dnes budeme pokračovat a vysvětlíme si pojmy abstrakce a dědičnost.


Článek navazuje na jeho první část, kterou naleznete zde.

Abstrakce

V příkladu na konci předchozí části jsme si vytvořili jednoduchou abstrakci nad digitálním výstupem, která slouží k ovládání LED. Už neovládáme výstup tak, že bychom pomocí digitalWrite() přímo nastavovali HIGH a LOW na výstupu, ale ovládáme jej skrze různé metody objektu LED. (Ve skutečnosti i digitalWrite je abstrakcí nad ovládáním registrů řídících jednotlivé piny.)

Třídu z ukázky v praxi pravděpodobně nevyužijeme, protože ovládat LED není nic složitého. Ukazuje nám ale cestu, kterou bychom se mohli vydat u složitějších programů, které ovládají pokročilejší komponenty, nebo mají komplexní chování.

Abstrakce není věcí pouze objektově orientovaného programování a setkáme se s ní i u jiných přístupů. Je to vlastně jeden z nejdůležitějších principů informatiky. Když se podíváte na vývoj programovacích jazyků, který jsem nastínil v prvním článku této série, zjistíte, že od dob strojového kódu, přes assembler, jazyk C a další, dochází k tvorbě vyšších a vyšších vrstev abstrakce. Tento pojem zde ale zmiňuji, protože je na příkladu LED velice dobře zřejmý.

Dědičnost

Jedním z významných pojmů OOP je dědičnost, kterou Wikipedia definuje následovně:

Dědičnost je v objektově orientovaném programování způsob, jak ustanovit is-a vztah mezi objekty. V třídové dědičnosti, kde jsou objekty definované třídami, mohou třídy zdědit atributy a chování od předem existujících tříd, které se nazývají rodičovské třídyzákladní třídy nebo super třídy. Výsledné třídy jsou nazývány odvozené třídy, podtřídy nebo potomek třídy. Koncept dědičnosti byl poprvé zaveden pro jazyk Simula v roce 1968.

Pojďme si tento pojem vysvětlit na příkladu. Vzpomínáte, jak jsme si v minulém článku vytvořili třídu Pes?

Přesuňme se trochu do historie – asi každý ví, že se předkem psa je Vlk, ze kterého se kdysi dávno vyvinul. Jak vypadá takový vlk? Vlk je divoké zvíře, takže rozhodně nebude mít jméno (na rozdíl od našeho psa). Vlk bude mít atribut druh, do kterého si v konstruktoru zapíšeme řetězec „Vlk“ (bude se hodit časem). Dále bude mít vlk určitě konstruktor, aby se mohl „narodit“. Řekněme, že bude umět také štěkat, ale na rozdíl od psa si při tom ještě zavyje. Vlk samozřejmě umí běhat.

class Vlk{
    private: 
        int vyska = 0;
    public:
        String druh = "";
        Vlk(int);
        void stekej(); 
        void behej();   
};

Vlk::Vlk(int vy){ 
    druh = "Vlk";
    vyska = vy;
}

void Vlk::stekej(){
    Serial.println("Vlk vyje Auuuuuuuu! Haf!");
}

void Vlk::behej(){
    Serial.print(druh);
    Serial.println(" beha...");
}

Vytvoření třídy Vlk nás už nepřekvapí. Není v ní nic, co bychom neviděli minule u třídy Pes. Tu si ale nyní trochu upravíme tak, aby dědila vlastnosti od třídy Vlk. Když porovnáme psa a vlka, pes má navíc atribut jmeno. Atribut vyska mají pes i vlk. Stejně tak funkci stekej() – ta je ale jiná, než u vlka. Určitě také bude pes umět běhat. To vše nám dědičnost umožňuje zařídit.

class Pes : public Vlk {
    private: 
        String jmeno = "";
    public:
        Pes(String, int);
        void stekej();
};

Pes::Pes(String jm, int vy) : Vlk(vy) {
    druh = "Pes";
    jmeno = jm;
}

void Pes::stekej(){
    Serial.print(jmeno);
    Serial.print(" steka ");
    Serial.println("Haf!");
}

Co se v kódu děje? Třídu začneme definovat stejně, ale za dvojtečkou následuje jméno třídy, ze které nová třída dědí, společně s klíčovým slovem public. To nám zajistí, že budeme mít ve třídě Pes přístup k public metodám a atributům třídy Vlk. V terminologii OOP se nové třídě (Pes) říká potomek a původní třídě (Vlk) se říká rodič. Mimo modifikátor přístupu public je možné použít ještě private a protected. To už by ale bylo na tento článek moc do hloubky. Pokud by vás ale zajímaly detaily, přesměruji vás sem.

Specifikum objektu Pes je vlastnost jmeno. Dále také musíme uvést, že Pes štěká.

Za definicí konstruktoru (Pes::Pes(String jm, int vy) ) následuje ještě vytvoření konstruktoru rodiče (Vlk(vy)). To nám zajistí nastavení hodnot předaných jako parametry konstruktoru do vnitřních atributů objektu.

Mimo metody a atributy, které jsme přímo uvedli v definici objektu Pes, má objekt pes dostupné všechny public metody a atributy třídy Vlk. Možná si říkáte, proč jsem u Pes uvedl metodu stekej(), když ji má i Vlk. To je zde potřeba, protože tím programu říkáme, že pes bude mít vlastní metodu stekej() a ne stejnou, jako má vlk.

Když si nyní vytvoříme instance objektů Pes a Vlk, uvidíme, že oba mají metodu behej(), kterou jsme sice u objektu Pes explicitně nevytvořili, ale byla zděděna. Oba také mají metodu stekej(), kterou jsme pro každého upravili zvlášť.

Vlk vlk(100);
Pes pes("Punta", 50);

void setup() {
    Serial.begin(9600);
}

void loop() {
    vlk.behej();
    vlk.stekej();
    pes.behej();
    pes.stekej();
    Serial.println("--------------------");
    delay(1000);
}

Praktický příklad

Ukažme si dědičnost opět v praxi. Asi nejjednodušší bude opět využít třídy LED diody, a to v té podobě, v jaké jsme ji minule dokončili. Máme tedy tento kód:

class LED{
    private: 
        int pin;
        boolean stav = LOW; //výchozí stav LED je vypnuto
        void nastav(boolean);

    public: 
        LED(int);
        void zapni();
        void vypni();
        void prepni();
        boolean vratStav();
};

LED::LED(int p){
    pin = p;
    pinMode(pin, OUTPUT);
    digitalWrite(pin, stav);
}

void LED::zapni(){
    nastav(HIGH);
}

void LED::vypni(){
    nastav(LOW);    
}

void LED::prepni(){
    nastav(!stav); //nastaví LED na obrácenou hodnotu (0->1, 1->0)
}

void LED::nastav(boolean s){
    stav = s;
    Serial.print("Nastavuji ");
    Serial.print(stav);
    Serial.print(" na pinu ");
    Serial.println(pin);
    digitalWrite(pin, stav);
}

boolean LED::vratStav(){
    return stav;     
}

Vytvořme si nyní potomka LED, s názvem BlikajiciLED. To bude „konfigurovatelná ledka“, které při vytváření řekneme, s jakou periodou má blikat, a poté už ji jenom necháme blikat.

class BlikajiciLED : public LED{
    private:
        int perioda = 0;
        unsigned long posledniZmena = 0;
    public:
        BlikajiciLED(int, int);
        void blikej();    
};

BlikajiciLED::BlikajiciLED(int pin, int _perioda) : LED(pin){
    perioda = _perioda;
    posledniZmena = millis();
}

void BlikajiciLED::blikej(){
    if(posledniZmena + perioda/2 <= millis()){
        prepni();
        posledniZmena = millis();
    }
}

BlikajiciLED L9(9, 500);
BlikajiciLED L10(10, 1500);

void setup() {
    Serial.begin(9600);
}

void loop() {
    L9.blikej();
    L10.blikej();
}

Privátní parametr posledniZmena má v sobě uložen poslední čas změny stavu dané instance LED. Je tedy pro každý objekt unikátní, což umožňuje, aby více ledek blikalo s různou frekvencí. Také si všimněte, že nikde v kódu není použita jediná funkce delay(), tedy program nikde nečeká. Dalo by se říct, že je řízený událostmi, které jsou závislé na čase. Pokud vám není jasné, co se zde děje, podívejte se na článek Blikání bez funkce delay.


Je něco ve článku nejasné, nebo jsem něco málo vysvětlil? Zeptejte se v komentářích 🙂

Zbyšek Voda

6 Comments on “Objektově orientované programování II. – abstrakce a dědičnost

mirao
19.7.2018 at 19:48

Díky za sérii o objektech. Mám základy ANSI C, ale tohle je pochopitelné.

Zbyšek Voda
19.7.2018 at 19:55
davidhart
10.7.2017 at 4:07

Dobrý den,
příklad je mi celkem jasný, ale co se děje s instancí objektu, když ji již nepotřebuji? Příklad: Nechal jsem chvíli blikat LED pomocí L9.blikej();
Nyní to již nepotřebuji, ale předpokládám, že instance L9, založená na BlikajiciLED, je někde stále v paměti a něco zabírá. Jak se vytvořené instance zbavím?

Předem díky za radu
David

Zbyšek Voda
10.7.2017 at 10:29

Dobrý den, díky za dotaz. Ve článku jsem toto opomněl.
Když vytvoříte objekt v rámci složených závorek – { }, dojde k jeho automatickému uvolnění, jakmile program „vyskočí“ z těchto závorek.
Pokud objekt vytvoříte na začátku programu mimo jakékoliv složené závorky – tedy jako globální objekt, setrvává v paměti po celou dobu běhu programu.
V C++ existuje pro ruční rušení objektů operátor delete. Předpokládá, že uvolí prostor alokovaný pro ukazatel. Vytvoříte tedy objekt, zapamatujete si jeho adresu a tu pak můžete pomocí delete uvolnit.

Při opuštění bloku, ve kterém byl objekt alokován, nebo při použití operátoru delete je volán tzv. destruktor. Ten je možné si vytvořit vlastní, ale je potřeba jen v případě, že byla například v konstruktoru alokovaná nějaká paměť navíc, kterou by bylo potřeba uvolnit při destrukci. Vytváří se podobně, jako konstruktor, jen před název destruktoru vložíte ~.

Použití delete bych se ale vyhnul, protože u globálních objektů většinou chcete, aby setrvaly v paměti po celou dobu běhu programu. U lokálních objektů (ve složených závorkách) se o uvolňování stará samotný systém.

Vláďa
28.7.2016 at 14:37

Dobrý den,
je možně, aby se v příkladu OBJEKTOVĚ ORIENTOVANÉ PROGRAMOVÁNÍ II.
rozblikaly diody jen funkcí např.
blikej(13,500);
blikej(12,1500); // pin, perioda
všechny informace by se vzali z této funkce včetně jména, jméno by bylo č. pinu
a nebyla by potřeba tato deklarace:
BlikajiciLED L9(9, 500);
BlikajiciLED L10(10, 1500);

Vláďa

Zbyšek Voda
28.7.2016 at 18:32

Dobrý den,
ano i ne.
Samotné blikání by bylo možné, ale musel byste někde mít uloženo, kdy jaká LED naposledy blikala. To je nyní uloženo v té instanci objektu, kterou deklarujeme přes blikajiciLED… 🙂

Napsat komentář