V předchozích dílech jsme se zabývali mnoha způsoby práce s Delphi, zatím jsme si ale neřekli nic konkrétního o objektově orientované architektuře, na které je Delphi vlastně postaveno.
Řešení domácího úkolu z minulého dílu
Chcete-li mít znak „&“ přímo v textu položky menu, v návrhu musíte ampersand zdvojit, tedy napsat např. „Vašek && Eva“. Chcete-li navíc podtrhnout (učinit aktivním) písmeno k, napište „Vaše&k && Eva“.
Objektově orientovaná architektura
Důvod, proč jsem zařadil popis objektově orientované architektury, spočívá v tom, že si nejsem jist, zda je vhodné toto téma ignorovat a opájet se představou, že jej buď všichni znají, nebo nepotřebují.
Co to je vlastně objektově orientované programování? Často vypadá spíše jako náboženství než jako přístup k programování. Tento programovací styl používá oddělené objekty, které obsahují (zapouzdřují) svá data i kód. Tyto objekty jsou stavebními prvky aplikace. Obvyklým důvodem používání objektů je možnost jednodušších zásahů do programu. Fakt, že data i kód jsou jaksi pohromadě (a že si tedy každý objekt plně za svá data zodpovídá), znamená, že proces odstraňování chyb (a také modifikace vlastností objektu) má minimální efekt na okolní objekty.
Jazyk objektově orientovaného programování obvykle zahrnuje implementaci alespoň tří principů:
- zapouzdření – kombinace vzájemně svázaných datových položek a metod, které nad nimi pracují;
- dědičnost – možnost vytvářet objekty vycházející z jiných objektů. Tento princip dovoluje vytvářet hierarchii – nejprve se vytvoří obecný objekt a postupným zpřesňováním vznikají specifičtější následníci;
- polymorfismus – volání metod proměnné typu třída způsobí volání kódu patřícího k instanci objektu (viz níže), kterou proměnná právě obsahuje.
Pokud bychom se měli pokusit stanovit základní dva termíny objektově orientovaného programování, šlo by zřejmě o třídu a objekt.
Třída je datový typ, který si můžete představit jako jakousi šablonu určitých objektů (třeba aut), která popisuje chování konkrétních objektů (aut). Konkrétní auta vytváříme podle této „šablony“. Třída obsahuje nějaká svá (interní) data (tzv.
atributy) a své
metody (tj. procedury a funkce). Třída by měla charakterizovat chování a vlastnosti několika podobných objektů (např. aut více značek).
Objekt je instancí třídy, jakýmsi konkrétním výskytem (konkrétním, fyzicky existujícím exemplářem auta). Jinak řečeno, je to proměnná datového typu, který představuje třída. Objekty při běhu programu zabírají paměť pro svou reprezentaci.
Vztah mezi objektem a třídou si lze představit třeba jako vztah mezi proměnnou a datovým typem.
Abychom si vše názorně předvedli (a také abychom si ukázali, jak to prakticky udělat v Object Pascalu - a tedy také v Delphi), vytvoříme třídu automobil, která bude mít následující atributy:
- Značka – značka automobilu, typ = řetězec;
- Rok výroby – typ = celé číslo;
- Benzín – množství benzínu v nádrži, typ = celé číslo;
- Kapacita – objem nádrže, typ = celé číslo.
Třída bude mít tyto metody:
- Vypiš informace – vypíše všechny atributy;
- Natankuj – naplní nádrž o dané množství litrů. Pokud se dané množství do nádrže nevejde, doplní nádrž na maximum a vrátí false (jako varování).
Abychom hovořili všichni o tomtéž, provedeme to tak: v Delphi vytvoříme novou aplikaci s jedním formulářem, na kterém bude jedno tlačítko.Veškerý níže uvedený kód budeme psát do modulu tohoto formuláře (standardně Unit1.pas).
type
TAuto = class
Znacka: String;
RokVyroby, Benzin, Kapacita: Integer;
procedure VypisInfo;
function Natankuj(Kolik: Integer): Boolean;
end;
Podotýkám, že tento kód se bude vyskytovat v sekci interface příslušného modulu (souboru). Abychom s touto třídou mohli pracovat, je ještě nutné říci, jak budou vypadat těla zmíněných metod. Tato těla budou zapsána v sekci implementation (rozdíl mezi oběma sekcemi vysvětlíme níže), a aby kompilátor věděl, ke které třídě budou těla patřit (lze totiž mít víc různých tříd a v každé např. metodu s názvem Natankuj), používá se v Object Pascalu tzv. tečková notace:
procedure TAuto.VypisInfo;
begin
ShowMessage( Format(`%s, %d: %d (%d).`,
[Znacka, RokVyroby, Benzin, Kapacita]) );
end;
function TAuto.Natankuj(Kolik: Integer): Boolean;
begin
Result := (Benzin + Kolik) <= Kapacita;
Benzin := Max(Kapacita, (Benzin + Kolik));
end;
Poznámky:
- Proměnné Result se používá k navrácení hodnoty funkce; v Delphi se příliš nepoužívá klasického „pascalského“ zápisu „název_funkce := návratová_hodnota.“
- Funkce Max (vrací maximum ze dvou argumentů, návratový typ závisí na typu argumentů) se nachází v jednotce Math, měli byste tedy tuto jednotku zapsat do sekce uses.
Nyní ještě provedeme deklaraci proměnné typu třída a ukážeme si, jak metody volat a jak s proměnnou pracovat. Následující kód bude umístěn v sekci implementation, v jakékoliv proceduře či funkci (např. v metodě ošetřující událost OnClick nějakého tlačítka umístěného na formulář).
procedure TwndHlavni.btnStartClick(Sender: TObject);
var
MujBlesk: TAuto;
begin // (A)
MujBlesk.Znacka := `Skoda 1000MB`; // (B)
MujBlesk.RokVyroby := 1950;
MujBlesk.Benzin := 0;
MujBlesk.Kapacita := 5;
MujBlesk.VypisInfo;
if not MujBlesk.Natankuj(2) then
ShowMessage(`Nepřehánějte to s tím benzínem!`);
MujBlesk.VypisInfo;
end;
Nyní si zkuste program zkompilovat a spustit. Vše bude v pořádku, ale jen do okamžiku, než kliknete na „startovací“ tlačítko. Pak se program zboří. (Tedy – nezboří se úplně, ale fungovat nebude a bude generována tzv. výjimka – viz dále).
Proč tomu tak je? Vysvětlení příčiny je složitější a souvisí se základní myšlenkou objektově orientovaného modelu. Musíme si říci několik informací o vytváření instancí. Následující řádky jsou klíčové pro pochopení OOP.
Základní myšlenka objektově orientovaného modelu spočívá v tom, že proměnná datového typu třída (nemluvíme o instanci objektu, jen o proměnné), jako je např. MujBlesk z předchozího příkladu, neobsahuje "hodnotu" objektu. Neobsahuje ani objekt auto ani atributy auta. Obsahuje pouze odkaz (ukazatel) na místo v paměti, kde je vlastní objekt fyzicky uložen.Vytvoříme-li proměnnou tak, jak jsme to předvedli o pár řádků výše (pomocí klíčového slova var), nevytvoříme zmíněnou fyzickou reprezentaci objektu (místo pro uložení objektu v paměti), ale jen odkaz na objekt (místo pro uložení tohoto odkazu v paměti)! Vlastní instanci musíme vytvořit ručně zavoláním jeho metody Create, což je tzv. konstruktor (procedura určená k alokování paměti a k inicializaci objektu).
Řešení je tedy prosté: mezi řádky označené (A) a (B) v předchozí proceduře vsuneme volání konstruktoru:
…
begin // (A)
MujBlesk := TAuto.Create;
MujBlesk.Znacka := `Skoda 1000MB`; // (B)
…
Kde se vzal konstruktor Create? Je to konstruktor třídy TObject, od něhož všechny ostatní třídy (a tedy i tato) dědí (viz dále).
Když jsme objekt vytvořili, je třeba jej nakonec také zrušit. To provedeme zavoláním metody Free:
…
MujBlesk.VypisInfo;
MujBlesk.Free;
end;
Metodu Create jsme volali kvůli přidělení paměti objektu. Často ale objekt potřebujeme také inicializovat. Za tím účelem přidáváme do třídy konstruktor. Lze použít upravenou verzi metody Create nebo definovat konstruktor úplně nový. Není však příliš vhodné pojmenovat konstruktor jinak než Create.
Konstruktor je velmi specifická procedura, protože Delphi samy alokují paměť pro objekt, nad kterým jej spustíte. Použití konstruktoru tedy za vás vyřeší problémy s alokací paměti. Konstruktor se deklaruje užitím klíčového slova constructor. Přidáme tedy do třídy TAuto konstruktor:
type
TAuto = class
Znacka: String;
RokVyroby, Benzin, Kapacita: Integer;
constructor Create(ZZnacka: String; RRokVyroby, BBarva, BBenzin, KKapacita: Integer);
procedure VypisInfo;
function Natankuj(Kolik: integer): Boolean;
end;
Musíme také zapsat tělo konstruktoru. To se zapisuje kamkoliv do modulu, klidně mezi další procedury a funkce, ale konvencí je zapisovat jej jako první podprogram modulu, hned na počátku sekce implementation. Správně bychom měli v konstruktoru každé nově vytvořené (odděděné – viz dále) třídy nejdříve vyvolat konstruktor předka a následně uvést své vlastní, specializující příkazy. Pro třídu odděděnou od TObject to jistě není třeba, ale přesto je to vhodné a formálně správné.
constructor TAuto.Create(ZZnacka: String; RRokVyroby, BBarva, BBenzin, KKapacita: Integer);
begin
inherited Create;
Znacka := ZZnacka;
RokVyroby := RRokVyroby;
Benzin := BBenzin;
Kapacita := KKapacita;
end;
Takhle teď bude vypadat tělo procedury btnStartClick, ve které pracujeme s objektem MujBlesk třídy TAuto:
procedure wndHlavni.btnStartClick(Sender: TObject);
var
MujBlesk: TAuto;
begin
MujBlesk := TAuto.Create(`Skoda 1000MB`, 1950, 0, 5);
MujBlesk.VypisInfo;
if not MujBlesk.Natankuj(2) then
ShowMessage(`Nepřehánějte to s tím benzínem!`);
MujBlesk.VypisInfo;
MujBlesk.Free;
end;
Destruktor je jednoduše řečeno opakem konstruktoru. V předchozím příkladu jsme se s ním již setkali (v řádce MujBlesk.Free). Implicitní název je tedy Free. Jeho funkcí je „zničit“ objekt (uvolnit jej z paměti). Platí, že dynamickou paměť, kterou jsme alokovali v konstruktoru, bychom měli v destruktoru uvolnit.
Třída může obsahovat teoreticky jakékoliv množství atributů a metod. Správně by měla být vlastní data třídy skrytá (neboli zapouzdřená) uvnitř třídy. K těmto skrytým datům by měly přistupovat pouze metody téže třídy. Není vhodné nechat kohokoliv manipulovat s daty naší třídy. Jedním z principů objektově orientovaného programování je „
každý je zodpovědný za svá data“.
Optimální přístup (z hlediska zásad OOP) je tedy takový: nelze „zvenku“ přistupovat k datům, ale jsou k dispozici veškeré potřebné metody, které tento přístup (a to jak čtení, tak zápis) zajišťují. Tím je zajištěn tzv. autorizovaný přístup k datům.
V Object Pascalu toho dosáhneme používáním specifikátorů přístupu:
- private – takto označené atributy a metody nejsou přístupné vně třídy;
- public – označuje položky, které jsou volně přístupné z jakékoliv části programu (ve které je viditelná jejich třída);
- protected – částečně chráněné položky. Může k nim přistupovat pouze vlastní třída a všichni její potomci (třídy vzniklé dědičností, viz dále).
- published – položka nebo metoda published je přístupná nejen za běhu, ale i při tvorbě aplikace, z hlediska aplikace je viditelná stejně jako položka public.
Ukážeme si, jak zajistit autorizovaný přístup k datům v naší třídě TAuto (protected atributy využijeme ve zděděné třídě TNakladak – viz dále).
type
TAuto = class
protected
Znacka: String;
RokVyroby, Benzin, Kapacita: Integer;
public
constructor TAuto.Create(ZZnacka: String; RRokVyroby, BBarva, BBenzin, KKapacita: Integer);
procedure VypisInfo;
function Natankuj(Kolik: integer): Boolean;
end;
Poznámka pro pokročilé uživatele:
V rámci pravdomluvnosti je nutné podotknout, že v Delphi, na rozdíl třeba od C++, je trochu jiný význam specifikátoru private. V C++ není sekce private přístupná nikomu jinému než vlastní třídě, zatímco zde můžeme k private položkám přistupovat z celého modulu (zdrojového souboru), ve kterém je daná třída deklarována.
Sekce modulu interface a implementation
Do sekce interface zapisujeme vše, co má být viditelné i v ostatních modulech – datové typy, hlavičky procedur, funkcí apod. V sekci implementation se jednak vyskytují těla funkcí a procedur, jejichž hlavičky jsou v interface, a jednak všechny „soukromé“ prvky (tedy klidně i další funkce potřebné jen v rámci modulu).
Dědičnost je vlastnost objektově orientovaného programování, kterou mohu využít, pokud potřebuji vytvořit novou třídu podle vzoru konkrétní (již hotové) třídy, ale s dalšími specifickými vlastnostmi (tedy poněkud "více konkrétní" třídu).
Jako příklad uvedeme vytvoření nákladního auta pomocí existující třídy auto. Třída nákladní auto bude mít navíc atribut nosnost a bude mít vlastní konstruktor. Metoda VypisInfo bude navíc vypisovat i nosnost vozu.
type
TNakladak = class (TAuto) // dědíme od třídy TAuto
private
Nosnost: integer;
public
constructor Create(ZZnacka: String; RRokVyroby, BBarva, BBenzin, KKapacita, NNosnost: Integer);
procedure VypisInfo;
end;
Těla konstruktoru a pozměněné metody:
constructor TNakladak.Create(ZZnacka: String; RRokVyroby, BBarva, BBenzin, KKapacita, NNosnost: Integer);
begin
inherited Create(ZZnacka, RRokVyroby, BBarva, BBenzin, KKapacita);
Nosnost := NNosnost;
end;
procedure TNakladak.VypisInfo;
begin
ShowMessage(Format(`%s, %d: %d (%d). Nosnost = %d`,
[Znacka, RokVyroby, Benzin, Kapacita, Nosnost]));
end;
Nyní si vyzkoušíme práci s novou třídou:
var
…
Vejtraska: TNakladak;
begin
…
Vejtraska := TNakladak.Create(`Avia`, 1980, 20, 200, 10);
…
Vejtraska.VypisInfo;
…
Vejtraska.Free;
end;
Poznámka k typové kompatibilitě v případě dědičnosti: objekt třídy potomka můžeme kdykoliv použít na místě objektu třídy předchůdce. Opačný postup však není možný. Příklad:
MujBlesk := Vejtraska; // lze
Vejtraska := MujBlesk; // NELZE, chyba!!!
Polymorfismus, virtuální a abstraktní metody
Bohužel, článek je již teď mnohem delší, než je únosné, proto nezbývá, než další koncepty zmínit jen velmi stručně. Pascalovské funkce a procedury jsou obvykle založeny na tzv.
statické vazbě. To znamená, že volání metody je „vyřešeno“ již překladačem (a linkerem). Všechny metody z našich dosavadních příkladů mají statické vazby. Objektově orientované jazyky však umožňují použití jiného druhu vazby, známého jako
pozdní vazba (někdy též dynamická).
Výhoda tohoto přístupu je známa jako polymorfismus. Předpokládejme, že naše dvě třídy TAuto a TNakladak definují metodu s dynamickou vazbou. Potom lze tuto metodu aplikovat na všeobecnou proměnnou (jako např. MujBlesk), která může za běhu odkazovat na objekty obou tříd. Určení metody, která bude volána, bude provedeno za běhu, podle konkrétní situace. K definici se používá klíčových slov virtual a override:
type
TAuto = class
procedure VypisInfo; virtual;
end;
TNakladak = class(TAuto)
procedure VypisInfo; override;
end;
Klíčovým slovem abstract deklarujeme metody, které budou definovány až v potomcích současné třídy. Prakticky z toho plyne, že ve třídě nemusíme zapisovat (definovat) tělo metody deklarované jako abstraktní.
type
TAuto = class
procedure Zmen_rok; virtual; abstract;
end;
Problémem je, že víc toho z objektově orientovaných principů probrat nezvládneme. Není na to prostor. Pokud byste měli eminentní zájem o další popis této problematiky, vyjádřete se v diskusi, jinak musíme toto téma uzavřít.