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ů:
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:
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:
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;
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;
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:
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.
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!!!
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;