2007 04 Ewolucja wzorca polimorfizmu zewnętrznego w C [Inzynieria Oprogramowania]


Inżynieria
oprogramowania
Ewolucja wzorca polimorfizmu
zewnętrznego w C++
Paweł Kapłański
ęzyk pretendując do określania się mianem ję- jektowy Adapter opisany przez  gang czterech , pio-
zyka obiektowego winien wspierać paradyg- nierów w dziedzinie wzorców projektowych w inży-
Jmaty obiektowości  w tym polimorfizmu. W ję- nierii oprogramowania [Gamm95]. W tym artykule
zyku C++ owo wsparcie mamy zapewnione w dwóch zostanie przedstawiony wzorzec Polimorfizmu Ze-
odmianach: statycznej (czasu kompilacji) i dynamicz- wnętrznego, jego ewolucja i odmiany.
nej (czasu wykonania). Problem wytworzenia interfejsu użytkownika dla
Statyczna odmiana polimorfi zmu dostępna po- programu odgrywającego muzykę jest na tyle czy-
przez mechanizm przeciążania funkcji i operatorów telny i intuicyjny, że wydaje się ręcz idealnym przy-
ze względu na typy argumentów, pozwala na trakto- kładem do demonstracji wzorca Polimorfizmu Ze-
wanie symbolu funkcji jako obiektu polimorficznego. wnętrznego. Otóż: dysponując klasą MP3Player, któ-
Mechanizm ten jest rozszerzony na pozostałe ele- ra udostępnia funkcjonalność odgrywania i zatrzy-
menty języka dzięki możliwościom, które daje pro- mywania bieżącego utworu za pomocą metod play
gramiście system szablonów. Moc polimorfizmu sta- i stop, stajemy przed problemem podłączenia ele-
tycznego pokazuje programowanie generyczne sto- mentów grafi cznego interfejsu użytkownika składają-
sowane w bibliotece standardowej STL, które łączy cego się z dwóch przycisków.
szybkość i optymalność z dobrodziejstwem technik Każdy z przycisków w odpowiedzi na interakcję
obiektowych, właśnie dzięki zastosowaniu polimorfi - użytkownika generuje zdarzenie, które należy ob-
zmu statycznego. służyć poprzez wywołanie odpowiedniej metody kla-
Na przełomie 1994 i 1995 niemalże przez przy- sy Mp3Player.
padek odkryto, że system szablonów języka C++ jest W pierwszym podejściu do tego problemu zasto-
kompletny w rozumieniu Turinga, a co za tym idzie po- sujemy wzorzec Adapter.
zwala na zapis dowolnego algorytmu w tym: rozwią- Wzorzec Adapter [Gamm95] opisuje sposób ada-
zywanie rekursywnych warunków, generowanie pro- ptacji interfejsu klasy Adaptowanej (ang. Adaptee) 
gramów w czasie kompilacji itp. Ta własność syste- do postaci akceptowalnej przez interfejs klasy Klienc-
mu szablonów doprowadziła do burzliwego rozwo- kiej (ang. Client). Pozwala na współpracę klas, które
ju dziedziny o nazwie metaprogramowanie za pomo- nie mogą ze sobą współpracować bezpośrednio ze
cą szablonów (ang. template mataprogramming). Do- względu na wzajemną niekompatybilność interfejsów.
głębny opis tej problematyki można znalezć w książce Scenariusz, na którym wzorzec Adapter opiera
[Abra04], którą polecam wszystkim zainteresowanym. swoje działanie, można streścić następująco: obiekt
Polimorfi zm dynamiczny w języku C++ wiąże się klasy Klienckiej wywołuje operację na obiekcie Ada-
z jednej strony z możliwościami odwołania się do kla- ptera, poprzez interfejs Celu (ang. Target) pozwalają-
sy potomnej poprzez interfejs klasy bazowej za po- cy na takowe wywołanie. Wewnętrzna implementa-
mocą mechanizmu RTTI (ang. Run-Time-Type -In- cja Adaptera oddelegowuje z kolei wywołanie do kla-
formation), a z drugiej strony z mechanizmem metod sy adaptowanej (ang. Adaptee).
wirtualnych realizowanych przez tablicę vtable.. Do- Wewnątrz wzorca Adapter zawarte jest niejawne
głębne omówienie tego tematu znajduje się w pozy- założenie, iż interfejsy zarówno obiektu Klienta jak
cji [Lipp96] napisanej przez jednego z twórców języ- i Celu możemy podzielić na dwie części  udostęp-
ka C++. nianą (ang. provided) i wymaganą (ang. required). To
W pracy [Clee97] w 1997 roku zaprezentowano założenie okazuje się również prawdziwe dla każ-
wzorzec projektowy o nazwie Polimorfizm Zewnętrz- dego dobrze zdefi niowanego interfejsu komponen-
ny (ang. External Polymorphism), którego głównym tu. Część udostępniana  interfejs udostępniany jest
zadaniem było umożliwienie klasom w żaden sposób postrzegana jako główna część interfejsu kompo-
nie skojarzonych ze sobą na wzajemną współpracę nentu, pozwala na dostęp do podstawowych funk-
poprzez zautomatyzowanie procesu adoptowania in- cjonalności udostępnianych przez komponent. Dru-
terfejsów do siebie. Automatyzował on wzorzec pro- ga część  interfejs wymagany  jest zbiorem zało-
żeń, które komponent mniej lub bardziej jawnie robi
na rzecz użytkownika (ang. user) swoich funkcjonal-
Autor jest architektem wiodącym w korporacji między-
ności. Komponent może np.: wymagać stworzenia
narodowej zajmującej się m.in. systemami wbudowany-
i/lub zniszczenia swojej instancji w środowisku, dla
mi (ang. embedded).
którego został zaprojektowany, może wymagać by
Kontakt z autorem: pawel.kaplanski@wp.pl
komponenty używające udostępnionych przez niego
Kody żródłowe: http://www.digital-advanced.com
funkcjonalności  użytkownicy danego komponentu -
34
www.sdjournal.org Software Developer s Journal 04/2007
Ewolucja wzorca polimorfizmu zewnętrznego w C++
Client <>
Target
+Request()
Adapter Adaptee
+Request() +SpecificRequest()
1
?
adaptee->SpecificRequest()
Mp3Player
Rysunek 2. Schemat wzorca projektowego Adapter
+play()
+stop()
Obiekty klasy Button wymagają, by podać w ich konstruk-
torze odpowiednią implementację interfejsu ButtonClient, któ-
ra to udostępni implementację wirtualnej metody onPush wy-
Rysunek 1. Rozważany problem wytworzenia interfejsu
woływanej przez Button w reakcji GUI na interakcję z użyt-
użytkownika
kownikiem. Implementacja ta jest konkretną implementacją in-
wykonywali metody części udostępnionej interfejsu w określo- terfejsu wymaganego komponentu Button-ButtonClient.
nej kolejności. Co więcej, może wymagać tego, aby użytkow- Aby mieć możliwość przeprowadzenia symulacji działa-
nicy potrafili w określony sposób reagować na generowane nia naszego rozwiązania, wyposażmy dodatkowo klasę Button
przez niego zdarzenia. w funkcję Button::simulatePush, która umożliwi zasymulowa-
W przypadku wzorca Adapter zdarzeniowa część interfej- nie wciśnięcia przycisku poprzez odwołanie się do zdarzenio-
su wymaganego komponentu klienta realizowana jest za po- wej części interfejsu wymaganego  interfejsu Celu  w tym
mocą interfejsu Celu. Możemy na ową parę Klient-Cel patrzeć przypadku abstrakcyjnej klasy ButtonClient.
jako na interfejs dualny pojedynczego komponentu. Wyrazny Klasą Adaptowaną (ang. Adaptee) w naszym przypadku
podział na część funkcjonalną i zdarzeniową implikuje imple- jest klasa Mp3Player, której implementacja składa się z dwóch
mentację za pomocą konkretnej klasy Adaptera, zdarzeniowej metod  play i stop (Listing 2).
części interfejsu wymaganego. Pozostaje nam jedynie implementacja Adapterów, które
Powróćmy do analizowanego przykładu. Zastanówmy się to używając interfejsu Celu wywołają odpowiednie metody na
nad użyciem wzorca Adapter do rozwiązania przedstawio- obiekcie klasy Adaptowanej (Listing 3).
nego tam problemu. Załóżmy, że dysponujemy klasą Button Zwróćmy uwagę, że implementacja Adaptera dla przyci-
oraz ButtonClient, które to pełnią rolę owej pary interfejsów sku Stop różni się od implementacji adaptera przycisku Play
pojedynczego komponentu. Identyfikujemy Button z Klientem jedynie implementacją metody interfejsowej ButtonClient::on-
a ButtonClient z interfejsem Celu (Listing 1). Push. (Listing 4).
Uwieńczmy nasze rozwiązanie małym programem testu-
jącym, konfi gurującym poszczególne elementy rozwiązania
Listing 1. Elementy interfejsu komponentu przycisku
i spinającym je w całość (listing 5).
class ButtonClient
{ Polimorfizm zewnętrzny
public: Jak wyżej wspomniano, implementacja Adapterów obu przy-
virtual void onPush() = 0; cisków jest analogiczna, wydaje się więc naturalne, aby w pe-
}; wien sposób zautomatyzować ich wytwarzanie. Dokonać tego
class Button możemy przy użyciu systemu szablonów wytwarzając gene-
{ ryczny Adapter, który konkretyzowany z odpowiednim typem
public: i wskaznikiem na metodę może generować automatycznie od-
explicit Button(ButtonClient* client) powiedni Adapter.
: client_(client) {}
...
Listing 2. Klasa Mp3Player
//funkcja pozwalajaca na symulacje wcisniecia przycisku
void simulatePush() #include
{ class Mp3Player
client_->onPush(); {
} public:
private: void play() {std::cout << "PLAY" << std::endl;}
ButtonClient* client_; void stop() {std::cout << "STOP" << std::endl;}
}; };
Software Developer s Journal 04/2007 www.sdjournal.org
35
Inżynieria
oprogramowania
Listing 3. Implementacja adaptera dla przycisku Play Listing 5. Program testujący
class Mp3PlayerPlayButtonAdapter void main(char* args[], int argc)
: public ButtonClient {
{ Mp3Player aMp3Player;
public: Mp3PlayerPlayButtonAdapter playButtonClient(&aMp3Player);
explicit Mp3PlayerPlayButtonAdapter(Mp3Player* player) Mp3PlayerStopButtonAdapter stopButtonClient(&aMp3Player);
: player_(player) {} Button playButton(&playButtonClient);
virtual void onPush() Button stopButton(&stopButtonClient);
{ // symulacja wciskania przyciskow
player_->play(); playButton.simulatePush();
} stopButton.simulatePush();
private: }
Mp3Player* player_;
}; nie również interfejsu Celu. Takowy Generyczny Adapter nie-
jako delegowałby wywołanie metody w kontekście pewnego
Tak zautomatyzowane podejście jest podstawą wzorca obiektu, dlatego nazywamy go Delegatem (Delegate). Delegat
polimorfizmu zewnętrznego. Szablon generycznego Adapte- powinien służyć do adaptowania dowolnych metod  o różnej
ra dla interfejsu ButtonClient konkretyzowany z typem klasy liczbie i typach argumentów - w tym również metod statycz-
Adaptowanej, w konstruktorze pobierze wskaznik na obiekt nych, my jednak zajmiemy się szczegółowo przypadkiem me-
Adaptowany oraz wskaznik na metodę. Wskaznik na ową me- tod niestatycznych.
todę zostanie użyty w automatycznie wygenerowanej imple- Pracę rozpocznijmy od stworzenia szablonu generyczne-
mentacji metody interfejsu Celu  ButtonClient::onPush, któ- go interfejsu klasy Celu (Listing 8).
ra winna być wołana w odpowiedzi na zdarzenie generowane
przez obiekt klasy Klienckiej  Button (Listing 6).
Listing 6. Generyczny Adapter przycisku
Ta zmiana pociąga za sobą modyfi kację naszego progra-
mu testowego. Główna jego część pozostanie bez zmian, lecz template
nie potrzebujemy już redundantnych implementacji Adapte- class ExternallyPolymorphicButtonClient : public
rów. Konkretyzujemy je dopiero w momencie rzeczywistej po- ButtonClient
trzeby, w naszym przypadku z klasą Mp3Playera, podając od- {
powiednie wskazniki do metod (Listing 7). public:
Generyczny Adapter może być postrzegany jako konstrukt ExternallyPolymorphicButtonClient(T* t,void (T::*mth)())
pozwalający na polimorfi czne używanie klasy Adaptowanej : t_(t),mth_(mth){};
bez ingerencji w samą strukturę owej klasy, stając się swo-
istym pryzmatem, przez który możemy patrzeć na ową klasę. virtual void onPush()
{
Innej mówiąc: możemy uważać taki generyczny Adapter za
rodzaj polimorficznego interfejsu klasy Adaptowanej, który po-
zwala dowolnemu zródłu na używanie tej klasy bez potrzeby (t_->*PtrToMth)();
jej modyfi kacji. Stąd wynika nazwa wzorca projektowego Ze- }
wnętrznego Polimorfizmu. private:
Następnym krokiem w ewolucji wzorca Zewnętrznego Po- T* t_;
limorfizmu jest zautomatyzowanie Generycznego Adaptera do void (T::*mth_)();
tego stopnia, aby jego postać nie była zależna ani od interfej- };
su Adaptowanego, ani od komponentu używającego Adapte-
ra do zgłaszania zdarzeń, lub mówiąc inaczej: ugenerycznie-
Listing 7. Automatycznie generowane Adaptery
przycisków
Listing 4. Implementacja adaptera dla przycisku Stop
class Mp3PlayerStopButtonAdapter void main(char* args[],int argc)
: public ButtonClient {
{ ...
... ExternallyPolymorphicButtonClient playButtonCli
virtual void onPush() ent(&aMp3Player,&Mp3Player::play);
{
player_->stop(); ExternallyPolymorphicButtonClient stopButtonCli
} ent(&aMp3Player,&Mp3Player::stop);
... ...
}; }
36
www.sdjournal.org Software Developer s Journal 04/2007
Ewolucja wzorca polimorfizmu zewnętrznego w C++
Button
<>
-client
ButtonClient
+onPush()
Mp3Player
+play()
+stop()
Mp3PlayerPlayButtonAdapter Mp3PlayerStopButtonAdapter
-player -player
11
+onPush() +onPush()
player->play() player->play()
Rysunek 3. Rozwiązanie przykładowego problemu z pomocą wzorca Adapter
Generyczny Adapter, implementujący interfejs generycz- sność, iż argumenty szablonu nie muszą być podawane jaw-
nego Celu powinien mieć dostęp do klasy adaptowanej, więc nie. Kompilator może je wydedukować z argumentów funkcji.
szablon, za pomocą którego zrealizujemy ów konstrukt będzie Tak, więc zamiast
miał dodatkowy parametr odpowiadający owej Adaptowanej
klasie (Listing 9). int i,j,k;
Sama w sobie klasa Delegata będzie zarządzała życiem k=max(i,j);
generycznego Adaptera pozwalając na jego naturalne używa-
nie poprzez semantykę kopiowania, dodatkowo wyposażając możemy napisać
go w operator wywołania funkcji (Listing 10).
Aby uprościć syntaktycznie użycie delegatów wspomo- int i,j,k;
żemy się mechanizmem automatycznej dedukcji typów ar- k=max(i,j)
gumentów szablonów funkcji. Szablony funkcji mają tą wła-
Kompilator dopasuje sygnatury funkcji do parametrów jej wy-
wołania i automatycznie dokona odpowiedniej konkretyzacji 
w tym przypadku przyjmie jako parametr typ int.
Button <>
-client
ButtonClient Taki narzędziowy szablon funkcji nazwiemy delegate _
cast. Będzie on zwracał delegata wypełnionego odpowiednią
+onPush()
implementacją przyjmując za parametry: szablon, który obsłu-
guje, wskaznik na adaptowany obiekt oraz odpowiednią meto-
T
dę tego obiektu, do której będzie odbywać się delegacja wy-
ExternallyPolymorphicButtonClient
wołania (Listing 11).
-T* t_
(t_->*mth_)()
- void (T::*mth_) ()
Wróćmy do przykładu. Implementacja upraszcza się
+onPush()
w stopniu zaskakującym. Patrz Listing 12.
Zwróćmy uwagę, że nie ma już potrzeby tworzenia kla-
sy interfejsowej ButtonClient. Zamiast niej specyfikujemy
ExternallyPolymorphicButtonClient
miejsce podpięcia się do samej klasy Button poprzez użycie
-mth_=&Mp3Player::play obiektu Delegat w postaci publicznego atrybutu. Co więcej,
nie mamy potrzeby tworzenia odpowiednich Adapterów. Za-
1
Mp3Player
miast tego podpinamy się bezpośrednio do obiektu klasy But-
ton z funkcjonalnościami udostępnianymi przez klasę Mp3Play-
+play()
ExternallyPolymorphicButtonClient
er. Warto zauważyć, że ciało metody Button::simulatePush po-
+stop()
sługuje się zdefi niowanym w klasie Delegate operatorem wy-
-mth_=&Mp3Player::stop
1
wołania funkcji, przez co wygląda jak oddelegowanie wywo-
łania do innej metody, gdy tym czasem w rzeczywistości uru-
chamiają cały mechanizm Delegatów. Takie uruchomienie bę-
Rysunek 4. Generyczny Adapter przycisku
dziemy nazywali Odpaleniem Delegata.
Software Developer s Journal 04/2007 www.sdjournal.org
37
Inżynieria
oprogramowania
Listing 8. Interfejs Celu jako szablon. Listing 10. Implementacja Delegata
template template
class ICallable class Delegate
{ {
public: public:
ICallable(){} explicit Delegate(IDelegate virtual ~ICallable(){} g5>* ptr=0)
virtual RetVal call(Arg1 arg1, Arg2 arg2,...) const = 0; : ptr_(ptr){}
... ...
template struct MethodType RetVal operator()(Arg1 arg1, Arg2 arg2) const
{ {
typedef RetVal (T::*V) (Arg1, Arg2); return ptr_->call(arg1, arg2);
}; }
typedef RetVal (*StaticType) (Arg1, Arg2); private:
}; IDelegate* ptr_;
template };
class IDelegate : public ICallable
{ Aby się o tym przekonać zobaczmy jak wygląda doda-
public: nie funkcjonalności prezentacji bieżącej pozycji odtwarzane-
virtual bool isEqual(const IDelegate go utworu na odtwarzaczu mp3. Dodajmy do zbioru kontrolek
& h) const = 0; klasę ProgressBar (Listing 13).
virtual IDelegate* clone() const = 0; Klasa ta umożliwia wizualizację postępu dowolnej operacji
}; w postaci prostokąta wypełnionego w proporcjonalnym stop-
niu bazując na postępie podanym w metodzie ProgressBar::
Mechanizm metod wirtualnych wyposażając C++ we setProgress.
wsparcie dla polimorfi zmu dynamicznego, pozwolił na wyeli- Wzbogaćmy klasę Mp3Player w dodatkowego Delegata,
minowanie wielu konstrukcji składających się z garści rozga- który odpala się z taką informacją, gdy bieżąca pozycja od-
łęzień logicznych typu if/else if/else lub swich(...){...} po twarzanego utworu ulega zmianie (Listing 14).
znaczniku typu. Spowodowało to znaczne zwiększenie zro- Uważny czytelnik zauważy, że metoda Mp3Player::simu-
zumiałości algorytmów. Mając do dyspozycji Delegaty wypo- lateProgress używa Delegata Mp3Player::eventProgress ja-
sażajmy zestaw naszych narzędzi w polimorfizm zewnętrz- ko funktora podawanego do algorytmu standardowego std::
ny, pozwalając na znaczne rozluznienie wewnętrznych relacji for _ each. Kompatybilność Delegatów ze standardowymi al-
interfejsów miedzyobiektowych. W ten sposób udało nam się gorytmami jest możliwa właśnie dzięki przeciążeniu operato-
wzbogacić język narzędzi podstawowych o bardzo silne na- ra wywołania funkcji, a co za tym idzie konkretny Delegat jest
rzędzie pozwalające na separację interfejsów. funktorem w rozumieniu biblioteki STL.
Powróćmy jeszcze na chwilę do sprawy podziału inter-
fejsu komponentu na dwie części  część udostępnianą
Listing 9. Ogólny generyczny Adapter
templateListing 11. Pomocniczy narzędziowy szablon funkcji
Arg2,... >
class DelegateImpl : template