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