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 public virtual IDelegate class O, class T, typename
Arg5> RetVal, typename Arg1,
{ typename Arg2,...>
public: O:
typedef typename ICallable HandleType delegate_cast
::MethodType::V methodType; (T* obj, RetVal
DelegateImpl (T* obj, methodType method) (T::*methodType)
: obj_(obj) , method_(method) {} (Arg1, Arg2,...))
RetVal call(Arg1 arg1, Arg2 arg2, ...) const {
{ return O return (obj_->*method_)(arg1, arg2, ...); Arg2>::HandleType
}
protected: (new O
methodType method_; DelegateImlp
T *obj_; (obj, methodType));
}; }
38
www.sdjournal.org Software Developer s Journal 04/2007
Ewolucja wzorca polimorfizmu zewnętrznego w C++
Listing 12. Implementacja przykładu przy użyciu Listing 13. Prosta kontrolka ProgressBar
Delegatów
class ProgressBar
class Button {
{ public:
public: void setProgress(int position)
Delegate eventPush; {
// function used by std::cout << "PROGRESS:"
Framework to generate event << position << std::endl;
void simulatePush() }
{ };
eventPush();
}
};
Server Klient
void main(char* args[],int argc)
{
Mp3Player aMp3Player;
request=onRequest
Button playButton;
Button stopButton;
playButton.eventPush=delegate
_cast(&aMp3Player
request()
,&Mp3Player::play);
stopButton.eventPush=delegate
_cast(&aMp3Player
,&Mp3Player::stop);
// symulacja wcisniecia klawiszy
response()
playButton.simulatePush();
stopButton.simulatePush();
}
i część wymaganą. Interfejs korzystający z delegatów wy-
maga od swojego użytkownika, aby ten się do nich odpo-
wiednio przypiął, tworząc swoistego rodzaju wtyczkę na in-
terfejs innego komponentu. Co więcej - komponent z taką
Rysunek 6. Scenariusz interakcji Klient-Serwer w przypadku
wtyczką nie może działać poprawnie, jeśli nikt się do owej
Delegata
abstrakcyjny interfejs delegata delegat
RetVal, Arg1, Arg2, Arg3, ... RetVal, Arg1, Arg2, Arg3, ...
IDelegate Delegate
+RetVal call() +RetVal operator (Arg1, Arg2, Arg3,...)()
+bool isEqual() +bool operator==()
+clone() +operator=()
T, RetVal, Arg1, Arg2, Arg3, ...
DelegateImpl
-T* obj_
-RetVal (T::*mth_) (Arg1, Arg2, Arg3, ...)
+RetVal()
automatycznie generowana implementacja
+bool isEqua;()
+clone()
Rysunek 5. Schemat Delegata
Software Developer s Journal 04/2007 www.sdjournal.org
39
run
onRequest
run
Inżynieria
oprogramowania
Listing 15. Element cyklicznej listy dwukierunkowej
for all o in observers
o->update()
template
class DLListNode
Subject
{
<>
public:
- observer
Observer
+ Attach()
DLListNode() :
+ Detach() + Update()
*
currentPtr_(0)
+ Notify()
{
next_=prev_=this;
}
ConcreteSubject
// automatyczne wypiecie z listy
ConcreteObserver
- subjectState
virtual ~DLListNode()
- observerState
- subject
+ SetState()
{
+ Update()
+ GetState()
if(currentPtr_!=0 && *
currentPtr_==this) *currentPtr_=prev_;
prev_->next_=next_;
return observerState=
subjectState subject->GetState
next_->prev_=prev_;
}
//wstawienie pomiedzy
Rysunek 7. Wzorzec Observer
nastepnego a poprzedniego
wtyczki nie przypnie. Obiekt, którego interfejs posiada taką void insert(DLListNode
wtyczkę nazwijmy Serwerem, a obiekt używający tej wtycz- * elem,DLListNode** currentPtr)
ki Klientem. {
Rozluznienie powiązań miedzyobiektowych, które moż- elem->prev_=this;
na uzyskać za jej pomocą pozwala na całkowite uniezależnie- elem->next_=next_;
elem->prev_->next_=elem;
elem->next_->prev_=elem;
Listing 14. Zdarzenie o zmianie pozycji w bieżącym
utworze zaimplementowane jako Delegat elem->currentPtr_ = currentPtr;
}
#include private:
#include template
class Mp3Player friend class DLList;
{ DLListNode* next_;
public: //nastepny element w liscie
Delegate DLListNode* prev_;
eventProgress; //poprzedni element w liscie
// symulacja zmiany DLListNode** currentPtr_;
pozycji biezacego utworu //wskaznik na bieżąco iterowany element
void simulateProgress() };
{
std::vector positions; nie Klienta od Serwera i vice-versa. Spięcie obu komponen-
for(int i=0;i<=100;i+=10) tów może odbywać się na zewnątrz, w części konfiguracyjnej
positions.push_back(i); systemu.
std::for_each(positions.begin(),
positions.end(),eventProgress); Rozgłaszanie zdarzeń
} Podstawowy model interakcji pomiędzy Klientem a Serwerem
}; możemy opisać następująco:
void main(char* args[],int argc)
{ " Klient jest przypinany do odpowiedniego Delegata wysta-
Mp3Player aMp3Player; wionego przez Serwer;
ProgressBar progressBar; " Serwer Odpala Delegata;
aMp3Player.eventProgress= " Delegat przekazuje wywołanie do metody po stronie Klienta;
delegate_cast " Metoda Klienta jest uruchomiona synchronicznie w wątku
(&progressBar,&ProgressBar wołającego i po wykonaniu zwraca wartość poprzez Dele-
:setProgress); gata do Serwera.
aMp3Player.simulateProgress();
} W powyższym scenariuszu daje się zauważyć silne powią-
zanie jeden-do-jednego pomiędzy Klientem a Serwerem.
40
www.sdjournal.org Software Developer s Journal 04/2007
Ewolucja wzorca polimorfizmu zewnętrznego w C++
Listing 16. Cykliczna lista dwukierunkowa
Server Klient A Klient B Klient C
templaterequest=onRequest T=DLListNode >
class DLList : public
request=onRequest
DLListNode
request=onRequest
{
public:
request()
// poczatkowy element iteracji
void start() const {current_=next_;}
//nastepny element iteracji
void next() const
{current_=current_->next_;}
//biezacy element iterowany
request()
T* current() const {return static_
cast(current_==this?0 : current_);}
request=onRequest
//wstawienie elementu do listy
void insert(DLListNode* elem)
{
DLListNode::insert(elem,¤t_);
request()
}
//usuniecie elementu z listy
void remove (T* source)
{
for(DLListNode* x=next_;
x!=this; x=x->next_)
{
if(static_cast(x)
->isEqual(*source))
{
delete x;
}
Rysunek 8. Scenariusz Rozgłoszenia.
//wyczyszczenie zawartosci listy
Wymaga ono niejawnie, aby Klient był przypięty do Serwe- void clear(){while
ra zanim Serwer odpali Delegata. Z drugiej strony Klient mo- (next_!=this) delete next_;}
że zwracać rezultat działania metody do Serwera pozwalając //w destruktorze wyczysc zawartosc
w ten sposób na przekazanie komunikatu w kierunku przeciw- virtual ~DLList(){clear();}
nym od Klienta do Serwera. private:
Taki scenariusz nie odbiega w dużym stopniu od zwykłego mutable DLListNode* current_;
wywołania metody obiektu Klienta, więc będziemy go nazy- };
wać w dalszej części Wywołaniem Delegata w odróżnieniu od
DLList
DLListNode DLListNode DLListNode DLListNode
next next next next
prev prev prev prev
currentPtr currentPtr currentPtr currentPtr
current
Rysunek 9. Cykliczna lista dwukierunkowa
Software Developer s Journal 04/2007 www.sdjournal.org
41
onRequest
onRequest
for each
onRequest
Inżynieria
oprogramowania
scenariusza nazwanego Rozgłaszaniem, które realizuje po-
Listing 17. Interfejs Delegacji Rozgłoszenia
wiązanie Server-Klient typu jeden-do-wielu.
Rozgłaszanie to, inaczej mówiąc, wysyłanie komunika-
template typename Arg1, typename żadnego), którzy zarejestrowali się na owe zdarzenie. W przy-
Arg2,... > padku Rozgłoszenia nie mamy możliwości zwrócenia jedno-
class IeventDelegate znacznej wartości gdyż zależy ona od ilości zarejestrowanych
: public DLListNode<0> klientów.
, public Icallable Arg1,Arg2,...> wym Obserwator (ang. Observer). Wzorzec Obserwator znaj-
{ duje zastosowanie w sytuacji, gdy jeden obiekt powinien mieć
public: możliwość powiadamiania innych obiektów, o których egzy-
virtual bool isEqual(const stencji nie chce zakładać zbyt wiele, w tym nie interesuje go
IeventDelegate Arg2,...>& h) const = 0; wowe hierarchie klas. Hierarchię obserwowaną i hierarchię ob-
virtual IeventDelegate* serwującą. Hierarchia obserwowana ma korzeń w postaci kla-
clone() const = 0; sy Subject, która to umożliwia dopinanie się dowolnej ilości ob-
}; serwatorów implementujących interfejs Observer. Taka konkret-
na klasa obserwatora z kolei może w odpowiedzi na notyfikację
DLList DLList
DLListNode DLListNode
next next
Association
prev prev
DLListNode DLListNode
currentPtr currentPtr
next next
current current
prev prev
currentPtr currentPtr
DLList
Association
DLListNode
DLListNode DLListNode
next
next next
prev
prev prev
currentPtr
currentPtr currentPtr
current
Association
DLListNode DLListNode
next next
prev prev
currentPtr currentPtr
Rysunek 10. Asocjacje międzyobiektowe
42
www.sdjournal.org Software Developer s Journal 04/2007
Ewolucja wzorca polimorfizmu zewnętrznego w C++
lacje jeden-do-wielu dawałoby w rezultacie jak się można
Listing 18. Implementacja Delegacji Rozgłoszenia
spodziewać wzorzec Rozgłoszenia. Niestety naiwna imple-
mentacja Rozgłoszenia jako kolekcji delegatów, najeżona jest
template typename Arg1, typename Arg2,... > rzy w odpowiedzi na wywołanie mogą ową kolekcję zmodyfi -
class EventDelegateImpl : kować, mamy więc do wyboru albo kosztem wydajności
public virtual IeventDelegate przed samą iteracją skopiować kolekcję delegatów a iterację
Arg4, Arg5> ną kolekcję , która zagwarantuje sama swoją spójność w cza-
{ sie iteracji.
public:
typedef typename Icallable Listing 20. Rozgłoszenie
MethodType::V methodType; template EventDelegateImpl typename Arg1, typename Arg2,...>
(T* obj, methodType method) class Event
: obj_(obj) , method_(method) {} : public DLList<0, IeventDelegate
RetVal call(Arg1 arg1, Arg2 >
arg2, ...) const {
return (obj_->*method_) private:
(arg1, arg2, ...); template
} struct Tsel {};
protected: template
methodType method_; R callMethod(TSel,
T *obj_; Arg1 arg1, Arg2 arg2,...)
}; {
assert(!empty());
return elem()->call(arg1, arg2);
o zmianie ze strony klasy konkretnej klasy obserwowanej Con- }
creteSubject zapytać o jej bieżący stan i na tej podstawie zak- template
tualizować lokalne informacje na temat klasy obserwowanej. void callMethod(TSel
Patrząc z boku na wzorzec Obserwator zauważamy duże ,Arg1 arg1, Arg2 arg2,...)
podobieństwo w jego strukturze do wzorca Adapter, z tą jed- {
nak różnicą, że o ile Adapter stowarzysza obserwatora i ob- for(start();; next())
serwowanego w stosunku jedne-do-jednego, to Obserwator {
pozwala na skojarzenie jeden-do-wielu. Rozszerzenie kon- if(current()==0) break;
cepcji Delegata w taki sposób, aby można było łączyć je w re- current()->call(arg1, arg2);
}
}
Listing 19. Delegat Rozgłoszenia
public:
template typename Arg1, typename Arg2,...> method)
class EventDelegate {
{ IEventDelegate
public: * clone=method.get()->clone();
explicit EventDelegate insert(clone);
(IeventDelegate Arg2,Arg3,Arg4,Arg5>* ptr=0) method)
: ptr_(ptr){} {
RetVal operator()(Arg1 arg1, remove(method.get());
Arg2 arg2) const }
{ RetVal operator()(Arg1 arg1,
return ptr_->call(arg1, arg2); Arg2 arg2,...)
} {
private: return callMethod
IeventDelegate(),arg1, arg2,...);
Arg2,Arg3,Arg4,Arg5>* ptr_; }
}; };
Software Developer s Journal 04/2007 www.sdjournal.org
43
Inżynieria
oprogramowania
Listing 21. Implementacja przykładu przy użyciu Listing 22. Konfi guracja rozgłoszeń w przykładzie
Rozgłoszeń
void main(char* args[],int argc)
class Button {
{ std::auto_ptr
public: aMp3Player(new Mp3Player);
Event eventPush; Button playButton;
// function used by Button stopButton;
Framework to generate event ProgressBar progressBar;
void simulatePush() playButton.eventPush+=delegate
{ cast(aMp3Player.get(),
std::cout << " &Mp3Player::play);
PUSHING BUTTON" << std::endl; stopButton.eventPush+=delegate
eventPush(); cast(aMp3Player.get(),
} &Mp3Player::stop);
}; aMp3Player->eventProgress
class Mp3Player +=delegate_cast
{ (&progressBar,&ProgressBar
public: setProgress);
virtual ~Mp3Player() // done by framework
{std::cout << "MP3Player IS playButton.simulatePush();
DEAD" << std::endl;} stopButton.simulatePush();
void play() {std::cout << aMp3Player->simulateProgress();
"PLAY" << std::endl;} playButton.eventPush-=delegate
void stop() {std::cout << cast(aMp3Player.get(),
"STOP" << std::endl;} &Mp3Player::play);
Event eventProgress; // kill mp3 player
void simulateProgress() aMp3Player.reset();
{ // done by framework
std::vector positions; playButton.simulatePush(); // fail
for(int i=0;i<=100;i+=10) stopButton.simulatePush();
positions.push_back(i); }
std::for_each(positions.begin(),
positions.end(),eventProgress); Podobnie jak klasa EventDelegate, zarządzająca życiem
} delegata i wyposażająca go w operator wywołania funkcji (Li-
}; sting 19).
Przejdzmy teraz do klasy samego Rozgłoszenia odziedzi-
czonej po sprytnej kolekcji. Przedstawia to Listing 20.
Sprytna kolekcja ma swoje początki w implementacji
sprytnego-wskaznika opartego o cykliczną listę dwukierun-
Listing 23. Interfejs Delegacji Automatycznego
kową [Alex01]. Elementy tej kolekcji w trakcie niszczenia
Rozgłoszenia
w czasie wywoływania destruktorów potrafi ą się automa-
tycznie z listy wypiąć. Co więcej, taka sprytna kolekcja po- templatetrafi również odpowiednio skorygować iterator przesuwa- typename Arg1, typename Arg2,... >
jąc go w razie potrzeby na odpowiednią prawidłową pozy- class IAutoEventDelegate
cję. Kolekcja, na której się skupimy będzie bazowała na ta- : public DLListNode<0>
kim rozwiązaniu. , public DLListNode<1>
Przejdzmy do implementacji. Element takiej listy zadekla- , public Icallablerujemy jako szablon. Nietypowy parametr tego szablonu przy- Arg1,Arg2,...>
da nam się pózniej (Listing 15). {
Sama lista dwukierunkowa. Przedstawia ją Listing 16. public:
Dysponując taką listą możemy przejść do implementa- virtual bool isEqual(const
cji generycznego Obserwatora czy inaczej mówiąc Rozgło- IautoEventDelegateszenia. Abstrakcyjny interfejs Delegata, który może się pod- Arg1,Arg2,...>& h) const = 0;
piąć do rozgłoszenia odziedziczymy po elemencie listy, a co virtual IautoEventDelegate
za tym idzie będziemy mogli takie Delegaty dodawać do sa- * clone() const = 0;
mej listy. Patrz Listing 17. virtual void connect() = 0;
Implementacja z kolei jest identyczna z implementacją De- };
legata. Przedstawia to Listing 18.
44
www.sdjournal.org Software Developer s Journal 04/2007
Inżynieria
oprogramowania
Listing 24. Implementacja Delegacji Automatycznego Listing 25. Przykład przy wykorzystaniu Automatycznych
Rozgłoszenia Rozgłoszeń
class AutoEventDelegateImpl template : public IautoEventDelegate typename Arg1, typename Arg2,...>
class AutoEvent
{ : public DLList<0, Iauto
public: EventDelegate >
virtual void connect() {
{ public:
obj->DLListNode<1> void operator += (Auto
insert(this,0); EventDelegate } Arg1, Arg2,...> method)
}; {
IautoEventDelegateKomentarza wymaga zastosowana tu sztuczka, zwana ty- Arg1, Arg2,...>* clone=
pe-selection. Pozwala ona na symulacje przeciążania szablo- method.get()->clone();
nów metod i szerzej jest opisana w pracy [Alex01]. Dzięki niej clone->connect();
otrzymujemy specjalizację dla przypadku, gdy interesuje nas insert(clone);
wartość zwracana i dla przypadku, gdy takiej wartości nie po- }
trzebujemy. void operator -= (AutoEvent
Wróćmy do naszego przykładu z klasą Mp3Player i jej inter- Delegatefejsem użytkownika. Arg2,...> method)
Przy użyciu Rozgłaszania możemy ten problem zaimple- {
mentować tak jak przedstawia to Listing 21. remove(method.get());
A część konfi guracyjną przedstawia Listing 22. }
Niestety ten przykład powoduje błąd czasu wykonywania. RetVal operator()(Arg1 arg1,
Mp3Player jest niszczony przed odpisaniem się od przycisku Arg2 arg2,...)
stop. Symulacja wciśnięcia tego przycisku powoduje odwoła- {
nie się do nieistniejącego obiektu. Aby temu zapobiec musi- return callMethod
my zautomatyzować proces odpinania się obserwatorów od (TSel(),arg1, arg2,...);
obiektu obserwowanego. Jesteśmy już na to gotowi dysponu- }
jąc powyższa implementacją listy dwukierunkowej. };
Automatyczne wyrejestrowywanie Interfejs dziedziczy po dwóch typach elementów listy dwu-
się w czasie destrukcji obiektu kierunkowej, ponadto dodaje wirtualną metodę connect, któ-
Każdy Delegat wiążący obserwatora z obiektem obserwo- ra w implementacji wpina odpowiedni element do drugiej listy
wanym jest swoistą relacją i powinien być związany z dwoma związanej z obiektem obserwatora (Listing 24).
obiektami jednocześnie. W momencie śmierci jednego z nich Ostatnim etapem jest stworzenie klasy automatycznego
cała relacja powinna być niszczona powodując automatyczne generycznego Obserwatora. Patrz Listing 25.
wyrejestrowanie się obserwatora z obiektu obserwowanego Podmieniając w naszym przykładzie klasę Event na Auto-
(Rysunek 10 i Listing 23). Event, nie doświadczamy już błędu czasu wykonania, gdyż
Tabela 1. Porównanie funkcjonalne narzędzi wspierających wzorzec polimorfizmu zewnętrznego
Cecha Delegat Event z wartością Event bez wartości zwracanej
zwracaną
Separacja interfejsów tak tak Tak
Bezpieczeństwo w przypadku nie Tak Tak
śmierci Klienta lub Serwera
Relacja Klient-Serwer 1-1 1-1 1-N
Wartość zwracana tak tak Nie
Koszt a) virtual call a) virtual call a) for-each virtual call
b) pointer to method b) pointer to method b) for-each pointer to method
c) memory allocation c) association extraction c) for-each association extraction
for method binder d) memory allocation for d) for-each memory allocation for me-
method binder thod binder
e) memory allocation for e) for-each memory allocation per as-
association sociation
46
www.sdjournal.org Software Developer s Journal 04/2007
Ewolucja wzorca polimorfizmu zewnętrznego w C++
klasa Mp3Player będąc niszczona automatycznie wyrejestruje kowników tego komponentu. Z kolei z drugiej strony kompo-
wszystkich przypiętych do siebie obserwatorów w tym przy- nent zgłaszający jakieś zgłoszenie może działać w jeszcze in-
padku przycisk Stop. nym wątku. W przypadku użycia Delegatów, czy Rozgłoszeń
musimy o tym pamiętać.
Problemy w aplikacjach Po pierwsze do Delegata lub Rozgłoszenie użytkownik
wielowątkowych może się przypiąć z innego wątku niż wątek w którym Dele-
Przedstawione tu mechanizmy zakładały, że cała rzecz od- gat lub Rozgłoszenie działa, ponadto Rozgłoszenie lub Dele-
bywa się w kontekście pojedynczego wątku. Powstaje pyta- gat może się odpalać się w kontekście wątku innego niż wą-
nie: jaki wpływ ma próba użycia owych mechanizmów w apli- tek użytkownika komponentu. Musimy więc chronić w jakiś
kacjach wielowątkowych? Aby odpowiedzieć sobie na to pyta- sposób zarówno proces przypinania/odpinania się od Dele-
nie musimy spojrzeć jeszcze raz na interfejs komponentu ja- gata czy Rozgłoszenia, jak również pilnować tego aby w trak-
ko na twór składający się z części udostępnianej i wymaga- cie odpalania Delegata lub Rozgłoszenia inne wątki zaczeka-
nej. Część udostępniana komponentu może być zaprojekto- ły z przypinaniem/odpinaniem. Owe zabezpieczenie dość ła-
wana z myślą o bezpiecznym dostępie z wielu wątków użyt- two można zrealizować przy wykorzystaniu wielowątkowe-
go wzorca projektowego o nazwie Monitor opisanego w pra-
cy [Schm00], do której odsyłam zainteresowanych. Pozwolę
sobie tylko zasugerować rozwiązanie opierające się na mute-
Bibliografia
xsie chroniącym cały interfejs udostępniany komponentu.
" [Abra04] David Abrahams, Aleksey Gurtovoy. C++ Template
Metaprogramming. ISBN: 0321227255: Addison-Wesley, 2004;
Podsumowanie
" [Alex01] Andrei Alexandrescu. Modern C++ Design: Ge-
Artykuł ten miał na celu w główniej mierze pokazanie w ja-
neric Programming and Design Patterns Applied. ISBN:
ki sposób można w języku C++ uzyskać jednolity styl archi-
0201704315, Boston, MA: Addison-Wesley, 2001;
tektoniczny bazujący na koncepcji polimorfizmu zewnętrzne-
" [Clee97] Cleeland, Chris, Douglas C. Schmidt, Tim Harrison.
go. Separowalność interfejsów zmniejsza w znacznym stop-
External Polymorphism , PATTERN LANGUAGES OF PRO-
niu zależności pomiędzy obiektami, a więc zmniejsza wiel-
GRAM DESIGN, volume 3, edited by Robert Martin, Frank Bu-
kość szczepienia (ang. coupling) tak wytworzonej struktury,
schmann, and Dirke Riehle:Addison-Wesley, Reading MA, 1997.
" http://www.cs.wustl.edu/~schmidt/PDF/External-Polymor- jednocześnie dając szansę na zwiększenie spójności logicz-
phism.pdf;
nej (ang. cohesion) poszczególnych komponentów, co jest
" [Clug05] Don Clugston. Member Function Pointers and the
celem każdej dobrej architektury systemu informatycznego.
Fastest Possible C++ Delegates. http://www.codeproject.com/
Funktory, którymi są Delegate i Event nie są same w sobie re-
cpp/FastDelegate.asp;
wolucją. Z problematyką polimorfi zmu zewnętrznego zderzy-
" [Gamm95] E. Gamma, R. Helm, R. Johnson, and J. Vlissides.
li się chociażby twórcy modelu COM w firmie Microsoft, chcąc
Design Patterns: Elements of Reusable Object-Oriented So-
ujednolicić zdarzeniowy interfejs komponentów. W najnow-
ftware. Reading, MA: Addison-Wesley, 1995.
szej implementacji zarządzanego C++ są Delegaty są expli-
" [Jaku97] Paul Jakubik. Callback Implementations in C++
cite wymienione jako konstrukcje podstawowe [MSDN], dzia-
" http://www.newty.de/jakubik/callback.pdf, 1997;
łają one jednak na innej zasadzie i są o wiele bardziej zasobo-
" [Lipp96] Stanley B. Lippman. Inside the C++ Object Model.
żerne. Przedstawione tu konstrukcje zainspirowane są praca-
ISBN: 0201834545: Addison-Wesley, 1996;
" [MSDN] Generic Delegates (C++) http://msdn2.microsoft.com/ mi [Clee97], [Clug05] oraz [Jaku97]. Koncepcja cyklicznej listy
en-us/library/213x8e7w.aspx;
dwukierunkowej pochodzi natomiast z pracy Risto Lankinen
" [Schm00] Douglas Schmidt, Michael Stal, Hans Rohnert,
opublikowanej na grupach dyskusyjnych w listopadzie 1995
Frank Buschmann. Pattern-Oriented Software Architecture,
a opisanej szerzej w [Alex01]. Przedstawione tu konstrukcje
Volume 2, Patterns for Concurrent and Networked Objects
zostały zweryfi kowane w rzeczywistych projektach komercyj-
ISBN: 9780471606956: Addison-Wesley, 2000.
nych cechując się dużą wydajnością i zapewniając znaczne
zwiększenie jakości kodu programistycznego.
R E K L A M A
Software Developer s Journal 04/2007 www.sdjournal.org
47
Wyszukiwarka
Podobne podstrony:
2007 06 UML – potrzeba standaryzacji notacji [Inzynieria Oprogramowania]
2006 04 Rozszerzenie wzorca Template [Inzynieria Oprogramowania]
2007 08 UML – modelowanie statycznych aspektów oprogramowania [Inzynieria Oprogramowania]
2007 11 Extreme Programming i CMMI [Inzynieria Oprogramowania]
2007 07 Wykorzystanie przypadków użycia do modelowania zachowania [Inzynieria Oprogramowania]
2007 07 Wykorzystanie przypadków użycia do modelowania zachowania [Inzynieria Oprogramowania]
2007 03 Inspekcje kodu jako skuteczna metoda weryfikacji oprogramowania [Inzynieria Oprogramowania]
2007 11 UML – modelowanie dynamicznych aspektów oprogramowania [Inzynieria Oprogramowania]
2007 05 Mechanizm koncepcji w języku C nowe oblicze szablonów [Inzynieria Oprogramowania]
2007 10 Extreme Programming (XP) i CMMI – Kreatywność, czy Dyscyplina [Inzynieria Oprogramowania]
2006 06 Wstęp do Scrum [Inzynieria Oprogramowania]
Inżynieria oprogramowania II
2006 09 Wielozadaniowość w systemach operacyjnych [Inzynieria Oprogramowania]
2007 04 Nowoczesna metoda oceny rehabilitacji u pacjentów po endoprototezoplastyce st biodrowego
Inżynieria oprogramowania
2006 03 XFire w akcji [Inzynieria Oprogramowania]
2007 04 Rehabilitacja po europejsku
2007 04 Drawing Set Graph Visualization with Graphviz
więcej podobnych podstron