Objektové programování

Objekty jsou rozšířením klasických proměnných. Stojí na nich celé moderní programování (ale i tak musíte vědět, co to jsou obyčejné proměnné, cykly, řízení programu, podmíněný překlad, debugging,...)

V dnešní lekci je pár doporučení, co se vyplatí dělat. Ne všechna jsou hned vysvětlena, ale i tak se jich držte, časem poznáte, proč jsou důležitá.

Správný objekt má několik velice důležitých vlastností:

  1. Obsahuje jak data, tak i vlastní procedury (heterogennost)
  2. Může dědit vlastnosti jiných objektů (polymorfismus)
  3. Je přístupný pouze "zevnitř"
  4. ...

My se na tyto vlastnosti prozatím vykašleme.

Definice objektu je stejná jako u záznamu, ale slovo record nahradíme slovem object.

Zavodnik = object
            Jmeno:string;
       StartCislo:Integer;
           end;
var Vitez : Zavodnik;

Nyní trocha terminologického guláše. Pokud definujeme objekt pomocí type, říká se danému typu třída. Samotná proměnná dané třídy (=daného typu) se pak nazývá instance dané třídy. (Instancí dané třídy tedy může být mnoho). Aby toho nebylo málo, tak instance třídy se nazývá objekt. (Je nelogické, že se třída definuje pomocí slova object. Nikoho ani nenapadne psát object do sekce var!)

Nyní můžeme s objektem pracovat stejně tak, jako se záznamem (Pascal nám to z neznámých důvodů umožňuje).
Zavodnik.Jmeno:='Martin';

Objekty toho umí mnohem víc než obyčejný záznam. Mohou obsahovat vlastní procedury a funkce (tzv. metody).
Metod se nejčastěji používá k manipulaci s objektem a jeho proměnnými. Metoda objektu může zacházet se všemi jeho proměnnými. (Děsivě to jenom zní, podívejte se na příklad.)
U metod se uvádí pouze hlavička. Pak je musíme dodefinovat. (Při dodefinování nemusíte vypisovat celou hlavičku, ale je lepší ji uvést znovu.)

program Zaklad;
type TPotravina = object
                   Nazev : string;
                  Kalorie: Integer;
                  procedure Nastav(Co:string,Kolik:Integer);
                  procedure Secti(A:TPotravina); {Přičte k instancím našeho objektu
                                                  instance objektu A}
                  procedure Vypis;
                  end;
procedure TPotravina.Nastav(Co:string,Kolik:Integer);
begin
 Nazev:=Co;
 Kalorie:=Kolik;
end;

procedure TPotravina.Secti;
begin
 Nazev:=Nazev+' a k tomu ' + A.Nazev;
 Kalorie:=Kalorie+A.Kalorie;
end;

procedure TPotravina.Vypis;
begin
 Writeln('Dal jste si : ', Nazev);
 Writeln('Celkova kalorická hodnota vašeho pokrmu činí : ',Kalorie);
end;


var Vecere,Priloha:TPotravina;
begin
 Vecere.Nastav('Zeli',100);
 Priloha.Nastav('Dezert',5000);
 Vecere.Secti(Priloha);
 Vecere.Vypis;
 Readln;
end.

Snad jste to pochopili.

V objektovém programování bychom vždy měli dodržovat zásadu, že proměnné objektu lze měnit jenom pomocí metod daného objektu. Vyplatí se dokonce psát metody pro získání obsahu proměnných objektu. (Tedy nepsat BlaBla.Jmeno - a raději napsat funkci (metodu) BlaBla.ZjistiJmeno : string;)

Doufám, že zatím je vám vše jasné.

Objekt má jednu nevýhodu - nemůže být výsledným typem funkce (to ostatně nemůže být žádný uživatelem definovaný typ)...

Nadefinujeme-li si objekt, je zpočátku zcela prázdný. K jeho inicializaci se používá tzv. konstruktor. Je to metoda, která provede inicializaci naší proměnné. (Místo slova procedure napíšeme constructor a chováme se k ní jako by se jednalo o proceduru)
Konstruktor se obvykle pojmenovává Init. V našem případě by bylo lepší proceduru Nastav nadefinovat jako constructor.
constructor Init(Co:string, Kolik:Integer); a poté dále constructor Init(...);

Přestaneme-li s objektem pracovat, měli bychom ho odstranit z paměti. K tomu slouží destructor. (Má-li daný objekt otevřeno patnáct souborů a zabírá-li půlku paměti, je lepší ten svinčík prostě uklidit). Pro destruktor se nejčastěji používá název Done.
destructor Done;

My budeme dodržovat zásadu, že každý objekt má konstruktor i destruktor.

Všechny objekty v programu by měly být dynamické proměnné. (A v normálních programovacích jazycích to ani jinak nejde). Přistupovat k nim tedy budeme vždy pouze přes ukazatele. Máme-li tedy třídu TNejakyObjekt, současně s ní definujeme i typ PTenSamyObjekt = ^TNejakyObjekt. Po vytvoření instance dané třídy (pomocí New()) musíme zavolat konstruktor. Před Dispose voláme destruktor. (Všechny dynamické proměnné rušíme hned, jakmile nejsou zapotřebí, nejpozději před koncem programu).

 type PObjekt = ^TObjekt; {Je lepsi definovat ukazatel driv, trida ho pak muze pouzivat,
                                 coz se obcas hodi.}
             TObjekt = object
                          constructor Init;
                          destructor Done;
                          end;

 constructor TObjekt.Init;{I když konstruktor zdánlivě nic nedělá, vyplatí se ho psát}
 begin
 end;

 destructor TObjekt.Done;
 begin
 end;

 var A:PObjekt;
 begin
  New(A);
  A^.Init;
  {Tu se neco deje}
  A^.Done;
  Dispose(A);
 end.

Pascal má i zkrácený zápis:

  New(A,Init);
  {Tu se neco deje}
  Dispose(A,Done);

Budeme se držet zásady, že New a Dispose pro objekty voláme vždy s konstruktorem a destruktorem. (Je lépe, když má objekt všechny proměnné (a ukazatele obzvlášť) inicializované, jinak jsou v nich opravdu zcela náhodné hodnoty. A pokud pouze zrušíme ukazatel na objekt, který si v paměti pomocí New zabral několik gigabytů, ... tak zůstanou zabrány až do konce programu a my s tím nebudeme moc nic udělat.)

Ještě se zmíním o polymorfismu. Máme-li definován nějakou třídu, můžeme definovat další třídu, která zdědí všechny vlastnosti původní třídy (rodiče) a přidá některé další (jeden rodič může mít i více potomků).

type Syn = object(Otec)
             Hracky : THracky;
             constructor Narozeni;
             destructor Smrt;
           end;

Stačí uvést jméno rodiče v závorce a třída zdědí veškeré vlastnosti rodiče (všechny jeho proměnné a metody).

Platí, že libovolnou proměnnou potomka můžeme vždy přiřadit do proměnné typu rodič (a stejně tak je tomu s ukazateli).
Podívejme se na krátkou ukázku:

{----------------------------------TZvire----------------------------------------}
type PZvire=^TZvire; {Tady s vyhodou vyuzivame toho, ze ukazatel muzeme definovat pred samotnou
                             definici typu, na ktery ukazuje. Casto se totiz stane, ze trida vyuziva
                             ukazatel sama na sebe.}
            TZvire=object
                      jmeno:string;
                      procedure VydejZvuk;
                      constructor Init; {Kazda trida musi mit konstruktor.}
                      destructor Done; {Kazda trida musi mit destruktor.}
                   end;
{Nyni musime dodefinovat jednotlive metody tridy}
constructor TZvire.Init; {I kdyz konstruktor zdanlive nic nedela, pri jeho volani
                         se objekt inicializuje, takze je dobre ho vzdy napsat.}
begin
end;

procedure TZvire.VydejZvuk;
begin
 writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.');
end;

destructor TZvire.Done; {V tomto pripade destruktor skutecne nic nedela,
                        ale casto je nezbytny. Proto ho piseme u kazde
                        tridy.}
begin
end;

{-------------------------------------end of TZvire---------------------------------------}

{----------------------------------------TKocka-------------------------------------------}
type PKocka=^TKocka; {Ten samy trik s ukazatelem}
            TKocka=object(TZvire)
                       ChycenoMysi:integer;
                       procedure ChytMys; {Kocka zdedila vsechny vlastnosti od TZvirete,
                                                 pridala vsak novou metodu ChytMys;
                                                 A udaj, kolik mysi jiz chytla.}
                   end;
procedure TKocka.ChytMys;
begin
  ChycenoMysi:=ChycenoMysi+1;
end;
{-------------------------------------end of TKocka---------------------------------------}

var Zvire:PZvire;
           Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init); {Vsechny objekty by mely byt dynamicke a volany s konstruktorem}
   Zvire:=Kocka; {Jelikoz je TKocka potomkem TZvirete, je toto prirazeni spravne}
   Kocka^.VydejZvuk; {Tady vidime, ze i trida TKocka ma metodu VydejZvuk,
                     kterou zdedila od sveho TZvirete.}
   Zvire^.VydejZvuk;
   Dispose(Kocka,Done); {Dynamicky alokovane promenne nikdy nezapomeneme odstranit,
                        u objektu musime navic zavolat destruktor.}
   readln;
end.

Zkuste si uvedený program spustit.

Dědičnost toho ale umožňuje mnohem víc. Jednou z jejích výhod je, že potomek může předefinovat metody rodiče (ale nemůže je zrušit).
Zkusme nyní upravit náš program tak, aby měl větší smysl. Bylo by dobré, aby naše kočka mňoukala. Navíc doděláme pár dalších legrácek.
Výsledek by mohl vypadat třeba nějak takto:

{----------------------------------TZvire----------------------------------------}
type PZvire=^TZvire;
            TZvire=object
                      jmeno:string;
                      procedure VydejZvuk;
                      constructor Init(truename:string); {Konstruktor priradi zvireti jmeno}
                      destructor Done;
                      function getname:string;
                   end;
{Nyni musime dodefinovat jednotlive metody tridy}
constructor TZvire.Init(truename:string);
begin
   jmeno:=truename;
end;

procedure TZvire.VydejZvuk;
begin
 writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.');
end;

function TZvire.getname:string;
begin
 getname:=jmeno;
end;

destructor TZvire.Done;
begin
end;

{-------------------------------------end of TZvire---------------------------------------}

{----------------------------------------TKocka-------------------------------------------}
type PKocka=^TKocka;
            TKocka=object(TZvire)
                       ChycenoMysi:integer;
                       procedure VydejZvuk;{Chceme-li proceduru predefinovat,
                                                   musime ji znovu uvest v seznamu metod.}
                       constructor Init(truename:string;Chyceno:integer); {I konstruktor lze predefinovat
                                                  Kocka potrebuje navic nastavit pocet chycenych mysi.}
                       procedure ChytMys;
                       end;

procedure TKocka.VydejZvuk;
begin
 writeln('Mnau');
end;

constructor TKocka.Init(truename:string;Chyceno:integer);
begin
   jmeno:=truename;
   ChycenoMysi:=Chyceno;
end;

procedure TKocka.ChytMys;
begin
  ChycenoMysi:=ChycenoMysi+1;
end;
{-------------------------------------end of TKocka---------------------------------------}

var Zvire:PZvire;
           Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init('Micka',12));
   Kocka^.VydejZvuk; {Tady pouzivame predefinovanou metodu.}
   Zvire:=Kocka;
   Zvire^.VydejZvuk;
   Dispose(Kocka,Done);
   readln;
end.

Zkuste si uvedený program spustit.

Předefinovaná metoda může využít původní metody rodiče.
Toho se často využívá například u konstruktorů.
Chceme-li využít původní metodu rodiče (kterou náš objekt předefinoval), napíšeme před volání funkce klíčové slovo inherited.

Raději si vše ukažme na našem příkladu (konstruktor TKocka.Init využívá dvě původní metody třídy TZvire) - raději zkopíruji celý text, snadněji si ho zkopírujete do Pascalu:

{----------------------------------TZvire----------------------------------------}
type PZvire=^TZvire;
            TZvire=object
                      jmeno:string;
                      procedure VydejZvuk;
                      constructor Init(truename:string);
                      destructor Done;
                      function getname:string;
                   end;

constructor TZvire.Init(truename:string);
begin
   jmeno:=truename;
end;

procedure TZvire.VydejZvuk;
begin
 writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.');
end;

function TZvire.getname:string;
begin
 getname:=jmeno;
end;

destructor TZvire.Done;
begin
end;

{-------------------------------------end of TZvire---------------------------------------}

{----------------------------------------TKocka-------------------------------------------}
type PKocka=^TKocka; {Ten samy trik s ukazatelem}
            TKocka=object(TZvire)
                       ChycenoMysi:integer;
                       procedure VydejZvuk;
                       constructor Init(truename:string;Chyceno:integer);
                       procedure ChytMys;
                   end;

procedure TKocka.VydejZvuk;
begin
 writeln('Mnau');
end;

constructor TKocka.Init(truename:string;Chyceno:integer);
begin
   inherited Init(truename); {Volame puvodni konstruktor predka}
   inherited VydejZvuk; {Volame puvodni metodu predka}
   ChycenoMysi:=Chyceno;
   writeln('A ted se ze mne stala kocka a budu mnoukat!');
end;

procedure TKocka.ChytMys;
begin
  ChycenoMysi:=ChycenoMysi+1;
end;
{-------------------------------------end of TKocka---------------------------------------}

var Zvire:PZvire;
           Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init('Micka',12));
   writeln('Konec konstruktoru');
   writeln;

   Kocka^.VydejZvuk;
   Zvire:=Kocka;
   Zvire^.VydejZvuk;
   Dispose(Kocka,Done);
   readln;
end.

Zkuste se zamyslet nad tím, co přesně náš program teď bude dělat a pak ho spusťte. Trefili jste se?

Další zázračná věc, kterou objekty umožňují, je dynamická vazba. Té docílíme tak, že za deklaraci procedury ve třídě napíšeme klíčové slovo virtual. A kdykoli voláme nějakou virtuální funkci, podívá se překladač, s jakým konstruktorem byl daný objekt vytvořen, a podle toho vybere příslušnou funkci. Definujeme-li metodu jako virtuální, musí taková být ve všech potomcích. Také není možné aby metoda u rodiče virtuální byla a u potomka ne.

Raději si to osvětlíme na našem příkladu. Předěláme ho nyní tak, aby kočka Micka mňoukala pořád.

Je to jednoduché, stačí připsat dvakrát slovo virtual:

{----------------------------------TZvire----------------------------------------}
type PZvire=^TZvire;
            TZvire=object
                      jmeno:string;
                      procedure VydejZvuk;virtual; {Klicova zmena, nyni se bude
                                                          metoda vybirat podle toho,
                                                          jaky objekt je tam ve skutecnosti.}
                      constructor Init(truename:string);
                      destructor Done;
                      function getname:string;
                   end;
{Nyni musime dodefinovat jednotlive metody tridy}
constructor TZvire.Init(truename:string);
begin
   jmeno:=truename;
end;

procedure TZvire.VydejZvuk;
begin
 writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.');
end;

function TZvire.getname:string;
begin
 getname:=jmeno;
end;

destructor TZvire.Done;
begin
end;

{-------------------------------------end of TZvire---------------------------------------}

{----------------------------------------TKocka-------------------------------------------}
type PKocka=^TKocka; {Ten samy trik s ukazatelem}
            TKocka=object(TZvire)
                       ChycenoMysi:integer;
                       procedure VydejZvuk;virtual; {I zde musi zustat metoda virtualni}
                       constructor Init(truename:string;Chyceno:integer);
                       procedure ChytMys;
                   end;

procedure TKocka.VydejZvuk;
begin
 writeln('Mnau');
end;

constructor TKocka.Init(truename:string;Chyceno:integer);
begin
   inherited Init(truename);
   ChycenoMysi:=Chyceno;
end;

procedure TKocka.ChytMys;
begin
  ChycenoMysi:=ChycenoMysi+1;
end;
{-------------------------------------end of TKocka---------------------------------------}

var Zvire:PZvire;
           Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init('Micka',12));
   writeln('Konec konstruktoru');
   writeln;

   Kocka^.VydejZvuk;
   Zvire:=Kocka; {Jelikoz je TKocka potomkem TZvirete, je toto prirazeni spravne}
   Zvire^.VydejZvuk;
   Dispose(Kocka,Done);
   readln;
end.

Spusťte si nyní daný program a uvidíte ten rozdíl!

Jelikož virtuální metody jdou volat až po zavolání konstruktoru, nemůže být konstruktor nikdy virtuální. Navíc je z toho vidět, proč je důležité, aby každá třída měla konstruktor. Schválně si zkuste v uvedeném programu napsat pouhé Kocka:=New(PKocka); místo Kocka:=New(PKocka,Init(...)). Virtuální metody nebudou fungovat, v horším případě vám spadne počítač, v lepším se nestane vůbec nic, jako byste žádnou metodu nevolali. Pascal to takhle umožňuje, ale my se neinicializovaným objektům (tj. objektům, na které nebyl použit konstruktor) budeme vyhýbat.

Představme si následující situaci. Máme dva objekty Rodiče(TZvire) a Potomka(TKocka) a rodič destruktor nemá (nepotřebuje ho), ale potomek volání destruktoru vyžaduje. Přiřadíme-li nyní potomka do proměnné typu rodič a odstraníme tuhle dynamickou proměnnou pomocí Dispose, žádný destruktor se nezavolá. To může mít katastrofální následky. Proto každý objekt musí mít destruktor a tento destruktor musí být virtuální. Usnadníme si tak práci, pokud někdy v budoucnu budeme potřebovat dodefinovat potomka, který destruktor vyžaduje. Nebudeme tak muset v našem programu hledat všechny výskyty Dispose a kontrolovat, jestli náhodou nerušíme proměnnou tohoto nového typu... (Což je náročný oříšek, jelikož potomci jdou přiřazovat do proměnných typu rodič a rušíme-li proměnnou typu rodič, odkud máme jistotu, že v ní není onen destrukci vyžadující potomek?) Napíšeme nyní náš příklad v duchu všech uvedených zásad:

{----------------------------------TZvire----------------------------------------}
type PZvire=^TZvire;
            TZvire=object
                      jmeno:string;
                      procedure VydejZvuk;virtual; {Klicova zmena, nyni se bude
                                                          metoda vybirat podle toho,
                                                          jaky objekt je tam ve skutecnosti.}
                      constructor Init(truename:string);
                      destructor Done;virtual;
                      function getname:string;
                   end;
{Nyni musime dodefinovat jednotlive metody tridy}
constructor TZvire.Init(truename:string);
begin
   jmeno:=truename;
end;

procedure TZvire.VydejZvuk;
begin
 writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.');
end;

function TZvire.getname:string;
begin
 getname:=jmeno;
end;

destructor TZvire.Done;
begin
 writeln('Zviratko ', jmeno, 'umrelo.');
end;

{-------------------------------------end of TZvire---------------------------------------}

{----------------------------------------TKocka-------------------------------------------}
type PKocka=^TKocka; {Ten samy trik s ukazatelem}
            TKocka=object(TZvire)
                       ChycenoMysi:integer;
                       procedure VydejZvuk;virtual; {I zde musi zustat metoda virtualni}
                       constructor Init(truename:string;Chyceno:integer);
                       procedure ChytMys;
                       destructor Done;virtual;
                   end;

procedure TKocka.VydejZvuk;
begin
 writeln('Mnau');
end;

constructor TKocka.Init(truename:string;Chyceno:integer);
begin
   inherited Init(truename);
   ChycenoMysi:=Chyceno;
end;

procedure TKocka.ChytMys;
begin
  ChycenoMysi:=ChycenoMysi+1;
end;

destructor TKocka.Done;
begin
 writeln('Kocka ',jmeno, ' snedla za svuj zivot ', ChycenoMysi, ' mysi. Budiz ji zeme lehka.')
end;
{-------------------------------------end of TKocka---------------------------------------}

var Zvire:PZvire;
           Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init('Micka',12));
   writeln('Konec konstruktoru');
   writeln;

   Kocka^.VydejZvuk;
   Zvire:=Kocka; {Jelikoz je TKocka potomkem TZvirete, je toto prirazeni spravne}
   Zvire^.VydejZvuk;
   Dispose(Zvire,Done); {Bez virtualniho destruktoru by se zavolal spatny destruktor.}
   readln;
end.

Nejčastěji se pak využívají seznamy objektů (že nechcete vědět, co to je). Nebojte se, je to úplně stejné, jako u obyčejných seznamů. U seznamů hrají konstruktory a destruktory výraznou úlohu.

Hlavní využití tříd spočívá v tom, že spoustu tříd již někdo definoval. Potřebujeme-li tedy něco naprogramovat, podíváme se, která třída náš problém řeší. Pokud žádnou takovou nenajdeme, podíváme se, jestli není snadnější napsat řešení jako potomka nějaké již existující třídy. Pokud se to povede, ušetříme si spoustu práce.

Objekty se také používají, pokud vyvíjíme rozsáhlejší program. Představme si, že na vývoji programu pracuje několik programátorů a každý vyvíjí jinou část programu. Velice snadno se pak může stát, že dva z nich pojmenují nějakou funkci či proměnnou stejně. Říká se tomu kolize jmen. Pokud ovšem každý z nich vyvíjí jinou třídu, kolize jmen nehrozí (TMojeTrida.Vypis je jiný název než TTvojeTrida.Vypis). Programátoři tak nemusí vymýšlet šílené názvy svých funkcí a mohou je pojmenovávat zcela přirozeně. To vede k lepší čitelnosti programu (a tím i k rychlejšímu dokončení vývoje).

V sekci s příklady bude ukázka, která bude demonstrovat další výhody objektů. Bude se jedna o takovou demoverzi skutečného programování (občas bude zapotřebí něco přidat, občas něco upravit, finální verze se bude lišit od původní...)

To by byl pouze takový nezbytný základ objektově orientovaného programování. Dnes nedostanete žádný úkol, neboť je nezbytné, abyste si sami prošli a pochopili všechny příklady. Objekty se musíte prokousat každý po svém. Zkoušejte, pište vlastní programy, opravujte chyby v mojí lekci, ale hlavně - využívejte co možná nejvíce objektů (klidně i nesmysluplně, hlavně, ať zjistíte, jak fungují). Snažte se vymyslet, kdy a jak využít jejich vlastností.