Objektově orientované programování

Hlavní výhodou objektově orientovaného programování je rychlost. Pokud správně používáme objekty, můžeme si u rozsáhlejších projektů ušetřit i několik týdnů práce. Dnes se podíváme, které z těchto výhod jdou využít i v Pascalu. Nejprve krátká motivace a pak již, jak to vypadá s objekty v Pascalu.

Výhody

Obecně

Na začátku vývoje většího projektu rozdělíme program na jednotlivé třídy a dohodneme se, jak tyto třídy mezi sebou budou komunikovat (tj. dohodneme se, jakých metod budou využívat). K této dohodě často slouží tzv. UML diagramy, což jsou obrázky znázorňující jednotlivé třídy. Tomuto rozdělení se vyplatí věnovat dostatek času, špatné rozvržení může znamenat nefunkčnost projektu.

Nyní již každý programátor může pracovat na své části projektu, aniž by se staral o ostatní. Nemusí tak čekat na to, jaké globální proměnné se rozhodl používat někdo jiný, jak pojmenoval funkce ve své části projektu, prostě má svou třídu a vše dělá v ní. Protože konstruktory a destruktory jsou volány vždy téměř automaticky, nemusí také dotyčný programátor ostatním říkat, jakým stylem se má daný objekt inicializovat a rušit. O vše se postará kompilátor. Tímto způsobem se ušetří spousta času. Pokud jsou navíc třídy uloženy v samostatných programovacích jednotkách, ušetří se čas i při kompilaci. Pokaždé stačí zkompilovat jen změněné třídy. Při rozsáhlejších projektech tak jdou ušetřit i hodiny.

Ostatní programátoři ze třídy vidí pouze její komunikační rozhraní (tj. to na čem se na začátku dohodli), nemůžou tedy zneužívat znalostí toho, jak jsou její data reprezentována v paměti. To ale znamená, že pokud se dotyčný programátor rozhodně implementaci třídy změnit (např. proto, aby ji zrychlil, nebo proto, že na začátku ji navrhl úplně špatně), ostatní programátoři nemusí svůj kód přepisovat. Toto můžeme chápat také tak, že pokud programujete objektově, udělat chybu vás moc nestojí. (Dřív byste možná zničili půl roku práce desítky programátorů. Teď to opravíte, aniž si kdokoli čehokoli všimne.)

Další nespornou výhodou je možnost využívat univerzální třídy, které již navrhl někdo jiný. Dědičnost zaručuje, že je budeme moci upravit k obrazu svému. Není tedy nutné dělat již jednou udělanou práci.

Polymorfismus, dědičnost, dynamická vazba a abstraktní třídy nám dávají možnost, dívat se na proměnné, jako by byly jednoho typu. To usnadňuje práci s dynamickými datovými strukturami. Nyní si stačí navrhnout jeden dynamický seznam. A můžeme do něj ukládat libovolná data. Nemusíme pro každý typ dat psát nové procedury a funkce.

Tento způsob se tedy hodí, píšeme-li nějakou dostatečně univerzální třídu (dynamický seznam) nebo pokud vyvíjíme rozsáhlý projekt. V opačném případě vede využití tříd ke zbytečnému nárůstu délky kódu, což (obzvláště u krátkých programů) může efektivitu práce snižovat.

V Pascalu

V Pascalu jsou objekty navrženy dost nešikovně, chybí jim zde řada podstatných vlastností. Jelikož Pascal nemá přetěžování funkcí (= neumožňuje, aby dvě funkce s různým seznamem parametrů měly stejný název), může mít objekt vždy jen jeden konstruktor a musíme nechat na ostatních programátorech, aby ho správně používali. Pascal také umožňuje, aby po zániku objektu nebyl volán jeho destruktor (vytvořte si statický objekt v proceduře a pak ji ukončete -- proto je lepší mít všechny objekty dynamické). Předávání objektů do funkce prostřednictvím parametrů by občas vyžadovalo použití kopírovacího konstruktoru. Pascal tuhle možnost nemá. Takže pokud chceme předat objekt hodnotou, musíme předat ukazatel na něj a pak sami musíme zavolat funkci, která daný objekt zkopíruje.

V C++ se tyto věci řeší automaticky, v Pascalu na ně musíme pamatovat, což trošku zdržuje, ale není to nic hrozného. Budeme-li se držet pravidel, bude objektové programování v Pascalu snadné.

Definice

Ale nyní se již podíváme na to, jak se s objekty v Pascalu pracuje. Jako vždy půjde o utřídění a prohloubení dosavadních poznatků.

Nejprve se podíváme, jak se taková třída v Pascalu definuje. Budeme se držet konvence, že název třídy vždy začíná velkým písmenem T a že před samotnou definicí třídy definujeme ukazatel na ní. V Pascalu se k definici třídy slouží klíčové slovo object, které se používá téměř stejně jako slovo record (vždy se vyplatí zvážit, který z těchto typů použít):

type PMojeTrida=^TMojeTrida;
            TMojeTrida=
                       object
                             {tady je misto na vlastni data tridy}
                             {a na hlavicky jejich metod}
                       end;.
{Zde metody tridy dodefinujeme}

Data

Data jsou jednou z nejdůležitějších částí třídy. Definují se stejně jako u záznamu, podívejte se na ukázku:

type PMojeTrida=^TMojeTrida;
            TMojeTrida=
                       object
                             sz:integer;
                             avaible:integer;
                             pole:array[1..12] of {blue,black,white};
                             {sem patri hlavicky metod}
                       end;.
{Zde metody tridy dodefinujeme}

Na rozdíl od záznamu ale platí, že uživatel třídy by o datech (až na výjimky) neměl vědět nic. Pokud se pak rozhodneme, můžeme implementaci třídy velice snadno změnit. Data třídy mohou být nějaké objekty. Takovémuto postupu se říká kompozice (např. Třída TAuto může obsahovat data typu TMotor, TKabina, TKaroserie a TKufr).

Metody

Metody jsou to, co dělá třídu třídou. Jedná se o funkce a procedury, které jsou jakoby součásti proměnné. Metody mají přístup k datům dané třídy. A nikdo jiný by ho správně mít neměl. Některé existuje několik speciálních metod, na jejichž definici se podíváme nejdřív.

Konstruktor

Tohle je jedna z nejdůležitějších metod vůbec. Každá třída ji musí mít (Pascal to sice nevyžaduje, ale my ji stejně budeme psát vždycky). Zavolání konstruktoru nastaví tabulku virtuálních metod. Pokud navíc chceme nastavit nějaká data, je konstruktor ideální příležitost. Nejčastěji se konstruktor pojmenovává Init; K jeho deklaraci slouží vyhrazené slovo constructor:

type PMojeTrida=^TMojeTrida;
            TMojeTrida=
                       object
                             {data}
                             sz:integer;
                             avaible:integer;
                             pole:array[1..12] of {blue,black,white};
                             {metody}
                             constructor Init;
                             {sem patri hlavicky metod}
                       end;.
{Zde metody tridy dodefinovavame}
constructor TMojeTrida.Init;
begin
   {Pokud chceme, sem dopiseme nastavovaci kod.
   Coz se hodi, napr. pokud vytvarime tridu pro nejaky seznam s hlavickou apod.}
end;

Máme-li nyní definovanou proměnnou A typu PMojeTrida, můžeme napsat A:=New(PMojeTrida,Init); a do naší dynamické proměnné A se umístí nová instance třídy TMojeTrida (zavolá se konstruktor Init.) Jedna třída samozřejmě může mít více konstruktorů, pokud se liší jménem, to se nejčastěji využívá, pokud jeden konstruktor nastaví prázdný objekt, kdežto druhý mu předá či nastaví nějaká data. Můžeme také vytvořit tzv. kopírovací konstruktor Copy;, ten bude mít jako parametr objekt naší třídy. A tento objekt rozumně zkopíruje do objektu právě vytvářeného. (Např. při kopírovaní seznamu vytvoří druhý seznam a zkopíruje všechny položky).

Nepovede-li se nám v průběhu konstruktoru z nějakého důvodu objekt vytvořit. Zavoláme proceduru Fail; ta lze použít pouze v konstruktoru a zruší právě vytvářený objekt.

Destruktor

Destruktor slouží k odklizení objektu. Měl by se volat automaticky, což Pascal nedělá. Na rozdíl od konstruktoru, destruktor, který má prázdné tělo, skutečně nic nedělá. V Pascalu může mít daná třída víc destruktorů. My budeme vždy používat jen jeden. V Pascalu může mít destruktor parametry. Ač se to možná zdá logické, odporuje to tomu, že destruktory by měly být volány automaticky, proto se budeme parametrům destruktoru vyhýbat. Destruktor se klasicky pojmenovává Done;. :

type PMojeTrida=^TMojeTrida;
            TMojeTrida=
                       object
                             {data}
                             sz:integer;
                             avaible:integer;
                             pole:array[1..12] of {blue,black,white};
                             {metody}
                             constructor Init;
                             destructor Done;
                             {sem patri hlavicky metod}
                       end;.
{Zde metody tridy dodefinovavame}
constructor TMojeTrida.Init;
begin
   {Pokud chceme, sem dopiseme nastavovaci kod.
   Coz se hodi, napr. pokud vytvarime tridu pro nejaky seznam s hlavickou apod.}
end;

destructor TMojeTrida.Done;
begin
   {Sem dopiseme rusici kod. Nezapomeneme zavolat
   destruktory vsech objektu, ktere nas objekt vytvoril.
   Specialne, pokud jsme nasi tridu vytvorili pomoci kompozice,
   rozmyslime si, zda nechceme zrusit i jeji jednotlive casti.}
end;

Metody get

Jelikož chceme, aby k datům naší třídy nikdo přímo nepřistupoval, musíme jejich získávání ošetřit jinak. K tomu se nejčastěji používá metoda getjmeno:typdat;. Takovouto metodou je zajištěna dostatečná možnost pozdějších změn, které se nikterak nedotknou koncového uživatele třídy. Raději na praktické ukázce:

type PMojeTrida=^TMojeTrida;
            TMojeTrida=
                       object
                             {data}
                             sz:integer;
                             avaible:integer;
                             pole:array[1..12] of {blue,black,white};
                             {metody}
                             constructor Init;
                             destructor Done;
                             function getsz:integer;
                             {sem patri hlavicky metod}
                       end;.
{Zde metody tridy dodefinovavame}
constructor TMojeTrida.Init;
begin
   {Pokud chceme, sem dopiseme nastavovaci kod.
   Coz se hodi, napr. pokud vytvarime tridu pro nejaky seznam s hlavickou apod.}
end;

destructor TMojeTrida.Done;
begin
end;

function TMojeTrida.getsz:integer;
begin
getsz:=sz; {Znamena-li napr. sz pocet prvku ulozenych v poli,
           ktere pozdeji zmenime na dynamicky seznam,
           muzeme stale tuto metodu prepsat tak,
           aby vracela pocet ulozenych prvku.
           Koncovy uzivatel nic nepozna.}
end;

Metody set

Podobně občas potřebuje uživatel naší třídy něco nastavit. Potom se z podobných důvodů jako metoda get používá metoda typu set. Tady ale platí, že bychom měli velice dobře zvážit, jestli je dobře, když uživatel může nastavovat danou vlastnost třídy. Rozhodneme-li se později danou vlastnost zrušit, stačí tuto metodu přepsat tak, aby nic nedělala.

type PMojeTrida=^TMojeTrida;
            TMojeTrida=
                       object
                             {data}
                             sz:integer;
                             avaible:integer;
                             pole:array[1..12] of {blue,black,white};
                             {metody}
                             constructor Init;
                             destructor Done;
                             function getsz:integer;
                             function setsz(s:integer):integer;{Vrati nastavenou hodnotu sz}
                       end;.

{Zde metody tridy dodefinovavame}
constructor TMojeTrida.Init;
begin
   {Pokud chceme, sem dopiseme nastavovaci kod.
   Coz se hodi, napr. pokud vytvarime tridu pro nejaky seznam s hlavickou apod.}
end;

destructor TMojeTrida.Done;
begin
end;

function TMojeTrida.getsz:integer;
begin
getsz:=sz;
end;

function setsz(s:integer):integer;{Vrati nastavenou hodnotu sz}
begin
sz:=s;
setsz:=sz;
end;

Private

U některých dat a metod chceme, aby je koncový uživatel neviděl. Ať již z důvodu možnosti pozdějších změn, či prostě proto, že jsou tajná. Některé metody navíc mohou být jen dílčí podmetody jiných, u takových je zbytečné ukazovat je koncovému uživateli. Jenom by ho mátly. (Koncový uživatel má svoje rozhraní, pomocí kterého zachází s danou třídou. Nic víc by vidět neměl.) Pascal nabízí direktivu private. Cokoli za nic uvedeme, bude viditelné jen v rámci daného modulu (=programu či unity). Toto řešení je ideální, pokud vyvíjíme naší třídu v samostatné unitě.

Public

Jiné metody (a občas i data) tvoří součást veřejného rozhraní. U těch je naopak žádoucí, aby byly vidět. K tomu slouží slovo public.
Menší poznámka: Deklarujeme-li třídu v samostatné unitě, nemusíme psát ani jedno z těchto slov. Metody a data se pak budou chovat jako by byly veřejné. Napíšeme-li private, zmizí z veřejné části vše, co je uvedeno dále (= mezi výskytem public a koncem deklarace objektu). Pokud někde dále napíšeme slovo public, vše co je uvedeno dál bude opět veřejné. Slova private a public tak slouží jako jakési přepínače mezi částí veřejnou a soukromou. Chceme-li využívat soukromá data i z jiné třídy, je to možné, ale musíme ji umístit do stejné unity. Z ukázky (třída reprezentující seznam vypsatelných objektů) by mělo být vše jasné:

unit pchary;
interface {Verejnosti pristupna cast}
uses strings;
type  pdata=^tdata;
      tdata=object
                private {Soukroma data a metody. Vi o nich pouze dana trida}
                        s:Pchar;
                        sz:word;
                        avaible:word;
                        procedure inflate(increment:integer); {Zvetsi alokovane misto}

                public {Nezatezujeme uzivatele nicim, zbytecnym}
                      constructor Init;
                      function vypis:word;                  {Vypise data a vrati kolik znaku vypsal}
                      function put(var Os:Openstring):Longint; {Vlozi radku s do Pcharu}
                      destructor Done;
          end;

implementation {Tahle cast je verejnosti skryta}
const increment:word=200;

constructor tdata.init;
begin
 s:=nil;
 sz:=0;
 avaible:=0;
end;

procedure tdata.inflate(increment:integer);
var b:Pchar;
    i:longint;
begin
 getmem(b,avaible+increment);
 if sz=0 then
  for i:=0 to avaible+increment do b[i]:=#0
  else
  for i:=0 to sz do
   b[i]:=s[i];
  b[sz+1]:=#0; {Radeji tam vrazime jeste jednu ukoncovaci nulu}
 FreeMem(s,avaible); {Uvolnime zabranou pamet}
 Inc(avaible,increment);
 s:=b;
end;

function tdata.vypis:word;
var i:word;
begin
 i:=0;
 while (s[i]<>>#0) do
  begin
   write(s[i]);
   i:=i+1;
  end;
 vypis:=i;
end;

function tdata.put(var Os:Openstring):Longint;
var b:array[0..300] of char; {To je take Pchar a tohle je rychlejsi
                             nez ho pri kazdem volani alokovat dynamicky}
begin
 if avaible<sz+Length(os)+2 then {V pameti mame alokovano malo mista}
      inflate(increment); {Tak ho zvetsim}
   StrCat(s,(StrPCopy(b,os))); {pripojim na konec stringu a}
   StrCat(s,#10#13#0); {za nej pripojim znaky nove radky, pojistim nulou na konci}
   inc(sz,length(os)+2);
   put:=sz;
end;

destructor tdata.done;
begin
 FreeMem(s,avaible);
 avaible:=0;
 sz:=0;
end;

begin {Inicializacni cast, ale my zadnou inicializaci nepotrebujeme.}
end.

Možnosti

Nyní se konečně dostáváme k tomu, proč je objektové programování tak populární.

Dědičnost

Pokud nějaká třída má některé všechny vlastnosti, které potřebujeme, můžeme od ní vytvořit potomka. Ten zdědí všechny vlastnosti rodiče. Můžeme dodat další data a metody, eventuálně některé metody předefinovat.
Chceme-li definovat potomka, uvedeme jméno rodiče v závorce za slovem object. Stane-li se, že nějaká metoda potřebuje využít původní metodu předka, zavoláme ji tak, že před název volané metody napíšeme inherited a místo metody dané třídy se zavolá metoda předka.
Vše krásně funguje, jak ilustruje následující ukázka:

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

constructor TZvire.Init;
begin
  writeln('A new animal was created.');
end;

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

destructor TZvire.Done;
begin
end;

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

{----------------------------------------TKocka-------------------------------------------}
type PKocka=^TKocka;
            TKocka=object(TZvire)
                       ChycenoMysi:integer;
                       constructor Init; {Predefinujeme konstruktor}
                       procedure ChytMys; {Pridame jednu promennou a jednu metodu navic}
                   end;
constructor TKocka.Init;
begin
 inherited Init;     {Tady volame puvodni konstruktor TZvirete.}
 writeln('A new cat was created.');
end;


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

var Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init);
   Kocka^.VydejZvuk; {Tady vidime, ze TKocka zdedila metodu VydejZvuk}
   Dispose(Kocka,Done);
   readln;
end.

Dědičnost nám tedy může usnadnit práci. Bohužel není možné dědit od více než jednoho rodiče, což je v jednom z miliónu případů hodí.

Polymorfismus

Další z vlastností objektů je polymorfismus. To je ta vlasnost, že do proměnné typu rodič, můžeme zařadit libovolného potomka. (To proto, že potomek má zdědil všechny vlastnosti rodiče, takže se může chovat stejně jak on.) Obráceně to však nejde.

var Zvire:PZvire;
           Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init);
   Zvire:=Kocka; {Tohle prirazeni je v poradku}
   Kocka:=Zvire; {Tohle jiz ne! ackoli s jistotou vime, ze tedka je ve Zvireti objekt tridy TKocka...}
   Kocka^.VydejZvuk; {Tady vidime, ze TKocka zdedila metodu VydejZvuk}
   Dispose(Kocka,Done);
   readln;
end.

Máme např. TList - třídu pro dynamický seznam objektů třídy TObject. Ta má metody getfirst:PObject; a getnext:PObject;. My jsme do proměnné list^ (typu TList) uložili proměnné typu PString. Máme var A:PString; a rádi bychom provedli přiřazení typu A:=list^.getfirst; To ale nemůžeme, ač víme, že v tomto případě je skutečně výsledkem ukazatel na nějaký TString.Co s tím? Jediná možnost je přetypování A:=PString(list^.getfirst);.

Pascal umožňuje zjistit, jakého typu daný objekt ve skutečnosti je. Slouží k tomu metoda TypeOf(objekt):pointer. Jejím parametrem může být buď skutečný existující objekt, nebo název třídy. Tato funkce vrací ukazatel na tabulku virtuální metod. Jsou-li oba objekty stejné třídy, mají tuto tabulku stejnou. To nám umožňuje rozlišovat typy objektů. V našem případě bychom toho využili např. takto:

if TypeOf(list^.getfirst^)=TypeOf(TString) then A:=PString(list^.getfirst)
          else writeln('Objekt je spatneho typu!');

Poznámka: Tabulka virtuálních metod obsahuje ukazatele na funkce, které se mají volat pomocí tzv. pozdní (dynamické) vazby. (To jsou ty, u kterých je napsáno slovo virtual).
Není příliš šťastné, že k těmto funkcím můžeme přistupovat i pomocí TypeOf a přetypování. (Doufám, že vás to ani nenapadlo!)
Polymorfismu se nejčastěji využívá tak, že vytvořím nějakou abstraktní třídu (např. TObject) a všechny ostatní od ní odvodím. Nyní můžu napsat libovolnou třídu, která pracuje s TObjectem (ať už je to AVL strom, Hromada, či oboustranný seznam) a ukládat do ní libovolná data. Abstraktní třída mi tak slouží jen jako jakési držadlo sjednocující všechno možné.

Statická vazba

Statická vazba se využívá u nevirtuálních funkcí. Překladač prostě vidí proměnnou nějaké třídy a tak zavolá příslušnou metodu dané třídy. Občas se tato vazba hodí. Uživatel může žasnout, pokud se chování funkce mění podle toho, v jakém typu proměnné daný objekt právě "sedí".

Dynamická vazba

Dynamická (neboli pozdní) vazba, říká programu, ať se koukne, jakého typu je daný objekt ve skutečnosti (tj. na to, jako proměnná jaké třídy byl vytvořen) a zavolá příslušnou metodu této třídy (prostě při zavolání konstruktoru se nastaví ukazatel na tabulku virtuálních metod a daná funkce se spustí pomocí této tabulky). Pozdní vazby docílíme tak, že za hlavičku metody v deklaraci třídy napíšeme direktivu virtual. Jak jsme si říkali, tabulka virtuálních metod se inicializuje při zavolání konstruktoru, konstruktor tedy nemůže nikdy být virtuální (a bylo by to vůbec k něčemu užitečné?).

Zamyslete se nad tím, jak asi vypadá tabulka virtuálních metod. Byla by takhle jednoduchá, pokud bychom povolili vícenásobnou dědičnost?

Jak jsme si říkali, správná destrukce objektu je důležitá. Ovšem v Pascalu destruktor mohl správně fungovat, je nezbytné, aby byl vždy virtuální. Zamyslete se nad tímto příkladem:

var Zvire:PZvire;
           Kocka:PKocka;
begin
   Kocka:=New(PKocka,Init);
   Zvire:=Kocka;
   Dispose(Zvire,Done);
   readln;
end.

Zavolá se správný destruktor, pokud není virtuální?

Dnes to bylo jenom takové lehké opakování. Dozvěděli jsme, že i objekty je občas nutné přetypovat dolů (tj. z předka na potomka) a jak se to dělá. Ukázali jsme si použití funkce TypeOf a procedury Fail. Dále jsme se naučili rozlišovat soukromé a veřejné komponenty objektu.

Skončili jsme technický popis jazyka, takže teď bude následovat hromada domácích úkolů:

Povídáním o objektech skončila část, která se věnovala samotnému popisu jazyka Pascal. Nyní se podíváme na seznam standardních knihoven. Znalost toho, co už někdo naprogramoval za nás, nám může výrazně urychlit práci. Ale i tak berte následující kapitoly spíše jako referenční příručku. Učit se tyto věci nazpaměť nemá smysl, pokud se nechcete živit programování v Pascalu. Poté následuje popis IDE (tajná zákoutí vašeho editoru dokáží práci až dvojnásobně urychlit. Nevěříte?). A nakonec se podíváme na běžně užívané datové struktury a algoritmy.