2010.04_Upraszczanie zależności przy modyfikacji interfejsu klas_[Programowanie C C++].pdf

(348 KB) Pobierz
441718554 UNPDF
Programowanie C++
Wizytator
Upraszczanie zależności przy modyfikacji interfejsu klas
Operacje dla obiektów w hierarchii klas często implementujemy,
wykorzystując funkcje wirtualne. Gdy liczba takich metod rośnie,
klasy mają trudną do określenia odpowiedzialność, kod staje się mało
przejrzysty. Przedstawiona technika rozwiązuje ten problem.
Dowiesz się:
• Co to jest wzorzec wizytatora;
• Jak używać biblioteki boost::variant.
Powinieneś wiedzieć:
• Jak pisać proste programy w C++;
• Co to jest dziedziczenie i funkcje wirtualne.
kim podejściu kod dotyczący tej samej funkcji
będzie rozproszony, każda klasa będzie miała
fragment funkcjonalności, zmniejsza to czy-
telność kodu i utrudnia jego modyfikacje.
Przykładowo, obliczanie statystyk (liczby żoł-
nierzy, liczby czołgów itd.) wymaga, oprócz
klasy przechowującej liczniki, dostarczenia
metody w każdej klasie konkretnej, która
aktualizuje te liczniki (Listing 1). Dodatko-
wo klasy reprezentujące jednostki będą mia-
ły wiele różnych metod, trudno będzie okre-
ślić ich odpowiedzialność.
Kod źródłowy będzie bardziej przejrzysty,
jeżeli daną funkcję zrealizujemy w spójnym
fragmencie kodu. Dla rozpatrywanego przy-
kładu możemy aktualizację liczników prze-
nieść do klasy, która je zawiera (patrz Listing
2). Klasy reprezentujące jednostki będą prost-
sze, nie muszą zawierać metod związanych
z obliczaniem statystyk. Kod realizujący da-
ną funkcję jest umieszczony w jednym miej-
scu, np. kod obliczania statystyk zawiera kla-
sa Statistics (Listing 2). Niestety przedsta-
wiona technika wymaga jawnego badania ty-
pu obiektu za pomocą mechanizmów reflek-
sji, co jest dosyć czasochłonne. Łańcuch in-
strukcji warunkowych, który porównuje typ
obiektu ze wszystkimi typami w hierarchii,
nie jest eleganckim rozwiązaniem.
Wzorzec wizytatora pozwala zachować za-
lety wynikające z umieszczenia kodu realizu-
jącego daną funkcję w jednym miejscu (poza
klasami w hierarchii), usuwając konieczność
badania typu obiektu za pomocą łańcucha in-
strukcji dynamic_cast.
Poziom
trudności
mi w książce Gamma, Helm, Johnson, Vlis-
sides ,,Wzorce projektowe'', pozwala spra-
wić, że modyfikacja funkcjonalności będzie
prosta, natomiast złożona będzie modyfika-
cja struktury. Przykład ilustrujący omawianą
technikę wykorzystuje hierarchię klas Unit ,
przedstawioną na Rysunku 1, reprezentują-
cą różne oddziały wykorzystywane w grze
strategicznej: jednostki piechoty, czołgi, wy-
rzutnie rakiet itd. Podczas tworzenia kolej-
nych wersji gry hierarchii tej prawie nie bę-
dziemy zmieniać, typy jednostek będą usta-
lone we wczesnych etapach implementacji.
Spodziewamy się, że będą zmieniane funkcje
operujące na jednostkach lub grupach jedno-
stek. Funkcje te będą obliczały wartość bojo-
wą jednostek, ich szybkość przemieszczania,
będą automatycznie rozmieszczać jednostki
na danym obszarze, obrazować te jednostki,
generować statystyki itp. Zbiór tych funkcji
będziemy rozszerzali podczas tworzenia ko-
lejnych wersji aplikacji.
Gdyby funkcje operujące na jednostkach
implementować jako metody klas, tak jak po-
kazano na Listingu 1, to wystąpi niedogod-
ność, o której była mowa na początku, doda-
wanie lub usuwanie metody wymaga mody-
fikacji wszystkich klas w hierarchii. Przy ta-
chii polega na utworzeniu tej klasy
oraz nadpisaniu metod. Modyfiku-
jemy jedynie fragment kodu źródłowego za-
wierający implementację nowej klasy, najczę-
ściej tworzymy nowy plik. Dodawanie meto-
dy, która będzie nadpisywana, jest bardziej
skomplikowane: modyfikujemy klasę bazową
oraz wszystkie klasy pochodne, dostarczając
odpowiednią funkcjonalność. W tym przy-
padku modyfikacja obejmuje wiele różnych
fragmentów kodu. Taka asymetria pomię-
dzy modyfikacją hierarchii klas a modyfika-
cją interfejsu w tej hierarchii jest niewygod-
na, zwłaszcza gdy częściej będziemy modyfi-
kować funkcjonalność niż strukturę. Artykuł
przedstawia wzorzec projektowy wizytatora
(inna nazwa to odwiedzający), który pozwa-
la uprościć zależności przy modyfikacji funk-
cji operujących na hierarchii klas. W dalszej
części artykułu przedstawiona zostanie za-
sada działania tego wzorca, przykład użycia
oraz wykorzystanie wizytatora w bibliotece
boost::variant.
Szybki start
Aby uruchomić przedstawione przykłady, należy mieć dostęp do kompilatora C++ oraz edy-
tora tekstu. Niektóre przykłady korzystają z udogodnień dostarczanych przez bibliotekę bo-
ost::variant, warunkiem ich uruchomienia jest instalacja bibliotek boost (w wersji 1.36 lub
nowszej) Na wydrukach pominięto dołączanie odpowiednich nagłówków oraz udostępnia-
nie przestrzeni nazw, pełne źródła dołączono jako materiały pomocnicze.
Niedogodności
nadpisywania metod
Wzorzec projektowy wizytatora (inna na-
zwa to odwiedzający), opisany między inny-
16
04/2010
D odawanie nowej klasy do hierar-
441718554.023.png 441718554.024.png 441718554.025.png 441718554.026.png 441718554.001.png 441718554.002.png 441718554.003.png 441718554.004.png 441718554.005.png 441718554.006.png 441718554.007.png
Wizytator
Wzorzec wizytatora
Wzorzec wizytatora wykorzystuje pomocni-
czą hierarchię klas, zwaną hierarchią wizy-
tującą lub odwiedzającą, która jest związana
z hierarchią klas, dla której chcemy odwie-
dzać (wizytować) obiekty. Klasa bazowa tej
nowej hierarchii, abstrakcyjny wizytator, do-
starcza metod, które będą wołane dla poszcze-
gólnych obiektów klas hierarchii odwiedza-
nej. Dla omawianego przykładu abstrakcyjny
wizytator został przedstawiony na Listingu 3.
Dla każdej klasy odwiedzanej (wizytowanej)
dostarczamy metodę accept , która woła odpo-
wiednią metodę wizytatora. Metoda accept
jest wykorzystywana przez wizytator do uzy-
skania informacji o typie obiektu. Modyfikacja
hierarchii odwiedzanej została przedstawiona
na Listingu 4. Dla wizytatora, który jest argu-
mentem, wołana jest jedna z przeciążonych
metod visit , w zależności od typu obiektu,
który nadpisuje metodę accept .
Operacje na hierarchii odwiedzanej, na
przykład zbieranie statystyk, implementu-
jemy, wykorzystując klasę pochodną po abs-
trakcyjnym wizytatorze (Listing 5). Technika
ta pozwala na uzyskanie typu obiektu odwie-
dzanego bez rzutowania dynamicznego, wy-
korzystujemy dwukrotnie funkcje wirtualne.
Innymi słowy, metody visit dostają jako ar-
gument obiekt odpowiedniego typu, wybór
tego typu odbywa się poprzez mechanizm
późnego wiązania, użyty przy wyborze odpo-
wiedniej metody accept oraz przy wołaniu
metody visit .
Przykład użycia wizytatora zawiera Listing
6. Metoda accept jest wołana dla obiektu u ,
który jest typu Tank (ale wskaźnik jest ty-
pu Unit* ). Metoda ta będzie wołała metodę
visit(Tank&) dla przekazanego argumentu,
czyli dla obiektu s .
Wzorzec wizytatora stosuje się po to, aby
ułatwić dodawanie nowych operacji dla hie-
rarchii klas, oraz po to, aby metody wykonują-
ce daną funkcję umieścić w tym samym miej-
scu. Dodanie nowego wizytatora nie wymaga
wiele zmian, należy wyprowadzić nową klasę
z klasy Visitor , a następnie nadpisać w niej
odpowiednie metody. Inne klasy nie są zmie-
niane, w szczególności nie są zmieniane klasy
w hierarchii wizytującej.
plementacji operacji na przechowywanym
obiekcie. Szablon variant tworzy typ złożo-
ny, który jest równoważny unii z C (definio-
wanej przez union ) lub rekordowi z warian-
tami z Pascala. Obiekt typu variant mo-
że przechowywać jeden z obiektów składo-
wych, wielkość (zajętość pamięci) jest zależ-
na od wielkości największego obiektu skła-
dowego. Typ ten posiada semantykę warto-
ści, to znaczy konstruktor kopiujący, opera-
tor przypisania, można go stosować jako ar-
gument funkcji, zwracać jako wartość, prze-
Listing 1. Przykład funkcji, która wymaga mody�kacji wszystkich elementów w hierarchii
class Statistics { //Klasa reprezentuje statystyki
public :
void addSoldiers ( int n ) { soldiers_ += n ; } ; //aktualizuje licznik
void addTanks ( int n ) { tanks_ += p ; }
void addRockets ( int n ) { rockets_ += n ; }
private :
int soldiers_ ; //licznik żołnierzy
int tanks_ ;
int rockets_ ;
} ;
class Unit { //klasa bazowa
public : //zawiera interfejs do metody aktualizującej statystyki
virtual void update ( Statistics & s ) = 0 ;
} ;
void Infantry :: update ( Statistics & s ) // aktualizuje statystyki
s . addSoldiers ( countSoldiers () );
}
void Tank :: update ( Statistics & s ) {
s . addTanks ( 1 );
}
Listing 2. Odwrócenie zależności pozwala zgrupować kod aktualizujący w jednym miejscu,
jednak wymaga jawnego badania typu obiektów
void Statistics :: update ( const Unit & unit ) {
if ( const Infantry * p = dynamic_cast < const Infantry *>(& unit ) ) { //jawne badanie
typu
soldiers_ += p -> countSoldiers ();
} else if ( dynamic_cast < const Tank *>(& unit ) {
++ tanks_ ;
} // łańcuch warunków dla wszystkich typów w hierarchii
}
Listing 3. Abstrakcyjny wizytator dla jednostek z omawianej gry strategicznej
class Visitor { //abstrakcyjny wizytator
public :
virtual void visit ( Infantry &) = 0 ;
virtual void visit ( Tank &) = 0 ;
virtual void visit ( Rocket &) = 0 ;
} ;
Listing 4. Metoda w hierarchii odwiedzanej wykorzystywana przez wizytatory
class Unit { //Klasa bazowa
public :
virtual void accept ( Visitor & v ) = 0 ;
} ;
class Infantry : public Unit {
public :
virtual void accept ( Visitor & v ) { v . visit (* this ); } //woła v.visit(Infantry&)
} ;
class Tank : public Unit {
public :
virtual void accept ( Visitor & v ) { v . visit (* this ); } //woła v.visit(Tank&)
} ;
boost::variant
Wzorzec wizytatora jest wykorzystywany
w szablonie klasy boost::variant do im-
Rysunek 1.
www.sdjournal.org
17
441718554.008.png 441718554.009.png 441718554.010.png 441718554.011.png 441718554.012.png 441718554.013.png 441718554.014.png 441718554.015.png 441718554.016.png 441718554.017.png 441718554.018.png 441718554.019.png
Programowanie C++
chowywać w kontenerach standardowych.
Typami składowymi mogą być klasy dostar-
czające konstruktorów, co jest zabronione
przy używaniu unii w C++. Przykłady wy-
korzystania wariantów pokazano na wy-
druku 7.
Dostęp do przechowywanych warto-
ści wariantu jest możliwy poprzez wi-
zytator. Musimy dostarczyć konkretne-
go wizytatora dziedziczącego po szablo-
nie static_visitor (parametrem tego sza-
blonu jest typ zwracany z metod wizytu-
jących, możemy zwracać wartość w meto-
dzie wizytującej). Metody wizytujące imple-
mentuje się jako przeciążone operatory wo-
łania funkcyjnego, zamiast metod visit , co
jest częstą praktyką przy implementacji tego
wzorca w C++. Wizytację uruchamia funk-
cja apply_visitor , do której przekazujemy
obiekt wizytatora i wariant, patrz Listing 8.
Wariant, czyli unia z kontrolą typów i wy-
różnikiem bieżącego typu, korzysta z pro-
gramowania generycznego do bezpiecznego
przechowywania obiektów w tym samym
miejscu pamięci. Narzuty pamięciowe są ma-
łe, związane jedynie z wyróżnikiem aktual-
nego typu (obecnie 4 bajty na platformach
wspieranych przez boost). Wewnętrzny bu-
for ma wielkość maksymalnego obiektu, któ-
ry może być przechowywany w wariancie
(uwzględniając wyrównanie).
Listing 5. Klasa obliczająca statystyki jako wizytator
Podsumowanie
Wizytator pozwala implementować funk-
cje operujące na hierarchii klas jako oddziel-
ne typy, co upraszcza zależności w aplikacji
i pozwala na tworzenie przejrzystego kodu.
Oprócz wielu zalet wizytator ma kilka wad.
Operacje na hierarchii odwiedzanej, imple-
mentowane w wizytatorze, wykorzystują in-
terfejs klas odwiedzanych, więc muszą istnieć
odpowiednie metody, które daną operację po-
zwolą wykonać. Może to prowadzić do złama-
nia enkapsulacji, na przykład jeżeli do realiza-
cji funkcji implementowanych w wizytatorze
wymagana jest znajomość wewnętrznego sta-
nu obiektów odwiedzanych.
Wizytator wprowadza cykliczne zależności
pomiędzy hierarchią odwiedzaną i odwiedza-
jącą, które sprawiają, że implementacja tego
wzorca wymaga użycia deklaracji klas. Klasa
bazowa hierarchii odwiedzanej (klasa Unit )
jest zależna od deklaracji klasy bazowej hie-
rarchii klas wizytujących (klasa Visitor ), po-
nieważ zawiera deklarację metody accept .
Abstrakcyjny wizytator jest zależny od de-
klaracji wszystkich klas konkretnych w hie-
rarchii odwiedzanej, a więc od klas Infantry ,
Tank , Rocket . Klasy konkretne w hierarchii
odwiedzanej zależne są od klasy bazowej,
dziedziczą po niej. Wprowadzając dodatko-
we klasy i wykorzystując dziedziczenie wie-
lobazowe, możemy pozbyć się zależności cy-
klicznych w tym wzorcu. Taką implementa-
cję opisano w książce „Średnio zaawansowane
programowanie w C++” (patrz ramka).
Wizytator dostarcza mechanizmu równo-
ważnego dodawaniu metod do klas. Dostar-
cza on mechanizmu wyboru odpowiedniej
metody w zależności od dwóch typów, jed-
nym jest typ obiektu odwiedzanego, dru-
gim typ wizytatora. Rozszerzeniem tej tech-
niki są wielometody, które będą omówione
w jednym z kolejnych artykułów.
class Statistics : public Visitor { //wizytator konkretny
public :
virtual void visit ( Infantry & u ) {//wołane dla jednostek piechoty
soldiers_ += u . countSoldiers ();
}
virtual void visit ( Tank & t ) {//wołane dla czołgów
++ tanks_ ;
}
/* ... */
};
Listing 6. Przykład użycia wizytatora
Unit * u = new Tank (); //dostarcza jednostkę
Statistics s ; //tworzy obiekt statystyk
s . accept ( u ); // dwukrotnie wykorzystuje funkcje wirtualne
Listing 7. Wariant, przykłady użycia
// obiekt , kt ó ry mo ż e przechowywa ć jeden z trzech typ ó w
variant < int , double , std :: string > var ;//obiekt przechowujący int
var = "Hej" ;//teraz przechowuje napis
var = 2.7 ; //teraz przechowuje wartość typu double
//przykładowa deklaracja funkcji, która ma argument typu variant
void function ( const variant < int , double , std :: string >& v );
function ( var ); // przekazuje jako argument do funkcji
Listing 8. Dostęp do wartości przechowywanych w wariancie
typedef variant < int , double , string > Var ;
class MyVisitor //dostęp za pomocą wizytatora
: public boost :: static_visitor < void > { //metody visit zwracają void
public :
void operator ()( int & i ) { /* metoda dla obiektu typu int */ }
void operator ()( double & d ) { /* ... */ }
void operator ()( string & s ) { /* ... */ }
} ;
apply_visitor ( MyVisitor , var ); // wizytacja obiektu var
Więcej w książce
Zagadnienia dotyczące współcześnie stosowanych technik w języku C++, wzorce projekto-
we, programowanie generyczne, prawidłowe zarządzanie zasobami przy stosowaniu wy-
jątków, programowanie wielowątkowe, ilustrowane przykładami stosowanymi w bibliotece
standardowej i bibliotekach boost, zostały opisane w książce ,,Średnio zaawansowane pro-
gramowanie w C++'', która ukaże się niebawem.
W Sieci
ROBERT NOWAK
Adiunkt w Zakładzie Sztucznej Inteligencji Insty-
tutu Systemów Elektronicznych Politechniki War-
szawskiej, zainteresowany tworzeniem aplikacji
bioinformatycznych oraz zarządzania ryzykiem.
Programuje w C++ od ponad 10 lat.
Kontakt z autorem: rno@o2.pl
http://www.boost.org – dokumentacja bibliotek boost;
http://www.open-std.org – dokumenty opisujące nowy standard C++.
18
04/2010
441718554.020.png 441718554.021.png 441718554.022.png
 
Zgłoś jeśli naruszono regulamin