Wykład dwudziesty pierwszy: Dziedziczenie i kompozycja

Transkrypt

Wykład dwudziesty pierwszy: Dziedziczenie i kompozycja
Podstawy Programowania – semestr drugi
Wykład dziewiętnasty
1.
Kompozycja i dziedziczenie
Kompozycja i dziedziczenie są dwiema technikami z zakresu programowania obiektowego pozwalającymi na wielokrotne wykorzystanie kodu. Dzięki tym technikom
możemy tworzyć nowe klasy na podstawie już istniejących. Zanim zostaną one opisane należy przyjąć jakąś notację, która ułatwiłaby wyrażenie związków między klasami zdefiniowanymi w programie. Taką notacją, niezależną od używanego języka programowania jest język UML (ang. Unified Modeling Language). Wbrew nazwie
nie jest to język programowania, ale graficzny sposób na przedstawienie zależności między klasami. Język ten jest stosunkowo skomplikowany. W dalszej części wykładu będziemy posługiwać się jego wersją okrojoną, niekoniecznie zgodną ze standardem. Pojedyncza klasa jest w tym języku reprezentowana przez prostokąt zawierający
w górnej części jej nazwę, a pozostała jego część jest wypełniona nazwami metod publicznych stanowiących tzw. interfejs klasy. Mogą tam również zostać wymienione
nazwy pól klasy, które są zadeklarowane jako publiczne. Pozostałe pola i metody o dostępie prywatnym nie są częścią interfejsu klasy lecz stanowią jej implementację i
nie są wymieniane w diagramie klasy. Oto przykład takiego diagramu dla klasy „Czlowiek” z programu zaprezentowanego na poprzednim wykładzie:
Czlowiek
podajImie
ustawImie
podajNazwisko
ustawNazwisko
podajWiek
ustawWiek
Jeśli interesują nas tylko zależności między klasami, a nie ich interfejsy to możemy reprezentować klasę za pomocą zwykłego prostokąta z wymienioną nazwą tej klasy.
Opis technik wielokrotnego wykorzystania kodu zaczniemy od kompozycji. Kompozycję stosujemy wówczas, gdy dojdziemy do wniosku, że obiekt, którego klasę chcemy
zdefiniować w naszym programie składa się lub jest właścicielem innych obiektów. W takim wypadku tworzymy taki obiekt jako prywatny atrybut (pole) naszej klasy.
Oto przykład takiego rozwiązania:
1 program Swiatlo;
2 uses Osw;
3 var
4
os:Oswietlenie;
5 begin
6 os.inicjuj;
7 os.pokaz;
8 os.zaswiec;
9 os.pokaz;
10 os.zgas;
11 os.pokaz;
12 end.
1 unit Osw;
2 interface
W pierwszej ramce został przedstawiony kod źródłowy programu, który tworzy tylko jeden obiekt klasy
„Oswietlenie” (Oświetlenie) i wysyła do niego komunikaty w bloku głównym. Klasa „Oswietlenie”
zdefiniowana jest w module „Osw”. Posiada ono pole będące tablicą obiektów klasy „Zarowka” (Żarówka).
Zarówno ta tablica jak i metoda „przelacznik” (przełącznik) są prywatnymi składowymi klasy „Oswietlenie”.
Wspomniana metoda stanowi część mechanizmu działania obiektów tej klasy i nie powinna być bezpośrednio używana. Pozostałe metody są metodami publicznymi, stanowiącymi interfejs tej klasy. Warto zwrócić
uwagę na metodę „inicjuj”. Ta metoda zajmuje się inicjalizacją tablicy obiektów „Zarowka”, czyli ustawia te
obiekty w bezpieczny stan początkowy (pamiętajmy, że duża część błędów w programie spowodowana jest
nieodpowiednią inicjalizacją zmiennych lub jej całkowitym brakiem). Tego typu metody będą odgrywały
ogromną rolę podczas tworzenie obiektów w sposób dynamiczny i będziemy je nazywać konstruktorami. Z
metody „przelacznik” korzysta metoda „zaswiec” (zaświeć), która jest publiczna. Działanie metody
„przelacznik” polega na symulowaniu losowych uszkodzeń żarówek w oświetleniu. Analizując kod metod
można dojść do wniosku, że wywołują one metody publiczne poszczególnych elementów tablicy, ale nie pozwalają skorzystać z nich bezpośrednio. Innymi słowy ktoś lub coś korzystające z obiektu klasy
„Oswietlenie” nie ma możliwości skorzystania z interfejsu jego obiektów składowych, które są klasy
„Zarowka”. Powyższy wniosek można zapisać jako jeszcze jedną przesłankę kiedy stosować kompozycję: „Jeśli potrzebny jest nam obiekt innej klasy, jako składowa definiowanej przez nas klasy, ale nie chcemy udostępniać interfejsu tego obiektu na zewnątrz naszej klasy, to wtedy stosujemy kompozycję”. Ta ostatnia
reguła nie zawsze obowiązuje. Możemy bowiem na etapie analizy bądź projektowania dojść do wniosku, że
bardziej naturalne będzie upublicznienie pól klasy i pozwolenie na bezpośrednie wywoływanie ich metod, niż czynienie ich prywatnymi i
ukrywanie ich interfejsu. Oto diagram ilustrujący kompozycję:
3 uses Zarowki,Crt;
4 const On=True;
5
Off=False;
Oswietlenie
6 type Oswietlenie = object
7
8
private
iluminacja:array[1..6] of Zarowka;
9
procedure przelacznik(var z:Zarowka; stan:boolean);
10
public
11
procedure zaswiec;
1
Zarowka
Podstawy Programowania – semestr drugi
12
procedure zgas;
13
procedure inicjuj;
14
procedure pokaz;
Celem uproszczenia rysowania takiego diagramu często
pomija się oznaczenie znajdujące się na początku linii
łączącej dwa diagramy klas. Takie połączenie jak wyżej
informuje, że obiekty klasy „Oswietlenie” składają się z
jednego lub kilku obiektów klasy „Zarowka”. Klasa
„Zarowka” zdefiniowana jest w module „Zarowki” (Żarówki). Posiada ona dwa pola prywatne opisujące stan obiektu
(„wlaczona” (włączona) i „uszkodzona”) oraz dwie metody
prywatne („ustawUszkodzenie”, „drukujUszkodzenie”),
które wpływają na stan jednego z pól obiektu
(„Uszkodzona”) i które nie są wywoływane bezpośrednio.
Pozostałe metody są metodami publicznymi. Na koniec
opisu kompozycji warto wspomnieć, że obiekty będące
składowymi innych obiektów są czasem nazywane obiektami zagnieżdżonymi.
15 end;
16
17 implementation
18
19
procedure Oswietlenie.inicjuj;
20
var
21
22
i:byte;
begin
23
randomize;
24
for i:=low(iluminacja) to high(iluminacja) do iluminacja[i].inicjuj;
25
end;
26
27
procedure Oswietlenie.pokaz;
28
var
29
30
i:byte;
begin
31
clrscr;
32
for i:=low(iluminacja) to high(iluminacja) do
33
begin
34
writeln(i,':');
35
iluminacja[i].drukujStatus;
36
writeln;
37
end;
38
readln;
39
end;
40
41
procedure Oswietlenie.przelacznik(var z:Zarowka; stan:boolean);
42
begin
43
44
if stan=On then z.wlacz else z.wylacz;
end;
45
46
procedure Oswietlenie.zaswiec;
47
var
48
49
50
51
i:byte;
begin
for i:=low(iluminacja) to high(iluminacja) do przelacznik(iluminacja[i],On);
end;
52
53
procedure Oswietlenie.zgas;
54
var
55
56
i:byte;
begin
2
Podstawy Programowania – semestr drugi
57
58
for i:=low(iluminacja) to high(iluminacja) do przelacznik(iluminacja[i],Off);
end;
59 end.
1 unit Zarowki;
2 interface
3
4 type
5 Zarowka = object
6
private
7
uszkodzona:boolean;
8
wlaczona:boolean;
9
procedure ustawUszkodzenie;
10
function podajUszkodzenie:boolean;
11 public
12
procedure wlacz;
13
procedure wylacz;
14
procedure inicjuj;
15
procedure drukujStatus;
16 end;
17
18 implementation
19
20
procedure Zarowka.ustawUszkodzenie;
21
begin
22
23
if random(2)=0 then uszkodzona:=false else uszkodzona := true;
end;
24
25
function Zarowka.podajUszkodzenie:boolean;
26
begin
27
28
podajUszkodzenie:=uszkodzona;
end;
29
30
procedure Zarowka.wylacz;
31
begin
32
33
wlaczona:=false;
end;
34
35
procedure Zarowka.wlacz;
36
begin
37
38
if podajUszkodzenie=false then wlaczona:=true;
end;
39
40
procedure Zarowka.inicjuj;
41
begin
42
ustawUszkodzenie;
3
Podstawy Programowania – semestr drugi
43
44
wylacz;
end;
45
46
procedure Zarowka.drukujStatus;
47
begin
48
if podajUszkodzenie=true then writeln('Uszkodzona: tak')
49
else writeln('Uszkodzona: nie');
50
51
if Wlaczona=true then writeln('Włączona') else writeln('Wyłączona');
end;
52 end.
Dziedziczenie stosujemy zawsze, kiedy dochodzimy do
1
wniosku na etapie analizy lub projektowania, że jakiś obiekt jest podobny do innych obiektów lub wprost jest obiektem innego typu . Wówczas czynimy klasę takiego
obiektu klasą potomną lub dziedziczącą lub pochodną natomiast klasę po której ona dziedziczy nazywamy klasą podstawową lub nadrzędną lub dziedziczoną lub
bazową. Dziedziczeniu podlegają zarówno pola jak i metody danej klasy. Zazwyczaj klasy nadrzędne są
klasami ogólnymi, natomiast klasy potomne są klasami specjalizowanymi. Na diagramie języka UML
przedstawiamy dziedziczenie w następujący sposób:
Kierunek strzałki wskazuje co po czym dziedziczy. Diagram przedstawiony obok jest najprostszym z możliKlasa Bazowa
wych. W bardziej skomplikowanych diagramach po klasie bazowej może dziedziczyć większa ilość klas.
Niektóre obiektowe języki programowania (takie jak C++) umożliwiają dziedziczenie klasie pochodnej po
kilku klasach równocześnie. W Object Pascalu takie praktyki są niemożliwe (ale można podobną technikę
symulować). Rozbudowane diagramy dziedziczenia tworzą hierarchię dziedziczenia lub drzewo dziedziczenia. Rozważmy program, w którym klasa „Kot” będzie dziedziczyła pola i metody po klasie „Zwierze”
(Zwierzę). Poniżej umieszczono kod źródłowy programu głównego i dwóch modułów. Na wstępie przyjrzyjmy się modułowi o nazwie „zwi”. Zawiera on definicję klasy „Zwierze”. Ta klasa jest klasą bazową po której
będą dziedziczyć inne klasy. Posiada ona trzy pola i osiem metod, które mogą wpływać na stan tych pól lub
ten stan odczytywać. Na szczególną uwagę zasługuje metoda „inicjuj”, która dokonuje inicjalizacji, czyli
ustawia wartości początkowe wspomnianych pól. Klasa „Kot” jest zdefiniowana w module o nazwie „zwi2”
Klasa Pochodna
i dziedziczy po klasie „Zwierze”. Składnia dziedziczenia w Obejct Pascalu jest następująca:
NazwaKlasyPochodnej = object(NazwaKlasyBazowej)
1 program ObiektoweZwierze;
Klasa „Kot” dziedziczy po klasie „Zwierze”
wszystkie jej metody i pola, ale również posiada
własne pole i własne metody obsługujące to pole.
Należy zwrócić uwagę, że w definicji klasy
umieszczono także niektóre metody z klasy
„Zwierze”. Stało się tak, gdyż te metody będą
zakrywane lub nadpisywane. Oznacza to, że
zostanie zmienione ich działanie. W metodzie
„inicjuj” klasy „Kot” użyto słowa kluczowego
inherited. Oznacza ono, że ta metoda wywołuje
2 uses zwi,zwi2;
3 var
4 zwierz:Zwierze;
5 kotDomowy:Kot;
6 begin
7 zwierz.inicjuj;
metodę o tej samej nazwie z klasy znajdującej się
w hierarchii dziedziczenia bezpośrednio nad nią.
Ma to na celu zainicjalizowanie pól odziedziczonych po klasie „Zwierze”. To samo słowo jest wykorzystywane w innych metodach. Jeśli chcemy
całkowicie zmienić działanie metody, wówczas
nie używamy tego słowa. Możliwe jest też
dziedziczenie z przeskokiem. Wykorzystujemy je
wtedy, kiedy w drzewie dziedziczenia mamy
więcej klas i chcemy dziedziczyć metodę nie od
klasy bezpośrednio znajdującej się nad naszą klasą, ale z klasy wyższej. Wówczas stosujemy zapis:
8 zwierz.drukuj;
9 readln;
10 zwierz.ustawNazwe('Zwierzak');
11 zwierz.ustawWielkosc('srednia');
12 zwierz.ustawWiek(16);
13 zwierz.drukuj;
14 readln;
15 kotDomowy.inicjuj;
NazwaKlasy.nazwaMetody;
16 kotDomowy.drukuj;
Nadpisując metodę w klasie pochodnej możemy
zmienić sposób jej implementacji (jeśli w klasie
bazowej była funkcją, to w pochodnej może być
procedurą lub odwrotnie) lub zmienić jej listę
argumentów formalnych. W ciele tej metody
możemy wywołać jej wersję pochodzącą z którejś
z klas bazowych (za pomocą słowa kluczowego
„inherited” lub za pomocą dziedziczenia z
przeskokiem).
Jeśli
w
klasie
pochodnej
zmieniliśmy w opisany wyżej sposób metodę, to
17 readln;
18 kotDomowy.ustawNazwe('Mruczek');
19 kotDomowy.ustawWielkosc('mala');
20 kotDomowy.ustawWiek(20);
21 kotDomowy.drukuj;
1
Przez typ rozumiemy tu, że ma on ten sam interfejs, czyli metody i pola publiczne, co tamte obiekty, inaczej – że można do niego wysyłać takie same komunikaty.
4
Podstawy Programowania – semestr drugi
tylko ta wersja metody będzie dostępna w tej
klasie. Istnieją języki programowania, które
umożliwiają dostęp do obu wersji metody, a
nawet pozwalają na posiadanie dwóch metod o
takich samych nazwach, ale różnych listach
argumentów, co nazywa się przeciążaniem
22 readln;
23 Zwierz:=KotDomowy;
24 Zwierz.drukuj;
25 readln;
metody. Niestety w Object Pascalu ten
mechanizm działa wyłącznie w opisany wyżej
sposób. Pozostałe metody, które nie zostały
nadpisane w klasie „Kot” są dziedziczone w
całości z klasy „Zwierze” i działają w ten sam
sposób. W programie głównym stworzono dwa
obiekty. Jeden klasy „Zwierze” drugi klasy „Kot”.
W obu przypadkach zainicjowano obiekty przy
pomocy metod „init” i wypisano ich zawartość
przy pomocy metody „drukuj”, a następnie
wypełniono pola tych obiektów przy pomocy
odpowiednich metod i ponownie wypisano ich
zawartość przy pomocy metody „drukuj”.
Ciekawym elementem tego programu są wiersze
23 i 24. W pierwszym z nich przypisujemy
obiektowi klasy „Zwierze” dane z obiektu klasy
„Kot”. Takie przypisanie intuicyjnie budzi
sprzeciw, dopóki nie uświado mimy sobie, że
obiekt klasy „Kot” jest obiektem klasy „Zwierze”.
Okaże się również, że metoda „drukuj” wywołana
w wierszu 24 nie wydrukuje zawartości pola
„Poluje”. Nie ma w tym również nic dziwnego. Ta
metoda pochodzi z klasy „Zwierze” i „nie potrafi”
tego pola wydrukować. Dodatkowo w instrukcji
przypisania
w
wierszu
23
zawartość
wspomnianego pola też została pominięta, gdyż
w obiekcie klasy „Zwierze” nie ma takiego pola.
Stosując takie przypisania należy pamiętać, że
tylko obiektowi klasy bazowej można przypisać
dane klasy pochodnej, nigdy odwrotnie. Kończąc
rozważania na temat dziedziczenia należy dodać,
że stosujemy je wtedy, gdy jest nam w nowej
klasie potrzebny interfejs innej klasy.
26 end.
1 unit zwi;
2 interface
3 uses crt;
4 type lancuch = string[10];
5 Zwierze = object
6
7
private
8
nazwa,wielkosc:lancuch;
9
wiek:byte;
10
11
public
12
procedure ustawWiek(age:byte);
13
function podajWiek:byte;
14
procedure ustawWielkosc(const size:lancuch);
15
function podajWielkosc:lancuch;
16
procedure ustawNazwe(const name:lancuch);
17
function podajNazwe:lancuch;
18
procedure inicjuj;
19
procedure drukuj;
20 end;
21
22 implementation
23
24
procedure Zwierze.ustawWiek(age:byte);
25
begin
26
27
wiek:=age;
end;
28
29
procedure Zwierze.ustawWielkosc(const size:lancuch);
30
begin
31
32
Wielkosc:=size;
end;
33
34
procedure Zwierze.ustawNazwe(const name:lancuch);
35
begin
36
37
nazwa:=name;
end;
38
39
procedure Zwierze.inicjuj;
40
begin
5
Podstawy Programowania – semestr drugi
41
ustawNazwe('nieznana');
42
ustawWiek(0);
43
ustawWielkosc('nieznana');
44
ustawNazwe('zwierzę');
45
end;
46
47
function Zwierze.podajWiek:byte;
48
begin
49
podajWiek:=wiek;
50
end;
51
52
function Zwierze.podajWielkosc:lancuch;
53
begin
54
podajWielkosc:=wielkosc;
55
end;
56
57
function Zwierze.podajNazwe:lancuch;
58
begin
59
podajNazwe:=nazwa;
60
end;
61
62
procedure Zwierze.drukuj;
63
begin
64
clrscr;
65
writeln('Nazwa: ',podajNazwe);
66
writeln('Wielkość: ',podajWielkosc);
67
writeln('Wiek:',podajWiek:3);
68
end;
69 end.
1 unit zwi2;
2 interface
3 uses zwi;
4 type
5
6
7
8
Kot = object(Zwierze)
private
poluje:boolean;
public
9
procedure ustawPoluje(hunts:boolean);
10
function podajPoluje:boolean;
11
procedure ustawWiek(age:byte);
12
procedure drukuj;
13
14
procedure inicjuj;
end;
15
6
Podstawy Programowania – semestr drugi
16 implementation
17
18
procedure Kot.inicjuj;
19
begin
20
inherited inicjuj;
21
poluje:=true;
22
end;
23
24
procedure Kot.drukuj;
25
begin
26
27
28
inherited drukuj;
if podajPoluje = true then writeln('Poluje: tak') else writeln('Poluje: nie');
end;
29
30
procedure Kot.ustawPoluje(hunts:boolean);
31
begin
32
33
poluje:=hunts;
end;
34
35
function Kot.podajPoluje:boolean;
36
begin
37
38
podajPoluje:=poluje;
end;
39
40
procedure Kot.ustawWiek(age:byte);
41
begin
42
randomize;
43
if age > 15 then inherited ustawWiek(random(15)+1)
44
else inherited ustawWiek(age);
45
end;
46 end.
2.
Podsumowanie
Dziedziczenie i kompozycja są bardzo elastycznymi i wydajnymi technikami powtórnego wykorzystania kodu. W praktyce częściej stosuje się kompozycję, niż dziedziczenie, natomiast w większych programach prawie zawsze obie te techniki są stosowane równocześnie.
7