Podstawy Programowania – semestr drugi Wyk ad trzynasty ł 1

Transkrypt

Podstawy Programowania – semestr drugi Wyk ad trzynasty ł 1
Podstawy Programowania – semestr drugi
Wykład trzynasty
1.
Kompozycja i dziedziczenie
Kompozycja i dziedziczenie są dwiema technikami z zakresu programowania obiektowego pozwalającymi na wielokrotne wykorzystanie kodu. Dzięki nim 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 w tym języku może być reprezentowana przez prostokąt
zawierający w górnej części nazwę klasy, 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 stanowią implementację klasy i nie są z reguły
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ć pojedynczą klasę za pomocą zwykłego prostokąta z jej nazwą.
Opis technik wielokrotnego wykorzystania kodu zaczniemy od kompozycji. Kompozycję stosujemy wówczas, gdy dojdziemy do wniosku, że obiekt, którego klasę chcemy
1
zdefiniować w naszym programie składa się lub jest właścicielem innych obiektów . W takim wypadku tworzymy takie obiekty jako prywatne atrybuty (pola) tej 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 ona 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 programowaniu 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 włączaniu lub wyłączaniu żarówek w oświetleniu, w zależności od argumentu jej
wywołania. 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 potrzebujemy obiektu innej klasy, jako składowej 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 publicznych, 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
1
private
8
iluminacja:array[1..6] of Zarowka;
9
procedure przelacznik(var z:Zarowka; stan:boolean);
10
public
11
Zarowka
W przypadku agregacji romb przy klasie „Oswietlenie” nie byłby
wypełniony.
procedure zaswiec;
Prawidłowo powinniśmy rozróżniać kompozycję i agregację. Różnica polega na tym, że w kompozycji obiekty składowe nie mogą istnieć samodzielnie, w agregacji mogą.
1
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 dotyczą stanu 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 then writeln('Włączona') else writeln('Wyłączona');
end;
52 end.
Dziedziczenie stosujemy zawsze, kiedy dochodzimy do wniosku na etapie analizy lub projektowania, że jakiś obiekt jest podobny do innych obiektów lub wprost ma
2
taki sam typ jak te inne obiekty . Wówczas czynimy klasę takiego obiektu klasą potomną (zwaną również 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:
Klasa Bazowa
Klasa Pochodna
Kierunek strzałki wskazuje co po czym dziedziczy. Diagram przedstawiony obok jest najprostszym z możliwych. W bardziej skomplikowanych diagramach po klasie bazowej może dziedziczyć większa liczba 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”
i dziedziczy po klasie „Zwierze”. Składnia dziedziczenia w Object Pascalu jest następująca:
NazwaKlasyPochodnej = object(NazwaKlasyBazowej)
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 (ang. overrided).
1 program ObiektoweZwierze;
2 uses zwi,zwi2;
3 var
4 zwierz:Zwierze;
Oznacza to, że zostanie zmienione ich działanie.
W metodzie „inicjuj” klasy „Kot” użyto słowa
kluczowego inherited. Oznacza ono, że ta metoda
5 kotDomowy:Kot;
6 begin
wywołuje metodę o tej samej nazwie z klasy
znajdującej się w hierarchii dziedziczenia
bezpośrednio nad nią (bazowej). 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:
7 zwierz.inicjuj;
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.
Jeśli
w klasie
pochodnej zmieniliśmy w opisany wyżej sposób
metodę, to 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
17 readln;
18 kotDomowy.ustawNazwe('Mruczek');
19 kotDomowy.ustawWielkosc('mala');
20 kotDomowy.ustawWiek(20);
21 kotDomowy.drukuj;
2
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ć te same komunikaty.
4
Podstawy Programowania – semestr drugi
w jednej klasie dwóch metod o takich samych
nazwach, ale różnych listach argumentów, co
nazywa się przeciążaniem metody (ang.
22 readln;
23 zwierz:=kotDomowy;
overloaded). Niestety w Object Pascalu ten
24 zwierz.drukuj;
mechanizm nie działa w opisany 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 fragmentem
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świadomimy 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ć obiekt klasy pochodnej, nigdy
odwrotnie. W przypadku opisywanego przykładu
oznacza to, że obiektu klasy „Zwierze” nie można
przypisać do obiektu klasy „Kot”. Kończąc
rozważania na temat dziedziczenia należy
podkreślić, że stosujemy je wtedy, gdy jest nam
w nowej klasie potrzebny interfejs innej klasy.
25 readln;
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
16 implementation
6
Podstawy Programowania – semestr drugi
17
18
procedure Kot.inicjuj;
19
begin
20
inherited inicjuj;
21
poluje:=true;
22
end;
23
24
procedure Kot.drukuj;
25
begin
26
inherited drukuj;
27
if podajPoluje = true then writeln('Poluje: tak') else writeln('Poluje: nie');
28
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
45
else inherited ustawWiek(age);
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