Pierwsze wydanie książki "C++. 50 efektywnych sposobów na udoskonalenie twoich programów" zostało sprzedane w nakładzie 100 000 egzemplarzy i zostało przetłumaczone na cztery języki. Nietrudno zrozumieć, dlaczego tak się stało. Scott Meyers w charakterystyczny dla siebie, praktyczny sposób przedstawił wiedzę typową dla ekspertów -- czynności, które niemal zawsze wykonują lub czynności, których niemal zawsze unikają, by tworzyć prosty, poprawny i efektywny kod. Każda z zawartych w tej książce pięćdziesięciu wskazówek jest streszczeniem metod pisania lepszych programów w C++, zaś odpowiednie rozważania są poparte konkretnymi przykładami. Z myślą o nowym wydaniu, autor opracował od początku wszystkie opisywane w tej książce wskazówki. Wynik jego pracy jest wyjątkowo zgodny z międzynarodowym standardem C++, technologią aktualnych kompilatorów oraz najnowszymi trendami w świecie rzeczywistych aplikacji C++.
Do najważniejszych zalet książki "C++. 50 efektywnych sposobów na udoskonalenie twoich programów" należą:
* Eksperckie porady dotyczące projektowania zorientowanego obiektowo, projektowania klas i właściwego stosowania technik dziedziczenia
* Analiza standardowej biblioteki C++, włącznie z wpływem standardowej biblioteki szablonów oraz klas podobnych do string i vector na strukturę dobrze napisanych programów
* Rozważania na temat najnowszych możliwości języka C++: inicjalizacji stałych wewnątrz klas, przestrzeni nazw oraz szablonów składowych
* Wiedza będąca zwykle w posiadaniu wyłącznie doświadczonych programistów
Książka "C++. 50 efektywnych sposobów na udoskonalenie twoich programów" pozostaje jedną z najważniejszych publikacji dla każdego programisty pracującego z C++.
C++. 50 efektywnych sposobów na udoskonalenie Twoich programów
1. IDZ DO
PRZYK£ADOWY ROZDZIA£
C++. 50 efektywnych
SPIS TRE CI sposobów na udoskonalenie
KATALOG KSI¥¯EK
Twoich programów
Autor: Scott Meyers
KATALOG ONLINE T³umaczenie: Miko³aj Szczepaniak
ISBN: 83-7361-345-5
ZAMÓW DRUKOWANY KATALOG Tytu³ orygina³u: Effective C++: 50 Specific Ways
to Improve Your Programs and Design
Format: B5, stron: 248
TWÓJ KOSZYK
Pierwsze wydanie ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie twoich
DODAJ DO KOSZYKA programów” zosta³o sprzedane w nak³adzie 100 000 egzemplarzy i zosta³o
przet³umaczone na cztery jêzyki. Nietrudno zrozumieæ, dlaczego tak siê sta³o.
Scott Meyers w charakterystyczny dla siebie, praktyczny sposób przedstawi³ wiedzê
CENNIK I INFORMACJE typow¹ dla ekspertów — czynno ci, które niemal zawsze wykonuj¹ lub czynno ci,
których niemal zawsze unikaj¹, by tworzyæ prosty, poprawny i efektywny kod.
Ka¿da z zawartych w tej ksi¹¿ce piêædziesiêciu wskazówek jest streszczeniem metod
ZAMÓW INFORMACJE
O NOWO CIACH pisania lepszych programów w C++, za odpowiednie rozwa¿ania s¹ poparte
konkretnymi przyk³adami. Z my l¹ o nowym wydaniu, Scott Meyers opracowa³
ZAMÓW CENNIK od pocz¹tku wszystkie opisywane w tej ksi¹¿ce wskazówki. Wynik jego pracy jest
wyj¹tkowo zgodny z miêdzynarodowym standardem C++, technologi¹ aktualnych
kompilatorów oraz najnowszymi trendami w wiecie rzeczywistych aplikacji C++.
CZYTELNIA Do najwa¿niejszych zalet ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie
twoich programów” nale¿¹:
FRAGMENTY KSI¥¯EK ONLINE • Eksperckie porady dotycz¹ce projektowania zorientowanego obiektowo,
projektowania klas i w³a ciwego stosowania technik dziedziczenia
• Analiza standardowej biblioteki C++, w³¹cznie z wp³ywem standardowej biblioteki
szablonów oraz klas podobnych do string i vector na strukturê dobrze napisanych
programów
• Rozwa¿ania na temat najnowszych mo¿liwo ci jêzyka C++: inicjalizacji sta³ych
wewn¹trz klas, przestrzeni nazw oraz szablonów sk³adowych
• Wiedza bêd¹ca zwykle w posiadaniu wy³¹cznie do wiadczonych programistów
Ksi¹¿ka „C++. 50 efektywnych sposobów na udoskonalenie twoich programów”
pozostaje jedn¹ z najwa¿niejszych publikacji dla ka¿dego programisty pracuj¹cego z C++.
Scott Meyers jest znanym autorytetem w dziedzinie programowania w jêzyku C++;
Wydawnictwo Helion zapewnia us³ugi doradcze dla klientów na ca³ym wiecie i jest cz³onkiem rady
ul. Chopina 6 redakcyjnej pisma C++ Report. Regularnie przemawia na technicznych konferencjach na
44-100 Gliwice ca³ym wiecie, jest tak¿e autorem ksi¹¿ek „More Effective C++” oraz „Effective C++ CD”.
tel. (32)230-98-63 W 1993. roku otrzyma³ tytu³ doktora informatyki na Brown University.
e-mail: helion@helion.pl
2. Spis treści
Przedmowa ................................................................................................................... 7
Podziękowania ............................................................................................................ 11
Wstęp......................................................................................................................... 15
Przejście od języka C do C++....................................................................................... 27
Sposób 1. Wybieraj const i inline zamiast #define......................................................................28
Sposób 2. Wybieraj <iostream> zamiast <stdio.h>.....................................................................31
Sposób 3. Wybieraj new i delete zamiast malloc i free...............................................................33
Sposób 4. Stosuj komentarze w stylu C++ ..................................................................................34
Zarządzanie pamięcią .................................................................................................. 37
Sposób 5. U ywaj tych samych form w odpowiadających sobie zastosowaniach
operatorów new i delete ..............................................................................................38
Sposób 6. U ywaj delete w destruktorach dla składowych wskaźnikowych ..............................39
Sposób 7. Przygotuj się do działania w warunkach braku pamięci.............................................40
Sposób 8. Podczas pisania operatorów new i delete trzymaj się istniejącej konwencji ..............48
Sposób 9. Unikaj ukrywania „normalnej” formy operatora new ................................................51
Sposób 10. Jeśli stworzyłeś własny operator new, opracuj tak e własny operator delete ............53
Konstruktory, destruktory i operatory przypisania ......................................................... 61
Sposób 11. Deklaruj konstruktor kopiujący i operator przypisania dla klas
z pamięcią przydzielaną dynamicznie ........................................................................61
Sposób 12. Wykorzystuj konstruktory do inicjalizacji, a nie przypisywania wartości .................64
Sposób 13. Umieszczaj składowe na liście inicjalizacji w kolejności zgodnej
z kolejnością ich deklaracji.........................................................................................69
Sposób 14. Umieszczaj w klasach bazowych wirtualne destruktory ............................................71
Sposób 15. Funkcja operator= powinna zwracać referencję do *this ...........................................76
Sposób 16. Wykorzystuj operator= do przypisywania wartości do wszystkich składowych klasy......79
Sposób 17. Sprawdzaj w operatorze przypisania, czy nie przypisujesz wartości samej sobie......82
Klasy i funkcje — projekt i deklaracja.......................................................................... 87
Sposób 18. Staraj się dą yć do kompletnych i minimalnych interfejsów klas..............................89
Sposób 19. Rozró niaj funkcje składowe klasy, funkcje niebędące składowymi
klasy i funkcje zaprzyjaźnione....................................................................................93
3. 6 Spis treści
Sposób 20. Unikaj deklarowania w interfejsie publicznym składowych reprezentujących dane ........98
Sposób 21. Wykorzystuj stałe wszędzie tam, gdzie jest to mo liwe...........................................100
Sposób 22. Stosuj przekazywanie obiektów przez referencje, a nie przez wartości ...................106
Sposób 23. Nie próbuj zwracać referencji, kiedy musisz zwrócić obiekt ...................................109
Sposób 24. Wybieraj ostro nie pomiędzy przecią aniem funkcji
a domyślnymi wartościami parametrów ...................................................................113
Sposób 25. Unikaj przecią ania funkcji dla wskaźników i typów numerycznych......................117
Sposób 26. Strze się niejednoznaczności...................................................................................120
Sposób 27. Jawnie zabraniaj wykorzystywania niejawnie generowanych funkcji
składowych, których stosowanie jest niezgodne z Twoimi zało eniami..................123
Sposób 28. Dziel globalną przestrzeń nazw ................................................................................124
Implementacja klas i funkcji ...................................................................................... 131
Sposób 29. Unikaj zwracania „uchwytów” do wewnętrznych danych .......................................132
Sposób 30. Unikaj funkcji składowych zwracających zmienne wskaźniki
lub referencje do składowych, które są mniej dostępne od tych funkcji ..................136
Sposób 31. Nigdy nie zwracaj referencji do obiektu lokalnego ani do wskaźnika
zainicjalizowanego za pomocą operatora new wewnątrz tej samej funkcji .............139
Sposób 32. Odkładaj definicje zmiennych tak długo, jak to tylko mo liwe ...............................142
Sposób 33. Rozwa nie stosuj atrybut inline ................................................................................144
Sposób 34. Ograniczaj do minimum zale ności czasu kompilacji między plikami....................150
Dziedziczenie i projektowanie zorientowane obiektowo ............................................... 159
Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” ............................160
Sposób 36. Odró niaj dziedziczenie interfejsu od dziedziczenia implementacji ........................166
Sposób 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych .....................174
Sposób 38. Nigdy nie definiuj ponownie dziedziczonej domyślnej wartości parametru ............176
Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia....................................................178
Sposób 40. Modelując relacje posiadania („ma”) i implementacji z wykorzystaniem,
stosuj podział na warstwy .........................................................................................186
Sposób 41. Rozró niaj dziedziczenie od stosowania szablonów ................................................189
Sposób 42. Dziedziczenie prywatne stosuj ostro nie ..................................................................193
Sposób 43. Dziedziczenie wielobazowe stosuj ostro nie............................................................199
Sposób 44. Mów to, o co czym naprawdę myślisz. Zdawaj sobie sprawę z tego, co mówisz ....213
Rozmaitości .............................................................................................................. 215
Sposób 45. Miej świadomość, które funkcje są niejawnie tworzone i wywoływane przez C++....... 215
Sposób 46. Wykrywanie błędów kompilacji i łączenia jest lepsze
od wykrywania błędów podczas wykonywania programów ....................................219
Sposób 47. Upewnij się, e nielokalne obiekty statyczne są inicjalizowane przed ich u yciem .......222
Sposób 48. Zwracaj uwagę na ostrze enia kompilatorów...........................................................226
Sposób 49. Zapoznaj się ze standardową biblioteką C++ ...........................................................227
Sposób 50. Pracuj bez przerwy nad swoją znajomością C++ .....................................................234
Skorowidz ................................................................................................................. 239
4. Dziedziczenie
i projektowanie
zorientowane obiektowo
Wielu programistów wyra a opinię, e mo liwość dziedziczenia jest jedyną korzyścią
płynącą z programowania zorientowanego obiektowo. Mo na mieć oczywiście ró ne
zdanie na ten temat, jednak liczba zawartych w innych częściach tej ksią ki sposobów
poświęconych efektywnemu programowaniu w C++ pokazuje, e masz do dyspozycji
znacznie więcej rozmaitych narzędzi, ni tylko określanie, które klasy powinny dzie-
dziczyć po innych klasach.
Projektowanie i implementowanie hierarchii klas ró ni się od zasadniczo od wszyst-
kich mechanizmów dostępnych w języku C. Problem dziedziczenia i projektowania
zorientowanego obiektowo z pewnością zmusza do ponownego przemyślenia swojej
strategii konstruowania systemów oprogramowania. Co więcej, język C++ udostępnia
bardzo szeroki asortyment bloków budowania obiektów, włącznie z publicznymi,
chronionymi i prywatnymi klasami bazowymi, wirtualnymi i niewirtualnymi klasami
bazowymi oraz wirtualnymi i niewirtualnymi funkcjami składowymi. Ka da z wy-
mienionych własności mo e wpływać tak e na pozostałe komponenty języka C++.
W efekcie, próby zrozumienia, co poszczególne własności oznaczają, kiedy powinny
być stosowane oraz jak mo na je w najlepszy sposób połączyć z nieobiektowymi czę-
ściami języka C++ mo e niedoświadczonych programistów zniechęcić.
Dalszą komplikacją jest fakt, e ró ne własności języka C++ są z pozoru odpowie-
dzialne za te same zachowania. Oto przykłady:
Potrzebujesz zbioru klas zawierających wiele elementów wspólnych.
Powinieneś wykorzystać mechanizm dziedziczenia i stworzyć klasy
potomne względem jednej wspólnej klasy bazowej czy powinieneś
wykorzystać szablony i wygenerować wszystkie potrzebne klasy
ze wspólnym szkieletem kodu?
5. 160 Dziedziczenie i projektowanie zorientowane obiektowo
Klasa A ma zostać zaimplementowana w oparciu o klasę B. Czy A powinna
zawierać składową reprezentującą obiekt klasy B czy te powinna prywatnie
dziedziczyć po klasie B?
Potrzebujesz projektu bezpiecznej pod względem typu i homogenicznej klasy
pojemnikowej, która nie jest dostępna w standardowej bibliotece C++ (listę
pojemników udostępnianych przez tę bibliotekę podano, prezentując sposób 49.).
Czy lepszym rozwiązaniem będzie skonstruowanie szablonów czy budowa
bezpiecznych pod względem typów interfejsów wokół tej klasy, która sama
byłaby zaimplementowana za pomocą ogólnych (XQKF
6. ) wskaźników?
W sposobach prezentowanych w tej części zawarłem wskazówki, jak nale y znajdo-
wać odpowiedzi na powy sze pytania. Nie mogę jednak liczyć na to, e uda mi się
znaleźć właściwe rozwiązania dla wszystkich aspektów projektowania zorientowane-
go obiektowo. Zamiast tego skoncentrowałem się więc na wyjaśnianiu, co faktycznie
oznaczają poszczególne własności języka C++ i co tak naprawdę sygnalizujesz, sto-
sując poszczególne dyrektywy czy instrukcje. Przykładowo, publiczne dziedziczenie
oznacza relację „jest” lub specjalizacji-generalizacji (ang. isa, patrz sposób 35.) i jeśli
musisz nadać mu jakikolwiek inny sens, mo esz napotkać pewne problemy. Podobnie,
funkcje wirtualne oznaczają, e „interfejs musi być dziedziczony”, natomiast funkcje
niewirtualne oznaczają, e „dziedziczony musi być zarówno interfejs, jak i imple-
mentacja”. Brak rozró nienia tych znaczeń doprowadził ju wielu programistów C++
do trudnych do opisania nieszczęść.
Jeśli rozumiesz znaczenia rozmaitych własności języka C++, odkryjesz, e Twój po-
gląd na projektowanie zorientowane obiektowo powoli ewoluuje. Zamiast przekonywać
Cię o istniejących ró nicach pomiędzy konstrukcjami językowymi, treść poni szych
sposobów ułatwi Ci ocenę jakości opracowanych dotychczas systemów oprogramo-
wania. Będziesz potem w stanie przekształcić swoją wiedzę w swobodne i właściwe
operowanie własnościami języka C++ celem tworzenia jeszcze lepszych programów.
Wartości wiedzy na temat znaczeń i konsekwencji stosowania poszczególnych kon-
strukcji nie da się przecenić. Poni sze sposoby zawierają szczegółową analizę metod
efektywnego stosowania omawianych własności języka C++. W sposobie 44. podsu-
mowałem cechy i znaczenia poszczególnych konstrukcji obiektowych tego języka.
Treść tego sposobu nale y traktować jak zwieńczenie całej części, a tak e zwięzłe
streszczenie, do którego warto zaglądać w przyszłości.
Sposób 35.
Dopilnuj, by publiczne dziedziczenie
modelowało relację „jest”
Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest”
William Dement w swojej ksią ce pt. Some Must Watch While Some Must Sleep
(W. H. Freeman and Company, 1974) opisał swoje doświadczenia z pracy ze studen-
tami, kiedy próbował utrwalić w ich umysłach najistotniejsze tezy swojego wykładu.
7. Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 161
Mówił im, e przyjmuje się, e świadomość historyczna przeciętnego brytyjskiego
dziecka w wieku szkolnym wykracza poza wiedzę, e bitwa pod Hastings odbyła się
w roku 1066. William Dement podkreśla, e jeśli dziecko pamięta więcej szczegółów,
musi tak e pamiętać o tej historycznej dla Brytyjczyków dacie. Na tej podstawie autor
wnioskuje, e w umysłach jego studentów zachowuje się tylko kilka istotnych i naj-
ciekawszych faktów, włącznie z tym, e np. tabletki nasenne powodują bezsenność.
Namawiał studentów, by zapamiętali przynajmniej tych kilka najwa niejszych
faktów, nawet jeśli mają zapomnieć wszystkie pozostałe zagadnienia dyskutowane
podczas wykładów. Autor ksią ki przekonywał do tego swoich studentów wielo-
krotnie w czasie semestru.
Ostatnie pytanie testu w sesji egzaminacyjnej brzmiało: „wymień jeden fakt, który wy-
niosłeś z moich wykładów, i który na pewno zapamiętasz do końca ycia”. Po spraw-
dzeniu egzaminów Dement był zszokowany — niemal wszyscy napisali „1066”.
Jestem teraz pełen obaw, e jedynym istotnym wnioskiem, który wyniesiesz z tej
ksią ki na temat programowania zorientowanego obiektowo w C++ będzie to, e me-
chanizm publicznego dziedziczenia oznacza relację „jest”. Zachowaj jednak ten fakt
w swojej pamięci.
Jeśli piszesz klasę D (od ang. Derived, czyli klasę potomną), która publicznie dziedzi-
czy po klasie B (od ang. Base, czyli klasy bazowej), sygnalizujesz kompilatorom C++
(i przyszłym czytelnikom Twojego kodu), e ka dy obiekt typu D jest tak e obiektem
typu B, ale nie odwrotnie. Sygnalizujesz, e B reprezentuje bardziej ogólne pojęcia
ni D, natomiast D reprezentuje bardziej konkretne pojęcia ni B. Utrzymujesz, e
wszędzie tam, gdzie mo e być u yty obiekt typu B, mo e być tak e wykorzystany
obiekt typu D, poniewa ka dy obiekt typu D jest tak e obiektem typu B. Z drugiej
strony, jeśli potrzebujesz obiektu typu D, obiekt typu B nie będzie mógł go zastąpić
— publiczne dziedziczenie oznacza relację D „jest” B, ale nie odwrotnie.
Taką interpretację publicznego dziedziczenia wymusza język C++. Przeanalizujmy
poni szy przykład:
ENCUU 2GTUQP ] _
ENCUU 5VWFGPV RWDNKE 2GTUQP ] _
Oczywiste jest, e ka dy student jest osobą, nie ka da osoba jest jednak studentem.
Dokładnie takie samo znaczenie ma powy sza hierarchia. Oczekujemy, e wszystkie
istniejące cechy danej osoby (np. to, e ma jakąś datę urodzenia) istnieją tak e dla stu-
denta; nie oczekujemy jednak, e wszystkie dane dotyczące studenta (np. adres szkoły,
do której uczęszcza) będą istotne dla wszystkich ludzi. Pojęcie osoby jest bowiem
bardziej ogólne, ni pojęcie studenta — student jest specyficznym „rodzajem” osoby.
W języku C++ ka da funkcja oczekująca argumentu typu 2GTUQP (lub wskaźnika do
obiektu klasy 2GTUQP bądź referencji do obiektu klasy 2GTUQP) mo e zamiennie pobie-
rać obiekt klasy 5VWFGPV (lub wskaźnik do obiektu klasy 5VWFGPV bądź referencję do
obiektu klasy 5VWFGPV):
XQKF FCPEG
EQPUV 2GTUQP R MC F[ OQ G VC E[è
XQKF UVWF[
EQPUV 5VWFGPV U V[NMQ UVWFGPEK OQIæ UVWFKQYCè
8. 162 Dziedziczenie i projektowanie zorientowane obiektowo
2GTUQP R R TGRTGGPVWLG QUQDú
QDKGMV MNCU[ 2GTUQP
5VWFGPV U U TGRTGGPVWLG UVWFGPVC
QDKGMV MNCU[ 5VWFGPV
FCPEG
R FQDTG R TGRTGGPVWLG QUQDú
FCPEG
U FQDTG U TGRTGGPVWLG UVWFGPVC
C YKúE VCM G QUQDú
UVWF[
U FQDTG
UVWF[
R D æF R PKG TGRTGGPVWLG UVWFGPVC
Powy sze komentarze są prawdziwe tylko dla publicznego dziedziczenia. C++ będzie
się zachowywał w opisany sposób tylko w przypadku, gdy klasa 5VWFGPV będzie publicz-
nie dziedziczyła po klasie 2GTUQP. Dziedziczenie prywatne oznacza coś zupełnie innego
(patrz sposób 42.), natomiast znaczenie dziedziczenia chronionego jest nieznane.
Równowa ność dziedziczenia publicznego i relacji „jest” wydaje się oczywista, w prakty-
ce jednak właściwe modelowanie tej relacji nie jest ju takie proste. Niekiedy nasza
intuicja mo e się okazać zawodna. Przykładowo, faktem jest, e pingwin to ptak;
faktem jest tak e, e ptaki mogą latać. Gdybyśmy w swojej naiwności spróbowali wy-
razić to w C++, nasze wysiłki przyniosłyby efekt podobny do poni szego:
ENCUU $KTF ]
RWDNKE
XKTVWCN XQKF HN[
RVCMK OQIæ NCVCè
_
ENCUU 2GPIWKP RWDNKE $KTF ] RKPIYKP[ Uæ RVCMCOK
_
Mamy teraz problem, poniewa z powy szej hierarchii wynika, e pingwiny mogą
latać, co jest oczywiście nieprawdą. Co stało się z naszą strategią?
W tym przypadku padliśmy ofiarą nieprecyzyjnego języka naturalnego (polskiego).
Kiedy mówimy, e ptaki mogą latać, w rzeczywistości nie mamy na myśli tego, e
wszystkie ptaki potrafią latać, a jedynie, e w ogólności ptaki mają mo liwość latania.
Gdybyśmy byli bardziej precyzyjni, wyrazilibyśmy się inaczej, by podkreślić fakt, e
istnieje wiele gatunków ptaków, które nie latają — otrzymalibyśmy wówczas poni -
szą, znacznie lepiej modelującą rzeczywistość, hierarchię klas:
ENCUU $KTF ]
DTCM FGMNCTCELK HWPMELK HN[
_
ENCUU (N[KPI$KTF RWDNKE $KTF ]
RWDNKE
XKTVWCN XQKF HN[
_
ENCUU 0QP(N[KPI$KTF RWDNKE $KTF ]
DTCM FGMNCTCELK HWPMELK HN[
_
9. Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 163
ENCUU 2GPIWKP RWDNKE 0QP(N[KPI$KTF ]
DTCM FGMNCTCELK HWPMELK HN[
_
Powy sza hierarchia jest znacznie bli sza naszej rzeczywistej wiedzy na temat pta-
ków, ni ta zaprezentowana wcześniej.
Nasze rozwiązanie nie jest jednak jeszcze skończone, poniewa w niektórych syste-
mach oprogramowania, proste stwierdzenie, e pingwin jest ptakiem, będzie całkowicie
poprawne. W szczególności, jeśli nasza aplikacja dotyczy wyłącznie dziobów i skrzydeł,
a w adnym stopniu nie wią e się z lataniem, oryginalna hierarchia będzie w zupełno-
ści wystarczająca. Mimo e jest to dosyć irytujące, omawiana sytuacja jest prostym
odzwierciedleniem faktu, e nie istnieje jedna doskonała metoda projektowania do-
wolnego oprogramowania. Dobry projekt musi po prostu uwzględniać wymagania
stawiane przed tworzonym systemem, zarówno te w danej chwili oczywiste, jak i te,
które mogą się pojawić w przyszłości. Jeśli nasza aplikacja nie musi i nigdy nie będzie
musiała uwzględniać mo liwości latania, rozwiązaniem w zupełności wystarczającym
będzie stworzenie klasy 2GPIWKP jako potomnej klasy $KTF. W rzeczywistości taki
projekt mo e być nawet lepszy ni rozró nienie ptaków latających od nielatających,
poniewa takie rozró nienie mo e w ogóle nie istnieć w modelowanym świecie.
Dodawanie do hierarchii niepotrzebnych klas jest błędną decyzją projektową, ponie-
wa narusza prawidłowe relacje dziedziczenia pomiędzy klasami.
Istnieje jeszcze inna strategia postępowania w przypadku omawianego problemu:
„wszystkie ptaki mogą latać, pingwiny są ptakami, pingwiny nie mogą latać”. Strate-
gia polega na wprowadzeniu takich zmian w definicji funkcji HN[, by dla pingwinów
generowany był błąd wykonania:
XQKF GTTQT
EQPUV UVTKPI OUI HWPMELC FGHKPKQYCPC Y KPP[O OKGLUEW
ENCUU 2GPIWKP RWDNKE $KTF ]
RWDNKE
XKTVWCN XQKF HN[
] GTTQT
2KPIYKP[ PKG OQIæ NCVCè _
_
Do takiego rozwiązania dą ą twórcy języków interpretowanych (jak Smalltalk), jed-
nak istotne jest prawidłowe rozpoznanie rzeczywistego znaczenia powy szego kodu,
które jest zupełnie inne, ni mógłbyś przypuszczać. Ciało funkcji nie oznacza bowiem,
e „pingwiny nie mogą latać”. Jej faktyczne znaczenie to: „pingwiny mogą latać, jed-
nak kiedy próbują to robić, powodują błąd”. Na czym polega ró nica pomiędzy tymi
znaczeniami? Wynika przede wszystkim z mo liwości wykrycia błędu — ogranicze-
nie „pingwiny nie mogą latać” mo e być egzekwowane przez kompilatory, natomiast
naruszenie ograniczenia „podejmowana przez pingwiny próba latania powoduje błąd”
mo e zostać wykryte tylko podczas wykonywania programu.
Aby wyrazić ograniczenie „pingwiny nie mogą latać”, wystarczy nie definiować
odpowiedniej funkcji dla obiektów klasy 2GPIWKP:
ENCUU $KTF ]
DTCM FGMNCTCELK HWPMELK HN[
_
10. 164 Dziedziczenie i projektowanie zorientowane obiektowo
ENCUU 0QP(N[KPI$KTF RWDNKE $KTF ]
DTCM FGMNCTCELK HWPMELK HN[
_
ENCUU 2GPIWKP RWDNKE 0QP(N[KPI$KTF ]
DTCM FGMNCTCELK HWPMELK HN[
_
Jeśli spróbujesz teraz wywołać funkcję HN[ dla obiektu reprezentującego pingwina,
kompilator zasygnalizuje błąd:
2GPIWKP R
RHN[
D æF
Zaprezentowane rozwiązanie jest całkowicie odmienne od podejścia stosowanego
w języku Smalltalk. Stosowana tam strategia powoduje, e kompilator skompilowałby
podobny kod bez przeszkód.
Filozofia języka C++ jest jednak zupełnie inna ni filozofia języka Smalltalk, dopóki
jednak programujesz w C++, powinieneś stosować się wyłącznie do reguł obowiązu-
jących w tym języku. Co więcej, wykrywanie błędów w czasie kompilacji (a nie
w czasie wykonywania) programu wią e się z pewnymi technicznymi korzyściami —
patrz sposób 46.
Być mo e przyznasz, e Twoja wiedza z zakresu ornitologii ma raczej intuicyjny
charakter i mo e być zawodna, zawsze mo esz jednak polegać na swojej biegłości
w dziedzinie podstawowej geometrii, prawda? Nie martw się, mam na myśli wyłącz-
nie prostokąty i kwadraty.
Spróbuj więc odpowiedzieć na pytanie: czy reprezentująca kwadraty klasa 5SWCTG
publicznie dziedziczy po reprezentującej prostokąty klasie 4GEVCPING?
Powiesz pewnie: „Te coś! Ka de dziecko wie, e kwadrat jest prostokątem, ale w ogól-
ności prostokąt nie musi być kwadratem”. Tak, to prawda, przynajmniej na poziomie
gimnazjum. Nie sądzę jednak, byśmy kiedykolwiek wrócili do nauki na tym poziomie.
Przeanalizuj więc poni szy kod:
ENCUU 4GEVCPING ]
RWDNKE
XKTVWCN XQKF UGV*GKIJV
KPV PGY*GKIJV
XKTVWCN XQKF UGV9KFVJ
KPV PGY9KFVJ
XKTVWCN KPV JGKIJV
EQPUV HWPMELG YTCECLæ DKG æEG
XKTVWCN KPV YKFVJ
EQPUV YCTVQ EK
_
11. Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 165
XQKF OCMG$KIIGT
4GEVCPING T HWPMELC YKúMUCLæEC RQNG
] RQYKGTEJPK RTQUVQMæVC T
KPV QNF*GKIJV TJGKIJV
TUGV9KFVJ
TYKFVJ
FQFCLG FQ UGTQMQ EK T
CUUGTV
TJGKIJV
QNF*GKIJV WRGYPKC UKú G Y[UQMQ è
_ RTQUVQMæVC T PKG OKGPK C UKú
Jest oczywiste, e ostatnia instrukcja nigdy nie zakończy się niepowodzeniem,
poniewa funkcja OCMG$KIIGT modyfikuje wyłącznie szerokość prostokąta reprezen-
towanego przez T.
Rozwa teraz poni szy fragment kodu, w którym wykorzystujemy publiczne dziedzi-
czenie umo liwiające traktowanie kwadratów jak prostokątów:
ENCUU 5SWCTG RWDNKE 4GEVCPING ] _
5SWCTG U
CUUGTV
UYKFVJ
UJGKIJV
YCTWPGM OWUK D[è RTCYFKY[
FNC YU[UVMKEJ MYCFTCVÎY
OCMG$KIIGT
U PC UMWVGM FKGFKEGPKC U
LGUV Y TGNCELK LGUV MNCUæ
4GEVCPING OQ GO[ YKúE YKúMU[è
RQNG LGIQ RQYKGTEJPK
CUUGTV
U9KFVJ
UJGKIJV
YCTWPGM PCFCN OWUK D[è RTCYFKY[
FNC YU[UVMKEJ MYCFTCVÎY
Tak e teraz oczywiste jest, e ostatni warunek nigdy nie powinien być fałszywy.
Zgodnie z definicją, szerokość kwadratu jest przecie taka sama jak jego wysokość.
Tym razem mamy jednak problem. Jak mo na pogodzić poni sze twierdzenia?
Przed wywołaniem funkcji OCMG$KIIGT wysokość kwadratu reprezentowanego
przez obiekt U jest taka sama jak jego szerokość.
Wewnątrz funkcji OCMG$KIIGT modyfikowana jest szerokość kwadratu,
jednak wysokość pozostaje niezmieniona.
Po zakończeniu wykonywania funkcji OCMG$KIIGT wysokość kwadratu U
ponownie jest taka sama jak jego szerokość (zauwa , e obiekt U jest
przekazywany do funkcji OCMG$KIIGT przez referencję, zatem funkcja
modyfikuje ten sam obiekt U, nie jego kopię).
Jak to mo liwe?
Witaj w cudownym świecie publicznego dziedziczenia, w którym Twój instynkt —
sprawdzający się do tej pory w innych dziedzinach, włącznie z matematyką — mo e
nie być tak pomocny, jak tego oczekujesz. Zasadniczym problemem jest w tym przy-
padku to, e operacja, którą mo na stosować dla prostokątów (jego szerokość mo e
być zmieniana niezale nie od wysokości), nie mo e być stosowana dla kwadratów
(definicja figury wymusza równość jej szerokości i wysokości). Mechanizm publiczne-
go dziedziczenia zakłada jednak, e absolutnie wszystkie operacje stosowane z powo-
dzeniem dla obiektów klasy bazowej mogą być stosowane tak e dla obiektów klasy
12. 166 Dziedziczenie i projektowanie zorientowane obiektowo
potomnej. W przypadku prostokątów i kwadratów (podobny przykład dotyczący zbio-
rów i list omawiam w sposobie 40.) to zało enie się nie sprawdza, zatem stosowanie
publicznego dziedziczenia do modelowania występującej między nimi relacji jest po
prostu błędne. Kompilatory oczywiście umo liwią Ci zaprogramowanie takiego mo-
delu, jednak — jak się ju przekonaliśmy — nie mamy gwarancji, e nasz program
będzie się zachowywał prawidłowo. Od czasu do czasu ka dy programista musi się
przekonać (niektórzy częściej, inni rzadziej), e poprawne skompilowanie programu
nie oznacza, e będzie on działał zgodnie z oczekiwaniami.
Nie denerwuj się, e rozwijana przez lata intuicja dotycząca tworzonego oprogramo-
wania traci moc w konfrontacji z projektowaniem zorientowanym obiektowo. Twoja
wiedza jest nadal cenna, jednak dodałeś właśnie do swojego arsenału rozwiązań pro-
jektowych silny mechanizm dziedziczenia i będziesz musiał rozszerzyć swoją intuicję
w taki sposób, by prowadziła Cię do właściwego wykorzystywania nowych umiejęt-
ności. Z czasem problem klasy 2GPIWKP dziedziczącej po klasie $KTF lub klasie 5SWCTG
dziedziczącej po klasie 4GEVCPING będzie dla Ciebie równie zabawny jak prezentowa-
ne Ci przez niedoświadczonych programistów funkcje zajmujące wiele stron. Mo li-
we, e proponowane podejście do tego typu problemów jest właściwe, nadal jednak
nie jest to bardzo prawdopodobne.
Relacja „jest” nie jest oczywiście jedyną relacją występującą pomiędzy klasami. Dwie
pozostałe powszechnie stosowane relacje między klasami to relacja „ma” (ang. has-a)
oraz relacja implementacji z wykorzystaniem (ang. is-implemented-in-terms-of).
Relacje te przeanalizujemy podczas prezentacji sposobów 40. i 42. Nierzadko pro-
jekty C++ ulegają zniekształceniom, poniewa któraś z pozostałych najwa niejszych
relacji została błędnie zamodelowana jako „jest”, powinniśmy więc być pewni, e
właściwie rozró niamy te relacje i wiemy, jak nale y je najlepiej modelować w C++.
Sposób 36.
Odróżniaj dziedziczenie interfejsu
od dziedziczenia implementacji
Sposób 36. Odró niaj dziedziczenie interfejsu od dziedziczenia implementacji
Po przeprowadzeniu dokładnej analizy okazuje się, e pozornie oczywiste pojęcie
(publicznego) dziedziczenia składa się w rzeczywistości z dwóch rozdzielnych części
— dziedziczenia interfejsów funkcji oraz dziedziczenia implementacji funkcji. Ró nica
pomiędzy wspomnianymi rodzajami dziedziczenia ściśle odpowiada ró nicy pomię-
dzy deklaracjami a definicjami funkcji (omówionej we wstępie do tej ksią ki).
Jako projektant klasy potrzebujesz niekiedy takich klas potomnych, które dziedziczą
wyłącznie interfejs (deklarację) danej funkcji składowej; czasem potrzebujesz klas
potomnych dziedziczących zarówno interfejs, jak i implementację danej funkcji, jednak
masz zamiar przykryć implementację swoim rozwiązaniem; zdarza się tak e, e
potrzebujesz klas potomnych dziedziczących zarówno interfejs, jak i implementację
danej funkcji, ale bez mo liwości przykrywania czegokolwiek.
13. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 167
Aby lepiej zrozumieć ró nicę pomiędzy zaproponowanymi opcjami, przeanalizuj
poni szą hierarchię klas reprezentującą figury geometryczne w aplikacji graficznej:
ENCUU 5JCRG ]
RWDNKE
XKTVWCN XQKF FTCY
EQPUV
XKTVWCN XQKF GTTQT
EQPUV UVTKPI OUI
KPV QDLGEV+
EQPUV
_
ENCUU 4GEVCPING RWDNKE 5JCRG ] _
ENCUU 'NNKRUG RWDNKE 5JCRG ] _
5JCRG jest klasą abstrakcyjną. Mo na to poznać po obecności czystej funkcji wirtual-
nej FTCY. W efekcie klienci nie mogą tworzyć egzemplarzy klasy 5JCRG, mogą to robić
wyłącznie klasy potomne. Mimo to klasa 5JCRG wywiera ogromny nacisk na wszyst-
kie klasy, które (publicznie) po niej dziedziczą, poniewa :
Interfejsy funkcji składowych zawsze są dziedziczone. W sposobie 35.
wyjaśniłem, e dziedziczenie publiczne oznacza faktycznie relację „jest”,
zatem wszystkie elementy istniejące w klasie bazowej muszą tak e istnieć
w klasach potomnych. Jeśli więc daną funkcję mo na wykonać dla danej
klasy, musi tak e istnieć sposób jej wykonania dla jej podklas.
W funkcji 5JCRG zadeklarowaliśmy trzy funkcje. Pierwsza, FTCY, rysuje na ekranie
bie ący obiekt. Druga, GTTQT, jest wywoływana przez inne funkcje składowe w mo-
mencie, gdy konieczne jest zasygnalizowanie błędu. Trzecia, QDLGEV+, zwraca unikalny
całkowitoliczbowy identyfikator bie ącego obiektu (przykład wykorzystania tego typu
funkcji znajdziesz w sposobie 17.). Ka da z wymienionych funkcji została zadekla-
rowana w inny sposób: FTCY jest czystą funkcją wirtualną, GTTQT jest prostą (nieczystą?)
funkcją wirtualną, natomiast QDLGEV+ jest funkcją niewirtualną. Jakie jest znaczenie
tych trzech ró nych deklaracji?
Rozwa my najpierw czystą funkcję wirtualną FTCY. Dwie najistotniejsze cechy czys-
tych funkcji wirtualnych to konieczność ich ponownego zadeklarowania w ka dej
dziedziczącej je konkretnej klasie oraz brak ich definicji w klasach abstrakcyjnych.
Jeśli połączymy te własności, uświadomimy sobie, e:
Celem deklarowania czystych funkcji wirtualnych jest otrzymanie klas
potomnych dziedziczących wyłącznie interfejs.
Jest to idealne rozwiązanie dla funkcji 5JCRGFTCY, poniewa naturalne jest udostęp-
nienie mo liwości rysowania wszystkich obiektów klasy 5JCRG, jednak niemo liwe
jest opracowanie jednej domyślnej implementacji dla takiej funkcji. Algorytm ryso-
wania np. elips ró ni się przecie znacznie od algorytmu rysowania prostokątów.
Właściwym sposobem interpretowania znaczenia deklaracji funkcji 5JCRGFTCY jest
instrukcja skierowana do projektantów podklas: „musicie stworzyć funkcję FTCY, jed-
nak nie mam pojęcia, jak moglibyście ją zaimplementować”.
14. 168 Dziedziczenie i projektowanie zorientowane obiektowo
Istnieje niekiedy mo liwość opracowania definicji czystej funkcji wirtualnej. Oznacza
to, e mo esz stworzyć taką implementację dla funkcji 5JCRGFTCY, e kompilatory
C++ nie zgłoszą adnych zastrze eń, jednak jedynym sposobem jej wywołania byłoby
wykorzystanie pełnej nazwy włącznie z nazwą klasy:
5JCRG
17. RU PGY 'NNKRUG FQDTG
RU FTCY
Y[YQ WLG 'NNKRUGFTCY
RU 5JCRGFTCY
Y[YQ WLG 5JCRGFTCY
RU 5JCRGFTCY
Y[YQ WLG 5JCRGFTCY
Poza faktem, e powy sze rozwiązanie mo e zrobić wra enie na innych programi-
stach podczas imprezy, w ogólności znajomość zaprezentowanego fenomenu jest
w praktyce mało przydatna. Jak się jednak za chwilę przekonasz, mo e być wykorzy-
stywana jako mechanizm udostępniania bezpieczniejszej domyślnej implementacji dla
prostych (nieczystych) funkcji wirtualnych.
Niekiedy dobrym rozwiązaniem jest zadeklarowanie klasy zawierającej wyłącznie
czyste funkcje wirtualne. Takie klasy protokołu udostępniają klasom potomnym jedy-
nie interfejsy funkcji, ale nigdy ich implementacje. Klasy protokołu opisałem, pre-
zentując sposób 34., i wspominam o nich ponownie w sposobie 43.
Znaczenie prostych funkcji wirtualnych jest nieco inne ni znaczenie czystych funkcji
wirtualnych. W obu przypadkach klasy dziedziczą interfejsy funkcji, jednak proste
funkcje wirtualne zazwyczaj udostępniają tak e swoje implementacje, które mogą
(ale nie muszą) być przykryte w klasach potomnych. Po chwili namysłu powinieneś
dojść do wniosku, e:
Celem deklarowania prostej funkcji wirtualnej jest otrzymanie klas potomnych
dziedziczących zarówno interfejs, jak i domyślną implementację tej funkcji.
W przypadku funkcji 5JCRGGTTQT interfejs określa, e ka da klasa musi udostępniać
funkcję wywoływaną w momencie wykrycia błędu, jednak obsługa samych błędów
jest dowolna i zale y wyłącznie od projektantów klas potomnych. Jeśli nie przewidują oni
adnych specjalnych działań w przypadku znalezienia błędu, mogą wykorzystać udostęp-
niany przez klasę 5JCRG domyślny mechanizm obsługi błędów. Oznacza to, e rzeczywi-
stym znaczeniem deklaracji funkcji 5JCRGGTTQT dla projektantów podklas jest zda-
nie: „musisz obsłu yć funkcję GTTQT, jednak jeśli nie chcesz tworzyć własnej wersji
tej funkcji, mo esz wykorzystać jej domyślną wersję zdefiniowaną dla klasy 5JCRG”.
Okazuje się, e zezwalanie prostym funkcjom wirtualnym na precyzowanie zarówno
deklaracji, jak i domyślnej implementacji mo e być niebezpieczne. Aby przekonać się
dlaczego, przeanalizuj zaprezentowaną poni ej hierarchię samolotów nale ących do
linii lotniczych XYZ. Linie XYZ posiadają tylko dwa typy samolotów, Model A
i Model B, z których oba latają w identyczny sposób. Linie lotnicze XYZ zaprojek-
towały więc następującą hierarchię klas:
18. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 169
ENCUU #KTRQTV ] _ TGRTGGPVWLG NQVPKUMC
ENCUU #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
_
XQKF #KTRNCPGHN[
EQPUV #KTRQTV FGUVKPCVKQP
]
FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW
FQ FCPGIQ EGNW
_
ENCUU /QFGN# RWDNKE #KTRNCPG ] _
ENCUU /QFGN$ RWDNKE #KTRNCPG ] _
Aby wyrazić fakt, e wszystkie samoloty muszą obsługiwać jakąś funkcję HN[ oraz
z uwagi na mo liwe wymagania dotyczące innych implementacji tej funkcji genero-
wane przez nowe modele samolotów, funkcja #KTRNCPGHN[ została zadeklarowana
jako wirtualna. Aby uniknąć pisania identycznego kodu w klasach /QFGN# i /QFGN$,
domyślny model latania został jednak zapisany w formie ciała funkcji #KTRNCPGHN[,
które jest dziedziczone zarówno przez klasę /QFGN#, jak i klasę /QFGN$.
Jest to klasyczny projekt zorientowany obiektowo. Dwie klasy współdzielą wspólny
element (sposób implementacji funkcji HN[), zatem element ten zostaje przeniesiony
do klasy bazowej i jest dziedziczony przez te dwie klasy. Takie rozwiązanie ma wiele
istotnych zalet: pozwala uniknąć powielania tego samego kodu, ułatwia przyszłe roz-
szerzenia systemu i upraszcza konserwację w długim okresie czasu — wszystkie wy-
mienione własności są charakterystyczne właśnie dla technologii obiektowej. Linie
lotnicze XYZ powinny więc być dumne ze swojego systemu.
Przypuśćmy teraz, e firma XYZ rozwija się i postanowiła pozyskać nowy typ samo-
lotu — Model C. Nowy samolot ró ni się nieco od Modelu A i Modelu B, a w szcze-
gólności ma inne właściwości lotu.
Programiści omawianych linii lotniczych dodają więc do hierarchii klasę repre-
zentującą samoloty Model C, jednak w pośpiechu zapomnieli ponownie zdefiniować
funkcję HN[:
ENCUU /QFGN% RWDNKE #KTRNCPG ]
DTCM FGMNCTCELK HWPMELK HN[
_
Ich kod zawiera więc coś podobnego do poni szego fragmentu:
#KTRQTV 1MGEKG
1MúEKG VQ NQVPKUMQ Y 9CTUCYKG
#KTRNCPG
20. 170 Dziedziczenie i projektowanie zorientowane obiektowo
Mamy do czynienia z prawdziwą katastrofą, a mianowicie z próbą obsłu enia lotu
obiektu klasy /QFGN%, jakby był obiektem klasy /QFGN# lub klasy /QFGN$. Z pewnością
nie wzbudzimy w ten sposób zaufania u klientów linii lotniczych.
Problem nie polega tutaj na tym, e zdefiniowaliśmy domyślne zachowanie funkcji
#KTRNCPGHN[, tylko na tym, e klasa /QFGN% mogła przypadkowo (na skutek nieuwagi
programistów) dziedziczyć to zachowanie. Na szczęście istnieje mo liwość przekazywa-
nia domyślnych zachowań funkcji do podklas wyłącznie w przypadku, gdy ich twórcy
wyraźnie tego za ądają. Sztuczka polega na przerwaniu połączenia pomiędzy interfejsem
wirtualnej funkcji a jej domyślną implementacją. Oto sposób realizacji tego zadania:
ENCUU #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
RTQVGEVGF
XQKF FGHCWNV(N[
EQPUV #KTRQTV FGUVKPCVKQP
_
XQKF #KTRNCPGFGHCWNV(N[
EQPUV #KTRQTV FGUVKPCVKQP
]
FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW
FQ FCPGIQ EGNW
_
Zwróć uwagę na sposób, w jaki przekształciliśmy #KTRNCPGHN[ w czystą funkcję
wirtualną. Zaprezentowana klasa udostępnia tym samym interfejs funkcji obsługują-
cej latanie samolotów. Klasa #KTRNCPG zawiera tak e jej domyślną implementację,
jednak tym razem w formie niezale nej funkcji, FGHCWNV(N[. Klasy podobne do /QFGN#
i /QFGN$ mogą wykorzystać domyślną implementację, zwyczajnie wywołując funkcję
FGHCWNV(N[ wbudowaną w ciało ich funkcji HN[ (jednak zanim to zrobisz, prze-
czytaj sposób 33., gdzie przeanalizowałem wzajemne oddziaływanie atrybutów KPNKPG
i XKTVWCN dla funkcji składowych):
ENCUU /QFGN# RWDNKE #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
] FGHCWNV(N[
FGUVKPCVKQP _
_
ENCUU /QFGN$ RWDNKE #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
] FGHCWNV(N[
FGUVKPCVKQP _
_
W przypadku klasy /QFGN% nie mo emy ju przypadkowo dziedziczyć niepoprawnej
implementacji funkcji HN[, poniewa czysta funkcja wirtualna w klasie #KTRNCPG wy-
musza na projektantach nowej klasy stworzenie własnej wersji funkcji HN[:
21. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 171
ENCUU /QFGN% RWDNKE #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
_
XQKF /QFGN%HN[
EQPUV #KTRQTV FGUVKPCVKQP
]
MQF OQFGNWLæE[ RTGNQV UCOQNQVW V[RW /QFGN% FQ FCPGIQ EGNW
_
Powy szy schemat postępowania nie jest oczywiście całkowicie bezpieczny (progra-
miści nadal mają mo liwość popełniania fatalnych w skutkach błędów), jednak jest
znacznie bardziej niezawodny od oryginalnego projektu. Funkcja #KTRNCPGFGHCWNV(N[
jest chroniona, poniewa w rzeczywistości jest szczegółem implementacyjnym klasy
#KTRNCPG i jej klas potomnych. Klienci wykorzystujący te klasy powinni zajmować się
wyłącznie własnościami lotu reprezentowanych samolotów, a nie sposobami imple-
mentowania tych własności.
Wa ne jest tak e to, e funkcja #KTRNCPGFGHCWNV(N[ jest niewirtualna. Wynika to
z faktu, e adna z podklas nie powinna jej ponownie definiować — temu zagadnieniu
poświęciłem sposób 37. Gdyby funkcja FGHCWNV(N[ była wirtualna, mielibyśmy do
czynienia ze znanym nam ju problemem: co stanie się, jeśli projektant którejś z pod-
klas zapomni ponownie zdefiniować funkcję FGHCWNV(N[ w sytuacji, gdzie będzie to
konieczne?
Niektórzy programiści sprzeciwiają się idei definiowania dwóch osobnych funkcji dla
interfejsu i domyślnej implementacji (jak HN[ i FGHCWNV(N[). Z jednej strony zauwa-
ają, e takie rozwiązanie zaśmieca przestrzeń nazw klasy występującymi wielokrotnie
zbli onymi do siebie nazwami funkcji. Z drugiej strony zgadzają się z tezą, e nale y
oddzielić interfejs od domyślnej implementacji. Jak więc powinniśmy radzić sobie
z tą pozorną sprzecznością? Wystarczy wykorzystać fakt, e czyste funkcje wirtualne
muszą być ponownie deklarowane w podklasach, ale mogą tak e zawierać własne
implementacje. Oto sposób, w jaki mo emy wykorzystać w hierarchii klas reprezen-
tujących samoloty mo liwość definiowania czystej funkcji wirtualnej:
ENCUU #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
_
XQKF #KTRNCPGHN[
EQPUV #KTRQTV FGUVKPCVKQP
]
FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW
FQ FCPGIQ EGNW
_
ENCUU /QFGN# RWDNKE #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
] #KTRNCPGHN[
FGUVKPCVKQP _
_
22. 172 Dziedziczenie i projektowanie zorientowane obiektowo
ENCUU /QFGN$ RWDNKE #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
] #KTRNCPGHN[
FGUVKPCVKQP _
_
ENCUU /QFGN% RWDNKE #KTRNCPG ]
RWDNKE
XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
_
XQKF /QFGN%HN[
EQPUV #KTRQTV FGUVKPCVKQP
]
MQF OQFGNWLæE[ RTGNQV UCOQNQVW V[RW /QFGN% FQ FCPGIQ EGNW
_
Powy szy schemat niemal nie ró ni się od wcześniejszego projektu z wyjątkiem ciała
czystej funkcji wirtualnej #KTRNCPGHN[, która zastąpiła wykorzystywaną wcześniej
niezale ną funkcję #KTRNCPGFGHCWNV(N[. W rzeczywistości funkcja HN[ została roz-
bita na dwa najwa niejsze elementy. Pierwszym z nich jest deklaracja określająca jej
interfejs (który musi być wykorzystywany przez klasy potomne), natomiast drugim
jest definicja określająca domyślne zachowanie funkcji (która mo e być wykorzystana
w klasach domyślnych, ale tylko na wyraźne ądanie ich projektantów). Łącząc funk-
cje HN[ i FGHCWNV(N[, straciliśmy jednak mo liwość nadawania im ró nych ograniczeń
dostępu — kod, który wcześniej był chroniony (funkcja FGHCWNV(N[ była zadeklaro-
wana w bloku RTQVGEVGF) będzie teraz publiczny (poniewa znajduje się w zadekla-
rowanej w bloku RWDNKE funkcji HN[).
Wróćmy do nale ącej do klasy 5JCRG niewirtualnej funkcji QDLGEV+. Kiedy funkcja
składowa jest niewirtualna, w zamierzeniu nie powinna zachowywać się w klasach
potomnych inaczej ni w klasie bazowej. W rzeczywistości niewirtualne funkcje
składowe opisują zachowanie niezale ne od specjalizacji, poniewa implementacja
funkcji nie powinna ulegać adnym zmianom, niezale nie od specjalizacji kolejnych
poziomów w hierarchii klas potomnych. Oto płynący z tego wniosek:
Celem deklarowania niewirtualnej funkcji jest otrzymanie klas potomnych
dziedziczących zarówno interfejs, jak i wymaganą implementację tej funkcji.
Mo esz pomyśleć, e deklaracja funkcji 5JCRGQDLGEV+ oznacza: „ka dy obiekt klasy
5JCRG zawiera funkcję zwracającą identyfikator obiektu, który zawsze jest wyznaczany
w ten sam sposób (opisany w definicji funkcji 5JCRGQDLGEV+), którego adna klasa
potomna nie powinna próbować modyfikować”. Poniewa niewirtualna funkcja opi-
suje zachowanie niezale ne od specjalizacji, nigdy nie powinna być ponownie dekla-
rowana w adnej podklasie (to zagadnienie szczegółowo omówiłem w sposobie 37.).
Ró nice pomiędzy deklaracjami czystych funkcji wirtualnych, prostych funkcji wirtu-
alnych oraz funkcji niewirtualnych umo liwiają dokładne precyzowanie właściwych
dla danego mechanizmów dziedziczenia tych funkcji przez klasy potomne: dzie-
dziczenia samego interfejsu, dziedziczenia interfejsu i domyślnej implementacji lub
dziedziczenia interfejsu i wymaganej implementacji. Poniewa wymienione ró ne
23. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 173
typy deklaracji oznaczają zupełnie inne mechanizmy dziedziczenia, podczas de-
klarowania funkcji składowych musisz bardzo ostro nie wybrać jedną z omawia-
nych metod.
Pierwszym popularnym błędem jest deklarowanie wszystkich funkcji jako niewirtual-
nych. Eliminujemy w ten sposób mo liwość specjalizacji klas potomnych; szczegól-
nie kłopotliwe są w tym przypadku tak e niewirtualne destruktory (patrz sposób 14.).
Jest to oczywiście dobre rozwiązanie dla klas, które w zało eniu nie będą wykorzy-
stywane w charakterze klas bazowych. W takim przypadku, zastosowanie zbioru wy-
łącznie niewirtualnych funkcji składowych jest całkowicie poprawne. Zbyt często
jednak wynika to wyłącznie z braku wiedzy na temat ró nić pomiędzy funkcjami
wirtualnymi a niewirtualnymi lub nieuzasadnionych obaw odnośnie wydajności funkcji
wirtualnych. Nale y więc pamiętać o fakcie, e niemal wszystkie klasy, które w przy-
szłości mają być wykorzystane jako klasy bazowe, powinny zawierać funkcje wirtu-
alne (ponownie patrz sposób 14.).
Jeśli obawiasz się kosztów związanych z funkcjami wirtualnymi, pozwól, e przypo-
mnę Ci o regule 80-20 (patrz tak e sposób 33.), która mówi, e 80 procent czasu
działania programu jest poświęcona wykonywaniu 20 procent jego kodu. Wspomnia-
na reguła jest istotna, poniewa oznacza, e średnio 80 procent naszych wywołań
funkcji mo e być wirtualnych i będzie to miało niemal niezauwa alny wpływ na cał-
kowitą wydajność naszego programu. Zanim więc zaczniesz się martwić, czy mo esz
sobie pozwolić na koszty związane z wykorzystaniem funkcji wirtualnych, upewnij
się, czy Twoje rozwa ania dotyczą tych 20 procent programu, gdzie decyzja będzie
miała istotny wpływ na wydajność całego programu.
Innym powszechnym problemem jest deklarowanie wszystkich funkcji jako wirtualne.
Niekiedy jest to oczywiście właściwe rozwiązanie — np. w przypadku klas protokołu
(patrz sposób 34.). Mo e jednak świadczyć tak e o zwykłej niewiedzy projektanta
klasy. Niektóre deklaracje funkcji nie powinny umo liwiać ponownego ich definio-
wania w klasach potomnych — w takich przypadkach jedynym sposobem osiągnięcia
tego celu jest deklarowanie tych funkcji jako niewirtualnych. Nie ma przecie naj-
mniejszego sensu udostępnianie innym programistom klas, które mają być dziedzi-
czone przez inne klasy i których wszystkie funkcje składowe będą ponownie definio-
wane. Pamiętaj, e jeśli masz klasę bazową $, klasę potomną oraz funkcję składową
OH, wówczas ka de z poni szych wywołań funkcji OH musi być prawidłowe:
25. RD RF
RD OH
Y[YQ WLG HWPMELú OH C RQOQEæ
YUMC PKMC FQ MNCU[ DCQYGL
RF OH
Y[YQ WLG HWPMELú OH C RQOQEæ
YUMC PKMC FQ MNCU[ RQVQOPGL
Niekiedy musisz zadeklarować funkcję OH jako niewirtualną, by upewnić się, e
wszystko będzie działało zgodnie z Twoimi oczekiwaniami (patrz sposób 37.). Jeśli
działanie funkcji powinno być niezale ne od specjalizacji, nie obawiaj się takiego
rozwiązania.
26. 174 Dziedziczenie i projektowanie zorientowane obiektowo
Sposób 37.
Nigdy nie definiuj ponownie dziedziczonych
funkcji niewirtualnych
Sposób 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych
Istnieją dwa podejścia do tego problemu: teoretyczne i pragmatyczne. Zacznijmy od po-
dejścia pragmatycznego (teoretycy są w końcu przyzwyczajeni do cierpliwego czekania).
Przypuśćmy, e powiem Ci, e klasa publicznie dziedziczy po klasie $ i istnieje
publiczna funkcja składowa OH zdefiniowana w klasie $. Parametry i wartość zwraca-
ne przez funkcję OH są dla nas na tym etapie nieistotne, załó my więc, e mają postać
XQKF. Innymi słowy, mo emy to wyrazić w następujący sposób:
ENCUU $ ]
RWDNKE
XQKF OH
_
ENCUU RWDNKE $ ] _
Nawet gdybyśmy nic nie wiedzieli o $, i OH, mając dany obiekt Z klasy :
Z Z LGUV QDKGMVGO V[RW
bylibyśmy bardzo zaskoczeni, gdyby instrukcje:
$
27. R$ Z QVT[OWLG YUMC PKM FQ Z
R$ OH
Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC
powodowały inne działanie, ni instrukcje:
28. R Z QVT[OWLG YUMC PKM FQ Z
R OH
Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC
Wynika to z faktu, e w obu przypadkach wywołujemy funkcję składową OH dla
obiektu Z. Poniewa w obu przypadkach jest to ta sama funkcja i ten sam obiekt, efekt
wywołania powinien być identyczny, prawda?
Tak, powinien, ale nie jest. W szczególności, rezultaty wywołania będą inne, jeśli OH
będzie funkcją niewirtualną, a klasa będzie zawierała definicję własnej wersji tej
funkcji:
ENCUU RWDNKE $ ]
RWDNKE
XQKF OH
WMT[YC FGHKPKELú $OH RCVT URQUÎD
_
R$ OH
Y[YQ WLG HWPMELú $OH
R OH
Y[YQ WLG HWPMELú OH
29. Sposób 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych 175
Powodem takiego dwulicowego zachowania jest fakt, e niewirtualne funkcje $OH
i OH są wiązane statycznie (patrz sposób 38.). Oznacza to, e poniewa zmienna R$
została zadeklarowana jako wskaźnik do $, niewirtualne funkcje wywoływane za po-
średnictwem tej zmiennej zawsze będą tymi zdefiniowanymi dla klasy $, nawet jeśli
R$ wskazuje na obiekt klasy pochodnej względem $ (jak w powy szym przykładzie).
Z drugiej strony, funkcje wirtualne są wiązane dynamicznie (ponownie patrz sposób 38.),
co oznacza, e opisywany problem ich nie dotyczy. Gdyby OH była funkcją wirtualną,
jej wywołanie (niezale nie od tego, czy z wykorzystaniem wskaźnika do R$ czy do R)
spowodowałoby wywołanie wersji OH, poniewa R$ i R w rzeczywistości wskazują
na obiekt klasy .
Nale y pamiętać, e jeśli tworzymy klasę i ponownie definiujemy dziedziczoną po
klasie $ niewirtualną funkcję OH, obiekty klasy będą się prawdopodobnie okazywały
zachowania godne schizofrenika. W szczególności dowolny obiekt klasy mo e —
w odpowiedzi na wywołanie funkcji OH — zachowywać się albo jak obiekt klasy $,
albo jak obiekt klasy ; czynnikiem rozstrzygającym nie będzie tutaj sam obiekt, ale
zadeklarowany typ wskazującego na ten obiekt wskaźnika. Równie zdumiewające za-
chowanie zaprezentowałyby w takim przypadku referencje do obiektów.
To ju wszystkie argumenty wysuwane przez praktyków. Chcesz pewnie teraz poznać
jakieś teoretyczne uzasadnienie, dlaczego nie nale y ponownie definiować dziedzi-
czonych funkcji niewirtualnych. Wyjaśnię to z przyjemnością.
W sposobie 35. pokazałem, e publiczne dziedziczenie oznacza w rzeczywistości
relację „jest”; w sposobie 36. opisałem, dlaczego deklarowanie niewirtualnych funk-
cji w klasie powoduje niezale ność od ewentualnych specjalizacji tej klasy. Jeśli wła-
ściwie wykorzystasz wnioski wyniesione z tych sposobów podczas projektowania
klas $ i oraz podczas tworzenia niewirtualnej funkcji składowej $OH, wówczas:
Wszystkie funkcje, które mo na stosować dla obiektów klasy $, mo na
stosować tak e dla obiektów klasy , poniewa ka dy obiekt klasy „jest”
obiektem klasy $.
Podklasy klasy $ muszą dziedziczyć zarówno interfejs, jak i implementację
funkcji OH, poniewa funkcja ta została zadeklarowana w klasie $ jako niewirtualna.
Jeśli w klasie ponownie zdefiniujemy teraz funkcję OH, w naszym projekcie powsta-
nie sprzeczność. Jeśli klasa faktycznie potrzebuje własnej implementacji funkcji OH,
która będzie się ró niła od implementacji dziedziczonej po klasie $, oraz jeśli ka dy
obiekt klasy $ (niezale nie od poziomu specjalizacji) rzeczywiście musi wykorzysty-
wać implementacji tej funkcji z klasy $, wówczas stwierdzenie, e „jest” $ jest zwy-
czajnie nieprawdziwe. Klasa nie powinna w takim przypadku publicznie dziedzi-
czyć po klasie $. Z drugiej strony, jeśli naprawdę musi publicznie dziedziczyć po $
oraz jeśli naprawdę musi implementować funkcję OH inaczej, ni implementuje ją
klasa $, wówczas nieprawdą jest, e OH odzwierciedla niezale ność od specjalizacji
klasy $. W takim przypadku funkcja OH powinna zostać zadeklarowana jako wirtualna.
Wreszcie, jeśli ka dy obiekt klasy naprawdę musi być w relacji „jest” z obiektem
klasy $ oraz jeśli funkcja OH rzeczywiście reprezentuje niezale ność od specjalizacji
klasy $, wówczas klasa nie powinna potrzebować własnej implementacji funkcji OH
i jej projektant nie powinien więc podejmować podobnych prób.
30. 176 Dziedziczenie i projektowanie zorientowane obiektowo
Niezale nie od tego, który argument najbardziej pasuje do naszej sytuacji, oczywiste
jest, e ponowne definiowanie dziedziczonych funkcji niewirtualnych jest całkowicie
pozbawione sensu.
Sposób 38.
Nigdy nie definiuj ponownie
dziedziczonej domyślnej wartości parametru
Sposób 38. Nigdy nie definiuj ponownie dziedziczonej domyślnej wartości parametru
Spróbujmy uprościć nasze rozwa ania od samego początku. Domyślny parametr mo e
istnieć wyłącznie jako część funkcji, a nasze klasy mogą dziedziczyć tylko dwa ro-
dzaje funkcji — wirtualne i niewirtualne. Jedynym sposobem ponownego zdefinio-
wania wartości domyślnej parametru jest więc ponowne zdefiniowanie całej dziedzi-
czonej funkcji. Ponowne definiowanie dziedziczonej niewirtualnej funkcji jest jednak
zawsze błędne (patrz sposób 37.), mo emy więc od razu ograniczyć naszą analizę do
sytuacji, w której dziedziczymy funkcję wirtualną z domyślną wartością parametru.
W takim przypadku wyjaśnienie sensu umieszczania tego sposobu w ksią ce jest bar-
dzo proste — funkcje wirtualne są wiązane dynamicznie, ale domyślne wartości ich
parametrów są wiązane statycznie.
Co to oznacza? Być mo e nie posługujesz się biegle najnowszym argonem związa-
nym z programowaniem obiektowym lub zwyczajnie zapomniałeś, jakie są ró nice
pomiędzy wiązaniem statycznym a wiązaniem dynamicznym. Przypomnijmy więc
sobie, o co tak naprawdę chodzi.
Typem statycznym obiektu jest ten typ, który wykorzystujemy w deklaracji obiektu
w kodzie programu. Przeanalizujmy poni szą hierarchię klas:
ENCUU 5JCRG%QNQT ] 4' )4''0 $.7' _
MNCUC TGRTGGPVWLæEC HKIWT[ IGQOGVT[EPG
ENCUU 5JCRG ]
RWDNKE
YU[UVMKG HKIWT[ OWUæ WFQUVúRPKCè T[UWLæEG LG HWPMELG
XKTVWCN XQKF FTCY
5JCRG%QNQT EQNQT 4' EQPUV
_
ENCUU 4GEVCPING RWDNKE 5JCRG ]
RWDNKE
YTÎè WYCIú PC KPPC FQO[ NPæ YCTVQ è RCTCOGVTW NG
XKTVWCN XQKF FTCY
5JCRG%QNQT EQNQT )4''0 EQPUV
_
ENCUU %KTENG RWDNKE 5JCRG ]
RWDNKE
XKTVWCN XQKF FTCY
5JCRG%QNQT EQNQT EQPUV
_
31. Sposób 38. Nigdy nie definiuj ponownie dziedziczonej domyślnej wartości parametru 177
Powy szą hierarchię mo na przedstawić graficznie:
Rozwa my teraz poni sze wskaźniki:
5JCRG
37. W powy szym przykładzie RU, RE i RT są zadeklarowane jako zmienne typu wskaźni-
kowego do obiektów klasy 5JCRG, zatem wszystkie nale ą do typu statycznego.
Zauwa , e nie ma w tym przypadku znaczenia, na co wymienione zmienne faktycz-
nie wskazują — ich statycznym typem jest 5JCRG
38. .
Typ dynamiczny obiektu zale y od typu obiektu aktualnie przez niego wskazywanego.
Oznacza to, e od dynamicznego typu zale y zachowanie obiektu. W powy szym
przykładzie typem dynamicznym zmiennej RE jest %KTENG
39. , zaś typem dynamicznym
zmiennej RT jest 4GEVCPING
40. . Inaczej jest w przypadku zmiennej RU, która nie ma
dynamicznego typu, poniewa w rzeczywistości nie wskazuje na aden obiekt.
Typy dynamiczne (jak sama nazwa wskazuje) mogą się zmieniać w czasie wykony-
wania programu, tego rodzaju zmiany odbywają się zazwyczaj na skutek wykonania
operacji przypisania:
RU RE V[RGO F[PCOKEP[O YUMC PKMC RU
LGUV VGTC %KTENG
42. Wirtualne funkcje są wiązane dynamicznie, co oznacza, e konkretna wywoływana
funkcja zale y od dynamicznego typu obiektu wykorzystywanego do jej wywołania:
RE FTCY
4' Y[YQ WLG HWPMELú %KTENGFTCY
4'
RT FTCY
4' Y[YQ WLG HWPMELú 4GEVCPINGFTCY
4'
Uwa asz pewnie, e nie ma powodów wracać do tego tematu — z pewnością rozu-
miesz ju znaczenie funkcji wirtualnych. Problemy ujawniają się dopiero w momencie,
gdy analizujemy funkcje wirtualne z domyślnymi wartościami parametrów, poniewa
— jak ju wspomniałem — funkcje wirtualne są wiązane dynamicznie, a domyślne
parametry C++ wią e statycznie. Oznacza to, e mo esz wywołać wirtualną funkcję
zdefiniowaną w klasie potomnej, ale z domyślną wartością parametru z klasy bazowej:
RT FTCY
Y[YQ WLG 4GEVCPINGFTCY
4'
43. 178 Dziedziczenie i projektowanie zorientowane obiektowo
W tym przypadku typem dynamicznym zmiennej wskaźnikowej RT jest 4GEVCPING
44. ,
zatem zostanie wywołana (zgodnie z naszymi oczekiwaniami) funkcja wirtualna FTCY
zdefiniowana w klasie 4GEVCPING. Domyślną wartością parametru funkcji 4GEVCP
INGFTCY jest )4''0. Poniewa jednak typem statycznym zmiennej RT jest 5JCRG
45. ,
domyślna wartość parametru dla tego wywołania funkcji będzie pochodziła z definicji
klasy 5JCRG, a nie 4GEVCPING! Otrzymujemy w efekcie wywołanie składające się z nie-
oczekiwanej kombinacji dwóch deklaracji funkcji FTCY — z klas 5JCRG i 4GEVCPING.
Mo esz mi wierzyć, tworzenie oprogramowania zachowującego się w taki sposób jest
ostatnią rzeczą, którą chciałbyś robić; jeśli to Cię nie przekonuje, zaufaj mi — na pewno
z takiego zachowania Twojego oprogramowania nie będą zadowoleni Twoi klienci.
Nie muszę chyba dodawać, e nie ma w tym przypadku adnego znaczenia fakt, e
RU, RE i RT są wskaźnikami. Gdyby były referencjami, problem nadal by istniał. Jedy-
nym istotnym źródłem naszego problemu jest to, e FTCY jest funkcją wirtualną i jedna
z jej domyślnych wartości parametrów została ponownie zdefiniowana w podklasie.
Dlaczego C++ umo liwia tworzenie oprogramowania zachowującego się w tak nienatu-
ralny sposób? Odpowiedzią jest efektywność wykonywania programów. Gdyby domyślne
wartości parametrów były wiązane dynamicznie, kompilatory musiałyby stosować
dodatkowe mechanizmy określania właściwych domyślnych wartości parametrów
funkcji wirtualnych podczas wykonywania programów, co prowadziłoby do spowolnienia
i komplikacji stosowanego obecnie mechanizmu ich wyznaczania w czasie kompilacji.
Decyzję podjęto więc wyłącznie z myślą o szybkości i prostocie implementacji. Efektem
jest wydajny mechanizm wykonywania programów, ale tak e — jeśli nie będziesz
stosował zaleceń zawartych w tym sposobie — potencjalne nieporozumienia.
Sposób 39.
Unikaj rzutowania
w dół hierarchii dziedziczenia
Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia
W dzisiejszych niespokojnych czasach warto mieć na oku poczynania instytucji
finansowych, rozwa my więc klasę protokołu (patrz sposób 34.) dla kont bankowych:
ENCUU 2GTUQP ] _
ENCUU $CPM#EEQWPV ]
RWDNKE
$CPM#EEQWPV
EQPUV 2GTUQP
48. Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia 179
Wiele banków przedstawia dzisiaj swoim klientom niezwykle szeroką ofertę typów
kont bankowych, załó my jednak (dla uproszczenia), e istnieje tylko jeden typ konta
bankowego, zwykłe konto oszczędnościowe:
ENCUU 5CXKPIU#EEQWPV RWDNKE $CPM#EEQWPV ]
RWDNKE
5CXKPIU#EEQWPV
EQPUV 2GTUQP
50. LQKPV1YPGT
`5CXKPIU#EEQWPV
XQKF ETGFKV+PVGTGUV
FQFCL QFUGVMK FQ MQPVC
_
Nie jest to mo e zbyt zaawansowane konto oszczędnościowe, jest jednak w zupełno-
ści wystarczające dla naszych rozwa ań.
Bank prawdopodobnie przechowuje listę wszystkich swoich kont, która mo e być zaim-
plementowana za pomocą szablonu klasy NKUV ze standardowej biblioteki C++ (patrz
sposób 49.). Przypuśćmy, e w naszym banku taka lista nosi nazwę CNN#EEQWPVU:
NKUV$CPM#EEQWPV
51. CNN#EEQWPVU YU[UVMKG MQPVC QDU WIKYCPG
RTG FCP[ DCPM
Jak wszystkie standardowe pojemniki, listy przechowują jedynie kopie umieszczanych
w nich obiektów, zatem, aby uniknąć przechowywania wielu kopii poszczególnych
obiektów klasy $CPM#EEQWPV, programiści zdecydowali, e lista CNN#EEQWPVU powinna
składać się jedynie ze wskaźników do tych obiektów, a nie samych obiektów repre-
zentujących konta.
Przypuśćmy, e naszym zadaniem jest kolejne przejście przez wszystkie konta i doliczenie
do nich nale nych odsetek. Mo emy zrealizować tę usługę w następujący sposób:
RúVNC PKG QUVCPKG UMQORKNQYCPC
LG NK PKIF[ YEG PKGL PKG OKC G
FQ E[PKGPKC MQFGO Y[MQT[UVWLæE[O KVGTCVQT[ RCVT PK GL
HQT
NKUV$CPM#EEQWPV
52. KVGTCVQT R CNN#EEQWPVUDGIKP
R CNN#EEQWPVUGPF
R ]
53. R ETGFKV+PVGTGUV
D æF
_
Nasze kompilatory szybko zasygnalizują, e lista CNN#EEQWPVU zawiera wskaźniki do
obiektów klasy $CPM#EEQWPV, a nie obiektów klasy 5CXKPIU#EEQWPV, zatem w ka dej
kolejnej iteracji zmienna R będzie wskazywała na obiekt klasy $CPM#EEQWPV. Oznacza
to, e wywołanie funkcji ETGFKV+PVGTGUV jest nieprawidłowe, poniewa została ona
zadeklarowana wyłącznie dla obiektów klasy 5CXKPIU#EEQWPV, a nie $CPM#EEQWPV.
Jeśli wiersz NKUV$CPM#EEQWPV
54. KVGTCVQT R CNN#EEQWPVUDGIKP
jest dla Ciebie
niezrozumiały i nie przypomina Ci kodu C++, z którym miałeś do czynienia do tej pory,
oznacza to, e prawdopodobnie nie miałeś wcześniej przyjemności korzystać z szablo-
nów klas pojemnikowych ze standardowej biblioteki C++. Tę część biblioteki nazywa
się często Standardową Biblioteką Szablonów (ang. Standard Template Library — STL),
więcej informacji na jej temat znajdziesz w sposobie 49. Na tym etapie wystarczy Ci
55. 180 Dziedziczenie i projektowanie zorientowane obiektowo
wiedza, e zmienna R zachowuje się jak wskaźnik wskazujący na przeglądane w pętli
kolejne elementy listy CNN#EEQWPVU (od jej początku do końca). Oznacza to, e zmien-
na R jest traktowana tak, jakby jej typem był $CPM#EEQWPV
56.
57. , a elementy przeglądanej
listy były przechowywane w tablicy.
Powy szy kod nie zostanie niestety skompilowany. Wiemy oczywiście, e lista
CNN#EEQWPVU została zdefiniowana jako pojemnik na wskaźniki typu $CPM#EEQWPV
58. ,
ale mamy tak e świadomość, e w powy szej pętli faktycznie przechowuje wskaźniki
typu 5CXKPIU#EEQWPV
59. , poniewa 5CXKPIU#EEQWPV jest jedyną klasą, dla której mo emy
tworzyć obiekty. Głupie kompilatory! Zdecydowaliśmy się przekazać im wiedzę, któ-
ra jest dla nas zupełnie oczywista i okazało się, e są zbyt tępe, by zauwa yć, e lista
CNN#EEQWPVU przechowuje w rzeczywistości wskaźniki typu 5CXKPIU#EEQWPV
61. KVGTCVQT R CNN#EEQWPVUDGIKP
R CNN#EEQWPVUGPF
R ]
UVCVKEAECUV5CXKPIU#EEQWPV
62.
63. R ETGFKV+PVGTGUV
_
Rozwiązaliśmy wszystkie nasze problemy! Zrobiliśmy to przejrzyście, elegancko i zwięźle
— wystarczyło jedno proste rzutowanie. Wiemy, jakiego typu wskaźniki faktycznie
znajdują się na liście CNN#EEQWPVU, nasze ogłupiałe kompilatory tego nie wiedzą, więc
zastosowaliśmy rzutowanie, by przekazać im naszą wiedzę. Czy mo na znaleźć bar-
dziej logiczne rozwiązanie?
Chciałbym w tym momencie przedstawić pewną biblijną analogię. Operacje rzutowa-
nia są dla programistów C++ jak jabłko dla biblijnej Ewy.
Zaprezentowany w powy szym kodzie rodzaj rzutowania (ze wskaźnika do klasy bazowej
do wskaźnika do klasy potomnej) nosi nazwę rzutowania w dół, poniewa rzutujemy
w dół hierarchii dziedziczenia. W powy szym przykładzie operacja rzutowania w dół
zadziałała, jednak — jak się za chwilę przekonasz — takie rozwiązanie prowadzi do
ogromnych problemów podczas konserwacji oprogramowania.
Wróćmy jednak do naszego banku. Zachęcony sukcesem, jakim było rozwiązanie pro-
blemu kont oszczędnościowych, bank postanawia zaoferować swoim klientom tak e
konta czekowe. Co więcej, załó my, e do tego typu kont tak e dolicza się nale ne od-
setki (podobnie, jak w przypadku kont oszczędnościowych):
ENCUU %JGEMKPI#EEQWPV RWDNKE $CPM#EEQWPV ]
RWDNKE
XQKF ETGFKV+PVGTGUV
FQFCL QFUGVMK FQ MQPVC
_
Nie muszę chyba dodawać, e lista CNN#EEQWPVU będzie teraz zawierała wskaźniki
zarówno do obiektów reprezentujących konta oszczędnościowe, jak i do tych repre-
zentujących konta czekowe. Okazuje się, e stworzona przed chwilą pętla naliczająca
odsetki przestaje działać.
64. Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia 181
Pierwszy problem polega na tym, e pętla nadal będzie poprawnie kompilowana, mimo
e nie wprowadziliśmy jeszcze zmian uwzględniających istnienie obiektów nowej
klasy %JGEMKPI#EEQWPV. Wynika to z faktu, e nasze kompilatory nadal będą pochopnie
zakładały, e kiedy sygnalizujemy (za pomocą UVCVKEAECUV), e
66. , tak jest w istocie. W końcu to do nas nale y przewi-
dywanie skutków wykonywania poszczególnych instrukcji. Drugi problem wynika
z typowego sposobu radzenia sobie z pierwszym problemem, co zwykle skutkuje
tworzeniem podobnego kodu:
HQT
NKUV$CPM#EEQWPV
67. KVGTCVQT R CNN#EEQWPVUDGIKP
R CNN#EEQWPVUGPF
R ]
KH
68. R YUMCWLG PC QDKGMV MNCU[ 5CXKPIU#EEQWPV
UVCVKEAECUV5CXKPIU#EEQWPV
72. R ETGFKV+PVGTGUV
_
Jeśli kiedykolwiek stwierdzisz, e Twój kod zawiera instrukcję warunkową w postaci:
„jeśli obiekt jest typu T1, zrób coś, jeśli jednak jest typu T2, zrób coś innego”, na-
tychmiast uderz się w pierś. Tego rodzaju instrukcje są nienaturalne dla języka C++;
podobną strategię mo na stosować w C lub Pascalu, ale nigdy w C++, w którym mo-
emy przecie u yć funkcji wirtualnych.
Pamiętaj, e zastosowanie funkcji wirtualnych sprawia, e za zapewnianie prawi-
dłowych wywołań funkcji — w zale ności od typu wykorzystywanego obiektu —
odpowiadają kompilatory. Nie zaśmiecaj więc swojego kodu instrukcjami warunko-
wymi ani przełącznikami; zamiast tego wykorzystuj mo liwości swoich kompilato-
rów. Oto przykład:
ENCUU $CPM#EEQWPV ] _ LCM Y[ GL
PQYC MNCUC TGRTGGPVWLæEC MQPVC RT[PQUæEG MNKGPVQO QFUGVMK
ENCUU +PVGTGUV$GCTKPI#EEQWPV RWDNKE $CPM#EEQWPV ]
RWDNKE
XKTVWCN XQKF ETGFKV+PVGTGUV
_
ENCUU 5CXKPIU#EEQWPV RWDNKE +PVGTGUV$GCTKPI#EEQWPV ]
LCM Y[ GL
_
ENCUU %JGEMKPI#EEQWPV RWDNKE +PVGTGUV$GCTKPI#EEQWPV ]
LCM Y[ GL
_
Powy szą hierarchię mo na przedstawić graficznie:
73. 182 Dziedziczenie i projektowanie zorientowane obiektowo
Poniewa zarówno konta oszczędnościowe, jak i konta czekowe wymagają naliczania
odsetek, naturalnym rozwiązaniem jest przeniesienie wspólnej operacji na wy szy
poziom hierarchii klas — do wspólnej klasy bazowej. Jednak przy zało eniu, e nie
wszystkie konta w danym banku muszą mieć naliczane odsetki (jak wynika z moich
doświadczeń, takie zało enie jest sensowne), nie mo emy przenieść wspomnianej
operacji do najwy szej (w hierarchii) klasy $CPM#EEQWPV. Wprowadziliśmy więc nową
podklasę klasy $CPM#EEQWPV, którą nazwaliśmy +PVGTGUV$GCTKPI#EEQWPV, i która jest
klasą bazową dla klas 5CXKPIU#EEQWPV i %JGEMKPI#EEQWPV.
Wymaganie, by zarówno dla konta oszczędnościowego, jak i dla konta czekowego
naliczać odsetki, uwzględniliśmy, deklarując w klasie +PVGTGUV$GCTKPI#EEQWPV czystą
funkcję wirtualną ETGFKV+PVGTGUV, która zostanie przypuszczalnie zdefiniowana w pod-
klasach 5CXKPIU#EEQWPV i %JGEMKPI#EEQWPV.
Nowa hierarchia klas umo liwia nam napisanie omawianej wcześniej pętli od początku:
TQYKæCPKG NGRUG CNG PCFCN PKGFQUMQPC G
HQT
NKUV$CPM#EEQWPV
74. KVGTCVQT R CNN#EEQWPVUDGIKP
R CNN#EEQWPVUGPF
R ]
UVCVKEAECUV+PVGTGUV$GCTKPI#EEQWPV
75.
76. R ETGFKV+PVGTGUV
_
Mimo e powy sza pętla nadal zawiera skrytykowane wcześniej rzutowanie, zapropo-
nowane rozwiązanie jest znacznie lepsze od omawianych do tej pory, poniewa będzie
poprawnie funkcjonowało nawet po dodaniu do naszej aplikacji nowych podklas do
klasy +PVGTGUV$GCTKPI#EEQWPV.
Aby całkowicie pozbyć się rzutowania, musimy wprowadzić do naszego projektu kilka
dodatkowych modyfikacji. Jedną z nich jest doprecyzowanie specyfikacji wykorzy-
stywanej listy kont. Gdybyśmy mogli wykorzystać listę obiektów klasy +PVGTGUV$G
CTKPI#EEQWPV, zamiast obiektów klasy $CPM#EEQWPV, rozwiązanie byłoby trywialne:
YU[UVMKG QDU WIKYCPG RTG DCPM MQPVC QFUGVMCOK
NKUV+PVGTGUV$GCTKPI#EEQWPV
77. CNN+$#EEQWPVU
RúVNC QUVCPKG UMQORKNQYCPC K DúFKG FKC C C RTCYKF QYQ
HQT
NKUV+PVGTGUV$GCTKPI#EEQWPV
78. KVGTCVQT R CNN+$#EEQWPVUDGIKP
R CNN+$#EEQWPVUGPF
R ]
R
80. Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia 183
Jeśli tworzenie bardziej specjalizowanej listy nie jest brane pod uwagę, mo na stwier-
dzić, e operację ETGFKV+PVGTGUV mo na stosować dla wszystkich kont bankowych;
jednak w przypadku kont, dla których nie nalicza się odsetek, wspomniana operacja
jest pusta. To samo mo emy wyrazić za pomocą poni szego fragmentu kodu:
ENCUU $CPM#EEQWPV ]
RWDNKE
XKTVWCN XQKF ETGFKV+PVGTGUV
]_
_
ENCUU 5CXKPIU#EEQWPV RWDNKE $CPM#EEQWPV ] _
ENCUU %JGEMKPI#EEQWPV RWDNKE $CPM#EEQWPV ] _
NKUV$CPM#EEQWPV
82. KVGTCVQT R CNN#EEQWPVUDGIKP
R CNN#EEQWPVUGPF
R ]
R
83. ETGFKV+PVGTGUV
_
Zauwa , e wirtualna funkcja $CPM#EEQWPVETGFKV+PVGTGUV udostępnia pustą do-
myślną implementację. Jest to wygodny sposób definiowania funkcji, która domyślnie
jest operacją pustą, mo e jednak w przyszłości doprowadzić do nieprzewidywalnych
trudności. Omówienie przyczyn takiego niebezpieczeństwa i sposobów jego elimino-
wania znajdziesz w sposobie 36. Zauwa tak e, e funkcja ETGFKV+PVGTGUV jest (nie-
jawnie) wbudowana. Oczywiście nie ma w tym nic złego, jednak z uwagi na fakt, e
omawiana funkcja jest tak e wirtualna, dyrektywa KPNKPG będzie prawdopodobnie
zignorowana przez kompilator (patrz sposób 33.).
Jak się przekonałeś, rzutowanie w dół hierarchii klas mo e być eliminowane na wiele
sposobów. Najlepszym z nich jest zastąpienie tego typu operacji wywołaniami funkcji
wirtualnych — mo na wówczas zdefiniować domyślne implementacje tych funkcji
jako operacje puste, co pozwoli na ich prawidłową obsługę przez obiekty klasy, dla
których dane działania nie mają sensu. Drugą metodą jest uściślenie wykorzystywa-
nych typów, co pozwala wyeliminować nieporozumienia wynikające z rozbie ności
pomiędzy typami deklarowanymi a typami reprezentowanymi przez u ywane obiekty
w rzeczywistości. Wysiłek związany z eliminowaniem rzutowania w dół nie idzie na
marne, poniewa kod zawierający operacje tego typu jest brzydki i mo e być źródłem
błędów, jest tak e trudny do zrozumienia, rozszerzania i konserwacji.
To, co napisałem do tej pory, jest prawdą i tylko prawdą. Nie jest jednak całą prawdą. Ist-
nieją bowiem sytuacje, w których naprawdę musisz wykonać operację rzutowania w dół.
Przykładowo, przypuśćmy, e mamy do czynienia z rozwa aną na początku tego spo-
sobu sytuacją, w której lista CNN#EEQWPVU przechowuje wskaźniki do obiektów klasy
$CPM#EEQWPV, funkcja ETGFKV+PVGTGUV jest zdefiniowana wyłącznie dla obiektów klasy
5CXKPIU#EEQWPV i musimy napisać pętlę naliczającą odsetki dla wszystkich kont.
Przypuśćmy tak e, e wszystkie wspomniane elementy są poza naszą kontrolą, co