Vítejte v pátém dílu malého seriálu o jazyku Object Pascal určeném pro začátečníky. Potřetí se budeme zabývat OOP (zkratku pro objektově orientované programování už znáte) a předchozí program výrazně vylepšíme tím, že začneme používat jednotky (knihovny) v Delphi. Přeji vám pohodu při čtení a programování :-).
Úvod
Pátý díl seriálu o jazyku Object Pascal je tedy (jako dva předchozí) věnován objektově orientovanému programování (OOP) a dále knihovnám neboli jednotkám v Delphi. V první kapitole se podíváme na domácí úkol z minulého dílu. Ve druhé kapitole zjistíme, že lze psaní programů v Delphi dále zefektivnit, a to pomocí unit (jednotek, knihoven). Jednu knihovnu si připravíme pro dva předchozí programy.
OOP a jednotky jsou velkými pomocníky; jakmile se s nimi seznámíte (čím dřív, tím líp), nebudete se asi chtít vracet zpět ke staršímu způsobu práce.
Mohu k tomu uvést jednu myslím typickou zkušenost: nedávno jsem na internetu našel pěkný program, u kterého mi ale nějaká maličkost chyběla. Obrátil jsem se na tvůrce s prosbou, jestli nechce věc doplnit nebo poskytnout kód k dopracování ostatním ve stylu open source projektů. Dostal jsem rychlou, vstřícnou, ale zamítavou odpověď. Příčinu odmítnutí autor i vysvětlil: nešlo o snahu chránit zdrojový kód, ale o to, že psal svůj program v době, kdy ještě neznal OOP ani knihovny, takže je zdrojový kód tak nepřehledný, že by se mu doplněk programoval velmi obtížně (natož někomu jinému, kdo původní program nezná). Vyrozuměl jsem, že by se tvůrce asi i styděl podobný kód někomu předložit.
Právě jednotky spolu s OOP umožňují psát programy s přehledným, znovupoužitelným a snadno upravovatelným kódem!
1. kapitola: řešení úkolu ze 4. dílu
Šlo o toto: Zkuste napsat program pro řešení kvadratické rovnice s pomocí tříd a objektů.
Doporučil jsem ale nezačít hned programovat, nýbrž:
- Přípravná fáze: provést podobnou analýzu a návrh třídy (atributů a metod) jako v minulém dílu, tentokrát pro úlohu řešení kvadratické rovnice.
- Implementace: teprve pak úkol naprogramovat (implementovat).
1. Přípravná fáze
Nejdřív tedy opět jakýsi plán na úvod. Zde bude ale velmi podobný přípravě na řešení příkladu z předchozího dílu:
- Co by měl program umět? Tedy: fáze shromažďování požadavků a analýza požadavků.
Měl by vyřešit kvadratickou rovnici. Bude požadovat zadání tří koeficientů (a, b, c) a potom by měl vypsat dva kořeny rovnice, pokud existují. Nebo napsat, že rovnice má jeden (dvojnásobný) kořen, popř. že nemá kořen žádný. - Jaké třídy (a tím také objekty) v programu použijeme? Tedy: fáze analýzy.
Naštěstí je i tento program velmi jednoduchý. Asi opět postačí jediná třída, kterou naučíme řešit kvadratickou rovnici. Z této třídy se pak v programu vytvoří objekt (neboli instance třídy), zastupující jednu konkrétní rovnici, kterou taky hned sám svými metodami vyřeší. Pokud bychom chtěli vyřešit rovnic víc, můžeme si z naší třídy třeba vytvořit víc různých instancí - objektů, v našem případě víc konkrétních rovnic i s řešením. Třídu bych nazval třeba TKvadrRovnice. - Jak bude třída detailně vypadat (atributy, metody, jejich parametry)? Tedy: fáze návrhu.
-
Atributy: Naše třída TKvadrRovnice by mohla mít šest atributů (vlastností, hodnot zastupovaných nějakými proměnnými): nejdřív tři atributy pro koeficienty, označím je a, b, c. Ty budou jistě pěkně zapouzdřené a chráněné uvnitř, čili s přístupovým právem (modifikátorem přístupu) private. Dál by mohla mít další dva privátní atributy zastupující možné kořeny rovnice: x1, x2. A užitečným atributem by mohl být pocetKorenu, který bude obsahovat právě tuto důležitou informaci.
Otázka: Promyslete, jaké by mohlo být přístupové právo této proměnné: private nebo public? (Odpověď je na konci článku.)
Atributům musíme ještě přiřadit vhodné datové typy, zde vesměs real a u pocetKorenu integer. - Metody: K atributům přidáme metody pro operace s daty. Navrhuji metodu najdiKoreny(), která kořeny vypočte. I tu bych nechal privátní. No a aby bylo vůbec možno se k atributům dostat, musíme nadefinovat také metody veřejné: třeba zadejKoeficienty(), vratKorenX1() a vratKorenX2(). Mohla by se hodit ještě metoda vratPocetKorenu(), která oznámí, zda jsou kořeny dva, jeden či žádný (o tom píšu také na konci v odpovědi na Otázku 1).
Protože metoda najdiKoreny() vrací až dvě hodnoty (kořeny) a ještě třetí hodnotu (číslo, udávající počet kořenů, to se uloží do atributu pocetKorenu), nemůže se už jednat o funkci (výstupem funkce musí být právě jedna hodnota), ale půjde o proceduru. O funkcích a procedurách toho možná ještě moc nevíte, ale zatím nám myslím postačí intuitivní porozumění.
Nakonec bych přidal ještě metody pro vrácení hodnot koeficientů (vratKoefA() apod.) a také konstruktor, čili metodu, která instanci (přesněji odkaz na instanci) vytvoří. Naprogramujme konstruktor tak, že přiřadí koeficientům nějaké rozumné počáteční hodnoty, např. a=1, b=2 a c=1, u kterých známe kořeny (bude to jeden dvojnásobný kořen s hodnotou -1). A konstruktor by mohl rovnou rovnici vyřešit voláním soukromé metody najdiKoreny().
Stejně tak metoda pro zadání hodnot koeficientů rovnou i vyřeší rovnici s těmito novými koeficienty, ať se nám nestane, že bychom zadali nové hodnoty koeficientů a bez znovuvyřešení rovnice nechali vypsat staré kořeny.
Myslím, že takto navržený konstruktor nakonec nebude ideální, ale zatím to tak zkusme. Vylepšení si necháme na další díl.
-
Atributy: Naše třída TKvadrRovnice by mohla mít šest atributů (vlastností, hodnot zastupovaných nějakými proměnnými): nejdřív tři atributy pro koeficienty, označím je a, b, c. Ty budou jistě pěkně zapouzdřené a chráněné uvnitř, čili s přístupovým právem (modifikátorem přístupu) private. Dál by mohla mít další dva privátní atributy zastupující možné kořeny rovnice: x1, x2. A užitečným atributem by mohl být pocetKorenu, který bude obsahovat právě tuto důležitou informaci.
-
Grafické znázornění: Strukturu třídy ukažme pro názornost opět obrázkem, který vychází z jazyka Unified Modeling Language (UML), používaného při analýze a návrhu. Znaménko '-' znamená private, '+' public.
TKvadrRovnice - a: real
- b: real
- c: real
- x1: real
- x2: real
- pocetKorenu: integer- procedure najdiKoreny()
+ constructor vytvorRovnici()
+ procedure zadejKoeficienty(a, b, c: real)
+ function vratKoefA(): real
+ function vratKoefB(): real
+ function vratKoefC(): real
+ function vratPocetKorenu(): integer
+ function vratKorenX1(): real
+ function vratKorenX2(): real
Na rozdíl od konstruktoru jsou ostatní metody v našem příkladu metody instance - vždy je provádí už nějaký dříve vytvořený objekt. K metodám třídy se dostanu podrobněji později.
2. Implementace
Konečně se pustíme do programování. Předchozí příprava se může zdát zdlouhavá, ale čím je tato fáze důkladnější, tím snáze se pak programuje a tím méně hrozí, že bude nutno program záhy doplňovat a opravovat, aby fungoval skutečně tak, jak si představoval zadavatel.
1. řešení - bez knihovny
V minulém dílu jsme psali program pro řešení lineární rovnice tak, že jsme definici třídy a jejích metod umístili do jediného souboru i s kódem samotného programu, který s objekty pracoval (v Delphi jde o hlavní program, soubor se zdrojovým kódem s příponou .dpr - Delphi project). I první řešení nového úkolu udělejme ještě takto, ať si později snáze uvědomíte rozdíl a výhody použití přiložené knihovny. Program nazvu třeba P_kvadrRovnice (podle písmene P později bezpečně poznám, že jde o hlavní Program neboli v Delphi Project). Samozřejmě ho uložím do zvláštní nové složky.
Zdrojový kód programu by mohl vypadat třeba takto. Všimněte si opět toho, že část programu, ve které se deklaruje třída a její metody, je dost obsáhlá. O to kratší a příjemnější je ale vlastní program, který už jen využívá metody instance vytvořené ze třídy (neboli jinými slovy: posílají se zde instanci zprávy o tom, co má dělat). Doporučuji vám zkusit ho napsat samostatně!
program P_kvadrRovnice;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
//**** definice třídy pro řešení kvadratické rovnice
TKvadrRovnice = class
private
a: real;
b: real;
c: real;
x1: real;
x2: real;
pocetKorenu: integer;
procedure najdiKoreny();
public
constructor vytvorRovnici();
procedure zadejKoeficienty(a, b, c: real);
function vratKoefA(): real;
function vratKoefB(): real;
function vratKoefC(): real;
function vratPocetKorenu(): integer;
function vratKorenX1(): real;
function vratKorenX2(): real;
end;
//**** konec definice třídy, musí následovat ještě definice metod
//**** deklarace proměnných programu
var instanceKvadrRov: TKvadrRovnice; // proměnná pro odkaz na instanci (objekt)
a, b, c: real; // pomocné proměnné pro možnost zadání koeficientů
//**** definice metod ke třídě (instancím třídy) TKvadrRovnice
//** soukromá metoda najdiKoreny, určí z koeficientů a, b, c kořeny rovnice,
// pokud existují, a určí také počet kořenů a vloží ho do atributu
// pocetKorenu dané instance
procedure TKvadrRovnice.najdiKoreny();
// lokální proměnné této metody
var D, odmD: real;
x1, x2: real;
begin
// diskriminant
D := self.b * self.b - 4 * self.a * self.c; // slovo "self" je odkazem
// na instanci, která právě metodu používá
// Není zde nutné, ale doporučuji ho používat.
// určení počtu kořenů a jejich výpočet
if D > 0
then
begin // budou dva kořeny
self.pocetKorenu := 2;
// připravíme si odmocninu z D, ať se nemusí opakovaně počítat
odmD := sqrt(D);
self.x1 := (-self.b - odmD) / (2 * self.a);
self.x2 := (-self.b + odmD) / (2 * self.a);
end // zde ještě nekončí podmíněný příkaz, nepíšeme tedy středník
else if D = 0
then
begin // bude jeden kořen
self.pocetKorenu := 1;
self.x1 := -self.b / (2 * self.a);
end
else self.pocetKorenu := 0; // nebude žádný kořen
end;
//** veřejný konstruktor, který vytvoří instanci (kvadratickou rovnici)
// s přednastavenými hodnotami koeficientů např. a = 1, b = 2, c = 1.
// Tato rovnice by měla mít jediný kořen x = -1 a může posloužit jako
// rychlý test programu.
// Konstruktor také hned rovnici vyřeší - určí počet kořenů a kořeny
// privátní metodou najdiKoreny().
constructor TKvadrRovnice.vytvorRovnici();
begin
self.a := 1;
self.b := 2;
self.c := 1;
self.najdiKoreny;
end;
//** veřejná procedura zadejKoeficienty() pro možnost zadání koeficientů
// instanci (kvadrat. rovnici, kterou chceme vyřešit)
// Po zadání koeficientů se rovnice ihned vyřeší - určí počet kořenů a kořeny
// privátní metodou najdiKoreny(), stejně jako u konstruktoru.
procedure TKvadrRovnice.zadejKoeficienty(a, b, c: real);
begin
self.a := a;
self.b := b;
self.c := c;
self.najdiKoreny;
end;
//** 3 veřejné funkce vratKoefA, B, C pro výpis koeficientů rovnice a, b, c
// Pouze hodnotu koeficientů vrátí do programu příkazem result.
function TKvadrRovnice.vratKoefA(): real;
begin
result := self.a;
end;
function TKvadrRovnice.vratKoefB(): real;
begin
result := self.b;
end;
function TKvadrRovnice.vratKoefC(): real;
begin
result := self.c;
end;
//** veřejná funkce vratPocetKorenu() pro výpis počtu kořenů rovnice
// Pouze hodnotu počtu kořenů vrátí do programu příkazem result.
function TKvadrRovnice.vratPocetKorenu(): integer;
begin
result := self.pocetKorenu;
end;
//** veřejná funkce vratKorenX1() pro výpis prvního kořene rovnice
// Pouze hodnotu kořene vrátí do programu příkazem result.
function TKvadrRovnice.vratKorenX1(): real;
begin
result := self.x1;
end;
//** veřejná funkce vratKorenX2() pro výpis druhého kořene rovnice
// Pouze hodnotu kořene vrátí do programu příkazem result.
function TKvadrRovnice.vratKorenX2(): real;
begin
result := self.x2;
end;
//**** vlastní program
begin
writeLn('Program na reseni kvadraticke rovnice, zkouska prace s objekty.');
//** První rovnice
// vytvoření odkazu na novou instanci - objekt - kvadratické rovnice:
instanceKvadrRov := TKvadrRovnice.vytvorRovnici;
// kontrolní výpis koeficientů zadaných naším základním konstruktorem
write('Koeficienty první rovnice jsou a = ', instanceKvadrRov.vratKoefA:3:3);
write(', b = ', instanceKvadrRov.vratKoefB:3:3);
writeLn(', c = ', instanceKvadrRov.vratKoefC:3:3);
// řešení rovnice už proběhlo během volání konstruktoru
// dle hodnoty atributu PocetKorenu se vypíší kořeny
if instanceKvadrRov.vratPocetKorenu = 2
then
begin
write('Rovnice ma dva koreny x1 = ', instanceKvadrRov.vratKorenX1:3:3);
writeLn(', x2 = ', instanceKvadrRov.vratKorenX2:3:3);
end
else
if instanceKvadrRov.vratPocetKorenu = 1
then
writeLn('Rovnice ma jeden koren x = ', instanceKvadrRov.vratKorenX1:3:3)
else
writeLn('Rovnice nema zadny koren');
//** Druhá rovnice
// zadání a řešení nové rovnice
writeLn; // vynechání řádku ve výpisu
writeLn('Zadejte koeficienty a, b, c nove rovnice:');
write('a = '); readLn(a);
write('b = '); readLn(b);
write('c = '); readLn(c);
instanceKvadrRov.zadejKoeficienty(a, b, c);
// řešení rovnice už proběhlo během volání metody zadejKoeficienty(a, b, c)
// dle hodnoty atributu PocetKorenu se vypíší kořeny
if instanceKvadrRov.vratPocetKorenu = 2
then
begin
write('Rovnice ma dva koreny x1 = ', instanceKvadrRov.vratKorenX1:3:3);
writeLn(', x2 = ', instanceKvadrRov.vratKorenX2:3:3);
end
else
if instanceKvadrRov.vratPocetKorenu = 1
then
writeLn('Rovnice ma jeden koren x = ', instanceKvadrRov.vratKorenX1:3:3)
else
writeLn('Rovnice nema zadny koren');
readLn;
end.
2. řešení - s využitím knihovny
O co se jedná? K hlavnímu programu můžeme přiložit knihovnu (neboli jednotku, unit), která bude obsahovat samostatnější část kódu - v našem případě definici třídy a jejích metod.
Jaké jsou výhody použití jednotky?
- Zpřehlední se zdrojový kód programu. Je snazší prohlédnout si kratší kód vlastního programu a kratší kód jednotky (v praxi spíše mnoha jednotek) než vše v jednom obrovském souboru.
- Snazší práce na programu. Je snazší upravovat kratší soubory než jeden obrovský a práce se dá snáze rozdělit mezi víc spolupracovníků.
- Znovupoužitelnost kódu. To je asi hlavní výhoda použití knihoven. Jakmile jednu užitečnou knihovnu napíšete, můžete ji později snadno přiložit k libovolnému jinému programu. To je zásadní výhoda!
Protože tento díl se vinou zdrojového kódu hodně prodloužil, nechám 2. řešení s využitím knihovny na příště. Můžete to ale zkusit promyslet dopředu: jednotku vytvoříme v Delphi volbou "New > File > Unit", přesuneme do ní z hlavního programu definici třídy a jejích metod. Jinak se celkem nic nezmění. Pokud vytváříme novou jednotku při otevřeném hlavním programu, vloží nám Delphi dokonce na tuto novou jednotku do hlavního programu odkaz (v části "uses").
Odpovědi na otázky z tohoto dílu
Otázka: Zněla takto - promyslete, jaké by mohlo být přístupové právo proměnné pocetKorenu: private nebo public?
Protože počet kořenů závisí na zadání kvadratické rovnice - koeficientech a, b, c - jistě by nikdo neměl mít možnost hodnotu počtu kořenů libovolně nastavit (dokonce třeba na nesmyslné číslo 3 nebo 1000!). Proto atributu pocetKorenu přiřadíme přístupové právo private.
Nakonec obecně platí, že všechny atributy mívají přístupové právo private!
Naše instance by ale měly mít veřejnou (public) metodu, která počet kořenů sdělí někomu, kdo se na to zeptá (programátorovi nebo jiné instanci nějaké třídy). Tu máme v naší třídě pod názvem vratPocetKorenu().
Přeji vám vše dobré, ať se vám dobře programuje. A pro volné chvíle si neodpustím doporučit vám poslech smutných, ale velmi potřebných rozhlasových dokumentů z poslední doby o procesu s Miladou Horákovou na http://www.rozhlas.cz/leonardo/proces. Věděli jste o nich? Velkou reklamu myslím neměly, ale zaslouží si ji.