+---+ +---+ +---+
| | | | | | +---+
+---+ +---+ |
Nazdárek kašpárek všichni ti, co zas až tak úplně vůbec nevíte co
to je OOP a nebo jak vlastně OOP zvládnout v tom vašem malém milém
Turbo nebo Borland Pascalu.
Já sám se s OOP potýkám teprve nedlouho, ale vím, jak těžké bylo
začít se potýkat, protože jsem neměl zbytečné peníze na drahé knihy,
a tak mým jediným zdrojem informací byly pascalské helpy a jeden
známý, který si říká Stuff. Nebo si tak alespoň donedávna říkal.
Takže jak jste jistě všichni pochopili, tento článek je pro ty z
vás, co znají slova jako dědičnost pouze z biologie a teď se snaží
napsat svůj první objektový program.
Protože samozřejmě nejlepší metoda jak se učit, je někde něco
opsat. Takže vám zde dodám množství příkladů.
K tomu použiji unitu Objects, která je myslím součástí standartní
výbavy Pascalu. Takže jdeme na to.
V první řadě musíte zapomenout, že existuje něco, jako globální
strukturované programování. V ideálním případě by tedy měl program
vypadat takto:
Var Aplikace:PAplikace;
Begin
Aplikace:=New( PClovece,Init ); { Inicializace programu }
Aplikace^.Work; { Prace programu }
Dispose( Aplikace,Done ); { Zruseni programu }
End;
A ted přistoupíme k tomu, jak vlastně vypadá takový objekt.
OBJECT je datová struktura značně podobná struktuře RECORD. Podobně
se dokonce deklarují jednotlivé typy objektů, takzvané třídy.
Deklarace třídy TAplikace:
Type TAplikace=Object
Procedure Work;
Constructor Init;
Destructor Done;
End;
Nyní jsme si nadeklarovali třídu TAplikace. Každá třída, kterou
si nadeklarujete, má svoje instance. Příklady instancí od třídy
TAplikace jsou:
Var StatickyAplikace:TAplikace;
DynamickyAplikace:^TAplikace
Protože ale v objektovém programování nebudete vůbec používat
statických objektů, lze dynamiku ( a určitě to tak udělejte, protože
to pro pozdější využití bude potřeba ) napsat spíše takhle:
Type PAplikace=^TAplikace;
TAplikace=Object
Procedure Work;
Constructor Init;
Destructor Done;
End;
Var Aplikace:PAplikace;
Lze tedy říci, že třída objektů je ve strukturovaném programování
vlastně typ a instance je vlastně proměnná. A nyní jaké položky může
třída obsahovat. Může obsahovat všechny datové typy, které používá
strukturovaný pascal a navíc metody. Metoda je vlastně funkce nebo
procedůra, kterou vlastní nějaký objekt a je schopná operovat pouze
s proměnnými vlastněnými tímto objektem. Navíc každý objekt musí
obsahovat speciální metody konstruktor a destruktor, které správně
nastaví všechny proměnné objektu a provedou nutné kroky
inicializace.
Například TScreen bude objekt obsluhující obrazovku v grafickém
režimu 320*200*256:
Type PScreen=^TScreen;
TScreen=Object
Procedure PutPixel( X,Y:Word;Color:Byte );
Function GetPixel( X,Y:Word ):Byte;
Consrtuctor Init;
Destructor Done;
End;
Procedure TScreen.PutPixel( X,Y:Word;Color:Byte );
Begin
Mem[A000:X+Y*320]:=Byte;
End;
Procedure TScreen.GetPixel( X,Y:Word ):Byte;
Begin
GetPixel:=Mem[A000:X+Y*320];
End;
Constructor Init;
Begin
Asm
mov ax,13h
int 10h
End;
End;
Destructor Done;
Begin
Asm
mov ax,3h
int 10h
End;
End;
Zde sice objekt nevlastnil žádné proměnné, ale konstruktor i
destruktor byly nutné pro nastavení obrazovkového módu. Obě metody (
PutPixel a GetPixel ) snad není nutno vysvětlovat.
Každá třída může mít vlastnosti společné s jinou třídou objektů.
Existují dva takovéto vztahy, jedním z nich je dědičnost a druhý
vlastnictví. Dědičnost znamená, že nějaká třída je potomkem třídy
jiné, je datově kompatibilní a má všechny vlastnosti svého
předchůdce. Příklad dědičnosti může být takovýto:
TStavba=Object
X,Y:Byte; { souřadnice stavby }
Procedure Draw; { metoda na vykreslení
na obrazovku }
Procedure SetXY( _X,_Y:Byte );
Constructor Init( _X,_Y:Byte );
{ nastaví proměnné }
Destructor Done;
End;
TDum=Object( TStavba ) { dům je potomkem stavby }
Bytu:Byte; { bude mít navíc ještě proměnou
vyjadřující nějakou vlastnost,
která objektu TStavba chybí }
Constructor Init( _X,_Y,_Bytu:Byte );
{ nastaví proměnné }
End;
Vzhledem k rozdílné datové struktuře pak konstruktory vypadají
takto:
Constructor TStavba.Init( _X,_Y:Byte )ů
Begin
X:=_X;
Y:=_Y;
End;
Constructor TDum.Init( _X,_Y,_Byytu:Byte );
Begin
Inherited Init( _X,_Y );
Domu:=_Domu;
End;
A navíc budeme schopni provést například takovéto přiřazení:
Var Dum:PDum;
Begin
Dum:=New( PDum,Init( 10,20,30 );
Dum^.SetXY( 30,20 ); { přiřazení přímo
instanci }
PDum( Dum )^.SetXY( 30,20 ); { přiřazení přetypovaním
stejným typem }
PStavba( Dum )^.SetXY( 30,20 ); { přiřazení přetypovaním
typem předchůdce }
End;
Z těchto přiřazení je hlavně zřejmá datová kompatibilita obou
tříd objektů, která se bude hodit hlavně u abstraktních metod.
Syntaxe zápisu metod není nikterak složitá. Jak jste si jistě
již všimli v příkladech na objekty, které jsem zde již napsal, v
metodě se používají proměnné daného objektu přímo, jako by to byly
lokální proměnné dané metody. Zvláštním případem je
pak identifikátor SELF, který vlastně zprostředkovává samu instanci
objektu. Využití tohoto identifikátoru je patrné hlavně pokud
přejdeme k druhému vztahu objektů, a to je vlastnění se. Objekt
vlastní jiný objekt jednoduše právě tehdy, když vlastní ukazatel na
něj. Příklad: Objekt TVeverka je potomkem nějakého obecného objektu
zvířátko a bude vlastnit objekt TZuby.
TVeverka=Object( TZviratko )
Zuby:PZuby;
Procedure Kousni; { metoda, která použije
vlastněný objekt }
End;
Potom, pokud bude chtít přistoupit na nějakou metodu třídy TZuby,
je schopen zavolat jejich metodu. Příklad takovéhoto zavolání je
metoda Kousni:
Procedure TVeverka.Kousni;
Begin
Zuby^.KSobe;
Zuby^.OdSebe;
End;
Vlastnění se je jediná možnost, jak v pascalu provést určité
zvláštnosti v dědičnosti. Typickým příkladem je to, že má objekt
dědit od dvou tříd současně. V paskalu to naní možné udělat přímo.
Představte si objekt THasubót, který by měl některé vlastnosti
dědit od třídy TLoď (plave přece na vodě ) a některé od třídy TDům,
protože je obytný. Strom dědičnosti by pak vypadal asi takto:
TObject
/ \
TStavba TLod
/ /
TDum /
\ /
\ /
\ /
THausbot
To samozřejmě paskal nedovolí, a proto se to řeší tak, že objekt
THausbot bude potomkem objektu TLod, který bude vlastnit TDum, tedy
vlastně hausbót je loď, která má na sobě dům. V programu to bude
vypadat takto:
THausbot=Object( TLod )
Dum:PDum;
End;
Další situace, kterou je nutno řešit, je, pokud se dva objekty
vlastní navzájem. Představte si například hru člověče nezlob se,kde
objekt TPolíčko vlastní ( ukazuje na ) objekt třídy TFigurka, který
na políčku stojí a každá figurka taktéž vlastní ( ukazuje na )
jedno políčko, na kterém stojí. Potom právě využijeme operátoru
SELF, protože s přesunem figurky je nutné změnit oba pointery. Tedy
třídy vypadají takto:
TFigurka=Object( THraciObjekt )
Policko:PPolicko;
Procedure PosunseNaPolicko( _Policko:PPolicko );
End;
TPolicko=Object( THraciObjekt )
Figurka:PFigurka;
Procedure AkceptujFigurku( _Figurka:PFigurka );
End;
Procedure TFigurka.PosunseNaPolicko( _Policko:PPolicko );
Begin
Policko:=_Policko;
Policko^.AkceptujFigurku( @SELF ); { poskytne políčku
pointer na sebe sama }
End;
Procedure TPolicko.AkceptujFigurku( _Figurka:PFigurka );
Begin
Figurka:=_PFigurka;
End;
Když už tak perfektně ovládáme dědičnost,je důležité, abychom se
naučili ji maximálně využívat. K tomu nám pomůžou metody nazvané
virtuální. Virtuální metoda je metoda, která má stejný interface,
ale různý kód pro různé potomky. Interface není přímo stejný, ale
opět pouze datově kompatibilní ( tzn může mít více, ale nikoliv
méně parametrů ). Příklad virtuální metody může být například
metoda, která v rámci nejakého jednoduchého simulátoru řekněme
zoologické zahrady zobrazuje jednotlivá zvířata ( pro jednoduchost
předpokládám třeba textový režim ).
TVeverka a TTygr jsou potomky TZvířete:
TZvire=Object
X,Y:Word;
Zivoty:Real;
Procedure NakresliSe; Virtual;
End;
TVeverka=Object( TZvire )
Zivoty:Real;
Procedure NakresliSe; Virtual;
End;
TTygr=Object( TZvire )
Zivoty:Real;
Procedure NakresliSe; Virtual;
End;
Procedure TZvire.NakresliSe;
Begin
GotoXY( X,Y );
End;
Procedure TVeverka.NakresliSe;
Begin
Inherited NakresliSe ;
Write( 'v' );
End;
Procedure TTygr.NekresliSe;
Begin
Inherited NakresliSe ;
Write( 'T' );
End;
Z tohoto příkladu ( i když není zrovna nejlepší ) jsou myslím si
docela dobře vidět vlastnosti dědění u virtuálních metod, hlavně
možnost použít identifikátoru Inherited pro zavolání již napsaného
kusu kódu. Zde bych chtěl upozornit na jednu věc. Předpokládejme,
že konstruktory TZvíře.Init a TVeverka.Init mají rozdílný počet
paramterů. Pak vypadá použití identifikátoru Inherited takto:
Constructor TZvire.Init( _X,_Y:Word );
Begin
X:=_X;
Y:=_Y;
End;
Constructor TVeverka.Init( _X,_Y:Word;_Zivoty:Real );
Begin
Inherited Init( _X,_Y );
Zivoty:=_Zivoty;
End;
Zde už je úspora trošku více znát a je i vidět způsob volání již
existující zděděné části.
Speciálním případem virtuální metody je metoda abstraktní. Její
využití je důležité tam, kde v době deklarace předka ještě neznáte
typ následníka. Typický případ je porovnávání podle obecné položky.
Představte si, že máte třídu TEntita, která vlastní pole nějakých
objektů TPoložka. A potom máte třídu TSeznam, která je spojovým
seznamem TPoložek. A vy chcete TPoložky v TSeznamu seřadit podle
n-té TPoložky. A každá TPoložka je jiného typu. Jak to vyřešíme?
Docela jednoduše - abstraktní metodou. Třída TPoložka bude mít
virtuální metodu JsiVětšíNež, která bude boolovská funkce. Ta bude
definována jako abstraktní ( je povinná pro všechny následníky, ale
její kód závisí na datové struktuře ). Pascal nezná přímo klíčové
slovo Abstract, ale lze abstraktní metody napsat. Ukážeme si tedy
abstraktní metodu pro potomky třídy TPoložka:
TPolozka=Object
Data:String; { data jsou ulozena ve stringu,
nemusi byt ale nutne string }
Function JsiVetsiNez( _Kdo:PPolozka ):Boolean;
Virtual;
End;
Function TPolozka.JsiVetsiNez( _Kdo:PPolozka ):Boolean;
Begin
End; { nic neprovadi ( vlastne implicitne vraci true ) }
TJmeno=Object( TPolozka )
Function JsiVetsiNez( _Kdo:PPolozka ):Boolean;
Virtual;
End;
TVyska=Object( TPolozka )
Function JsiVetsiNez( _Kdo:PPolozka ):Boolean;
Virtual;
End;
Function TJmeno.JsiVetsiNez( _Kdo:PPolozka ):Boolean;
Begin
If Data>_Kdo^.Data { porovnani stringu, u jmena postacuje }
Then
JsiVetsiNez:=True
Else
JsiVetsiNez:=False;
End;
Function TVyska.JsiVetsiNez( _Kdo:PPolozka ):Boolean;
Var Tmp1,Tmp2:Real;
Code:Integer;
Begin
Val( Data,Tmp1,Code );
Val( _Kdo^.Data,Tmp2,Code );
If Tmp1>Tmp2 Then
JsiVetsiNez:=True
Else
JsiVetsiNez:=False;
End;
Myslím si, že tento příklad je dostatečně názorný. Pokud pak
chcete takovýto seznam setřídit například podle čtvrté datové
položky, pustíte na něj jednoduše bubble sort tímto způsobem (
předpokládám, že metoda Prohoď patří TSeznamu a že není problém ji
napsat ). Takto pak vypadá celý problém:
Tseznam=Object
Entita:PCollection; { jsem liny, pouzivam pascalsky
spojovy seznam }
Procedure SetridSe( PodleCeho:Byte ); { nadefinujeme
si }
Procedure Prohod( _Koho,_Skym:PEntita );
{ predpokladam, ze neni problem
napsat prohozeni dvou pointeru }
End;
TEntita=Object
Polozka:Array[ 1..10 ]Of PPolozka; { vsimnete si
obecne definice
typu }
End;
Jak vypadá položka jsme si již nadefinovali včetně několika
potomků. Metody potom vypadají takto:
Procedure TSeznam.SetridSe( PodleCeho:Byte );
Var I:Byte;
Begin
{ jak vypada bubble sort snad vsichni vite, takze zde vypisi
pouze vnitrek toho while cyklu }
For I:=1 To Entita^.Count-1 Do { od 1 do poctu polozek-1 }
If Entita^.At( I )^.Polozka[ PodleCeho ]^.JeVetsiNez(
Entita^.At( I+1 )^.Polozka[ PodleCeho ] )
Then
Prohod( Entita^.At( I ),Entita^.At( I+1 ));
{ myslim si, ze genialita je natolik jasna, ze
netreba objasnovat, ale prece pro jistotu: i-ta
entita se podle sveho typu porovna s i+1-ni a sama
vrati pomoci funkce JeVetsiNez splneni nebo taky
nesplneni podminky pro bouble sort }
End;
Myslím si, že tento vyčerpávající pokus ukázat vám funkci metod,
které se jmenují abstraktní a virtuální k něčemu byl a že jste jakž
takž pochopili, o co v OOP jde. Takže na závěr ještě pár zásad
pro úspěšné používání.
Vždy si udělejte OBJEKTOVOU ANALÝZU a nestyďte si nakreslit si
na papír strom dědičnosti a pacičky, jak se objekty vlastně budou
vlastnit. Čím více tříd objektů, tím lépe se vám bude pracovat.
Využívejte možnosti paskalu rozdělit si program do několika unit a
v každé se starejte o něco jiného. Pokud se něco tváří jen trochu
obecně, udělejto to obecné a teprve potomky konkretizujte. A navíc
se nebojte udělat si více hladin dědičnosti.
Hodně štěstí v OOP vám přeje
Marky Parky
výheň