Dedičnosť, t.j. rozširovanie tried

<< Vytvárame funkčné triedy | Obsah | Polymorfizmus >>

Stalo sa, čo ste nepredpokladali. Vaša rodina bola nadšená vašim zoznamom DVD-čiek a chce od Vás, aby ste evidovali aj všetky filmy na videopáskach a v počítači. Samozrejme centrálne v jednom programe. Navyše im už nestačia informácie ako sa film volá, kto v ňom hrá, aké má žánre, dĺžku a hodnotenie. Chcú vedieť, kde film hľadať. Ale ono to je závislé na médiu, kde je film uložený:

  • DVD
    • Máme ich očíslované - rodičia sa snažili urobiť poriadok, na každé DVD-čko nalepili čísielko a vedú si ošuntelý a niekoľkokrát kávou poliaty zoznam. Samozrejme, niečo v tom zozname nájsť je horor, lebo filmy sú zotriedené podľa čísla (v akom poradí sa nakupovali) a nie podľa abecedy.
  • Pásky
    • tiež majú číslo
    • no keďže ide o nahrávky z TV, tak potrebujeme evidovať aj od ktorej minúty na páske daný film začína
  • Súbory v počítačoch
    • treba evidovať, v ktorom počítači je uložený
    • treba evidovať aj úplnú cestu k súboru kde je uložený
    • dôležitá je aj veľkosť súboru

Filmy na všetkých médiách majú nejaké spoločné dáta a funkcionalitu. Film na ľubovoľnom médiu si zachováva pôvodné dáta:

  • názov filmu
  • hercov, ktorý v ňom hrali
  • žánre, do ktorých spadá. Predpokladáme že film môže mať viac žánrov (napr. "kriminálka a thriller" alebo "romantika, komédia a rodinný")
  • dĺžku filmu
  • vaše hodnotenie kvality filmu na stupnici od nula do desať.

Rovnaké budú aj metódy spoločné pre filmy na všetkých médiách - mamZaner, mamHerca, retazecPreVypisFilmu

Rozdielne dáta sa týkajú identifikácie média, na ktorom sa film nachádza (očíslovanie, identifikácia počítača a cesty) a tiež niektoré doplňujúce údaje (začiatočná minúta, veľkosť súboru).

Rozdielne bude aj chovanie niektorých funkčných schopností (výpis umiestnenia filmu, uloženie do súboru, dodatočné informácie)

Dobrý programátor sa snaží každú metódu písať v celom programe iba na jednom mieste, v jednej jedinej kópii. Dôvod je zrejmý. Programátor pamätá na to, že každá metóda má veľkú šancu na svoju zmenu z rôznych dôvodov (prídu dáta s ktorými sa pred tým nerátalo, zákazník chce zmenu chovania metódy, Y2K,...) a v prípade jej zmeny to robí len na jednom mieste. Výhody sú tieto:

  • netreba veľa písať
  • neprihodí sa to, že sme niektorú kópiu metódy zabudli prepísať (niekedy sa to veľmi ťažko odhaľuje, lebo program v 99% prípadov funguje dobre)
  • pri neskorom odhalení zabudnutia zmeny v niektorej z kópií sa môžu dáta stať nekonzistentné, čo je často spojené s ťažkým a náročným opravovaním, ak je vôbec oprava možná

Z toho vyplýva, že nie je dobré, keby sme našu situáciu s troma druhmi médií pre filmy vyrobili 3 triedy (FilmNaDvd, FilmNaPaske, FilmVPocitaci) a každá z nich by zisťovala napríklad prítomnosť herca vo filme svojou metódou s rovnakým telom. Mohlo by sa nám totiž stať, že zrazu niekto zahlási, že meno herca nestačí, lebo o hercoch chceme uchovávať aj národnosť, rasu, ocenenia a podobne. To by viedlo k tomu, že hercov nebudeme uchovávať ako pole String-ov ale ako pole typu Herec[]. Nasledovalo by trojnásobné prepisovanie typu inštančnej premennej herci a trojnásobné prepisovanie metódy mamHerca (a to náš program je ešte malý).

Využijeme dedičnosť, ktorú ste doteraz poznali pod názvom rozširovanie tried. Vyrobíme si triedu pre premenné a metódy, ktoré sú spoločné pre všetky typy médií. Následne vytvoríme triedy, ktoré rozširujú túto triedu o ďalšie vlastnosti. Táto spoločná trieda sa bude nazývať Film a z nej rozšírené (oddedené) triedy sa budú volať FilmNaDvd, FilmNaPaske a FilmVPocitaci. Tieto triedy budú môcť používať všetky metódy rodičovskej (rozširovanej) triedy Film. Na podobné správanie sme už zvyknutí pri rozširovaní Turtle alebo WinPane.


public class Film {

        private String nazovFilmu;
        private String[] herci;
        private String[] zanre;
        private int dlzkaFilmu;
        private double hodnotenie;

        public boolean mamHerca(String herec) {
                ...
        }

        public boolean mamZaner(String zaner){
                ...
        }

        public String retazecPreVypis() {
                ...
        }

   //nasledujú gettery a settery
}
 
public class FilmNaDvd extends Film {
        private int cisloDvdcka;

        public String dajUmiestnenie() {
                return "DVD číslo "+cisloDvdcka;
        }

...
}
public class FilmNaPaske extends Film {
        private int cisloPasky;
        private int zaciatok;

        public String dajUmiestnenie() {
                return "Páska číslo "+cisloPasky+ ", "+ zaciatok+". minúta";
        }
...
}
public class FilmVPocitaci extends Film {
        private String nazovPocitaca;
        private String cestaKSuboru;
        private int    velkostSuboru;

        public String dajUmiestnenie() {
                String vysledok ="Počítač "+nazovPocitaca + "\n";
                return vysledok = vysledok + cestaKSuboru;
        }

        public int getVelkostSuboru(){
                return velkostSuboru;
        }
...
}

Pre celkový obraz sa používa takzvaný triedový diagram, ktorý znázorňuje, ktorá trieda je rozšírením ktorej (resp. ktorá trieda dedí od ktorej).

Keď máme už tieto triedy naprogramované, môžeme prispúpiť k renovácii zoznamu filmov. Keďže máme médiá troch druhov zrejme by ste očakávali, že vytvoríme tri polia:

public class ZoznamFilmov {
        private FilmNaDvd[]     filmyDvd;
        private FilmNaPaske[]   filmyPasky;
        private FilmVPocitaci[] filmyPocitac;
...            
}

a potom pri výpise, ale aj pri všetkých ostatných metódach budeme používať 3 cykly:

public class ZoznamFilmov {
...
        public void vypisVsetko() {
                for (int i = 0; i < filmyDvd.length; i++) {
                        System.out.println(filmyDvd[i].retazecPreVypis());
                }
                for (int i = 0; i < filmyPasky.length; i++) {
                        System.out.println(filmyPasky[i].retazecPreVypis());
                }
                for (int i = 0; i < filmyPocitac.length; i++) {
                        System.out.println(filmyPocitac[i].retazecPreVypis());
                }
        }
...            
}

Takýto prístup našťastie nie je potrebný, pretože platí, že premenná majúca typ nadradenej triedy (v našom prípade Film) dokáže referencovať aj objekt, ktorý je inštanciou ľubovoľnej triedy, ktorá je jej potomkom (v našom prípade napr. FilmNaDvd). Inými slovami z premennej typu Film vieme referencovať objekty typu Film, FilmNaDvd, FilmNaPaske alebo FilmVPocitaci:

Film matrix = new Film();
Film matrix2 = new FilmNaPaske();

Cez premenné matrix a matrix2 však môžeme volať iba tie metódy, ktoré sú definované v triede Film alebo jej predkoch. Nasledujúce teda nie je možné:

String s = matrix2.dajUmiestnenie();

Teda od premennej typu Film môžeme požadovať iba to, čo umožňuje trieda Film. Keďže pri dedičnosti platí, že triedy vedia všetko to, čo zdedili od predkov, tak aj objekty tried FilmNaDvd, FilmNaPaske alebo FilmVPocitaci vedia všetko to, čo by sme mohli požadovať od Film-u.

Treba si dať pozor na to, že opačné pravidlo neplatí, t.j. z premennej, ktorá je typu potomka (napr. FilmNaDvd) nevieme referencovať objekt typu predka (napr. Film). Je to logické keď si uvedomíte, že cez premennú typu FilmNaDvd očakávame možnosť volania ľubovoľnej metódy triedy FilmNaDvd, a to objekt triedy Film poskytnúť nevie.

Budeme si teda uchovávať iba jedno pole filmov a môžeme z neho referencovať všetky typy médií. Výpis filmov už teda bude iba cez jedno pole.

public class ZoznamFilmov {
        private Film[] filmy;
...            
        public void vypisVsetko() {
                for (int i = 0; i < filmy.length; i++) {
                        System.out.println(filmy[i].retazecPreVypis());
                }
        }
...
}