34
Inżynieria
oprogramowania
www.sdjournal.org
Software Developer’s Journal 04/2007
Ewolucja wzorca polimorfi zmu
zewnętrznego w C++
J
ęzyk pretendując do określania się mianem ję-
zyka obiektowego winien wspierać paradyg-
maty obiektowości – w tym polimorfi zmu. W ję-
zyku C++ owo wsparcie mamy zapewnione w dwóch
odmianach: statycznej (czasu kompilacji) i dynamicz-
nej (czasu wykonania).
Statyczna odmiana polimorfi zmu dostępna po-
przez mechanizm przeciążania funkcji i operatorów
ze względu na typy argumentów, pozwala na trakto-
wanie symbolu funkcji jako obiektu polimorfi cznego.
Mechanizm ten jest rozszerzony na pozostałe ele-
menty języka dzięki możliwościom, które daje pro-
gramiście system szablonów. Moc polimorfi zmu sta-
tycznego pokazuje programowanie generyczne sto-
sowane w bibliotece standardowej STL, które łączy
szybkość i optymalność z dobrodziejstwem technik
obiektowych, właśnie dzięki zastosowaniu polimorfi -
zmu statycznego.
Na przełomie 1994 i 1995 niemalże przez przy-
padek odkryto, że system szablonów języka C++ jest
kompletny w rozumieniu Turinga, a co za tym idzie po-
zwala na zapis dowolnego algorytmu w tym: rozwią-
zywanie rekursywnych warunków, generowanie pro-
gramów w czasie kompilacji itp. Ta własność syste-
mu szablonów doprowadziła do burzliwego rozwo-
ju dziedziny o nazwie metaprogramowanie za pomo-
cą szablonów (ang. template mataprogramming). Do-
głębny opis tej problematyki można znaleźć w książce
[Abra04], którą polecam wszystkim zainteresowanym.
Polimorfi zm dynamiczny w języku C++ wiąże się
z jednej strony z możliwościami odwołania się do kla-
sy potomnej poprzez interfejs klasy bazowej za po-
mocą mechanizmu RTTI (ang. Run-Time-Type -In-
formation), a z drugiej strony z mechanizmem metod
wirtualnych realizowanych przez tablicę vtable.. Do-
głębne omówienie tego tematu znajduje się w pozy-
cji [Lipp96] napisanej przez jednego z twórców języ-
ka C++.
W pracy [Clee97] w 1997 roku zaprezentowano
wzorzec projektowy o nazwie Polimorfi zm Zewnętrz-
ny (ang. External Polymorphism), którego głównym
zadaniem było umożliwienie klasom w żaden sposób
nie skojarzonych ze sobą na wzajemną współpracę
poprzez zautomatyzowanie procesu adoptowania in-
terfejsów do siebie. Automatyzował on wzorzec pro-
jektowy Adapter opisany przez „gang czterech”, pio-
nierów w dziedzinie wzorców projektowych w inży-
nierii oprogramowania [Gamm95]. W tym artykule
zostanie przedstawiony wzorzec Polimorfi zmu Ze-
wnętrznego, jego ewolucja i odmiany.
Problem wytworzenia interfejsu użytkownika dla
programu odgrywającego muzykę jest na tyle czy-
telny i intuicyjny, że wydaje się ręcz idealnym przy-
kładem do demonstracji wzorca Polimorfi zmu Ze-
wnętrznego. Otóż: dysponując klasą
MP3Player
, któ-
ra udostępnia funkcjonalność odgrywania i zatrzy-
mywania bieżącego utworu za pomocą metod
play
i
stop
, stajemy przed problemem podłączenia ele-
mentów grafi cznego interfejsu użytkownika składają-
cego się z dwóch przycisków.
Każdy z przycisków w odpowiedzi na interakcję
użytkownika generuje zdarzenie, które należy ob-
służyć poprzez wywołanie odpowiedniej metody kla-
sy
Mp3Player
.
W pierwszym podejściu do tego problemu zasto-
sujemy wzorzec Adapter.
Wzorzec Adapter [Gamm95] opisuje sposób ada-
ptacji interfejsu klasy Adaptowanej (ang. Adaptee) –
do postaci akceptowalnej przez interfejs klasy Klienc-
kiej (ang. Client). Pozwala na współpracę klas, które
nie mogą ze sobą współpracować bezpośrednio ze
względu na wzajemną niekompatybilność interfejsów.
Scenariusz, na którym wzorzec Adapter opiera
swoje działanie, można streścić następująco: obiekt
klasy Klienckiej wywołuje operację na obiekcie Ada-
ptera, poprzez interfejs Celu (ang. Target) pozwalają-
cy na takowe wywołanie. Wewnętrzna implementa-
cja Adaptera oddelegowuje z kolei wywołanie do kla-
sy adaptowanej (ang. Adaptee).
Wewnątrz wzorca Adapter zawarte jest niejawne
założenie, iż interfejsy zarówno obiektu Klienta jak
i Celu możemy podzielić na dwie części – udostęp-
nianą (ang. provided) i wymaganą (ang. required). To
założenie okazuje się również prawdziwe dla każ-
dego dobrze zdefi niowanego interfejsu komponen-
tu. Część udostępniana – interfejs udostępniany jest
postrzegana jako główna część interfejsu kompo-
nentu, pozwala na dostęp do podstawowych funk-
cjonalności udostępnianych przez komponent. Dru-
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-
ności. Komponent może np.: wymagać stworzenia
i/lub zniszczenia swojej instancji w środowisku, dla
którego został zaprojektowany, może wymagać by
komponenty używające udostępnionych przez niego
funkcjonalności – użytkownicy danego komponentu -
Paweł Kapłański
Autor jest architektem wiodącym w korporacji między-
narodowej zajmującej się m.in. systemami wbudowany-
mi (ang. embedded).
Kontakt z autorem: pawel.kaplanski@wp.pl
Kody żródłowe: http://www.digital-advanced.com
Ewolucja wzorca polimorfi zmu zewnętrznego w C++
35
www.sdjournal.org
Software Developer’s Journal 04/2007
wykonywali metody części udostępnionej interfejsu w określo-
nej kolejności. Co więcej, może wymagać tego, aby użytkow-
nicy potrafi li w określony sposób reagować na generowane
przez niego zdarzenia.
W przypadku wzorca Adapter zdarzeniowa część interfej-
su wymaganego komponentu klienta realizowana jest za po-
mocą interfejsu Celu. Możemy na ową parę Klient-Cel patrzeć
jako na interfejs dualny pojedynczego komponentu. Wyraźny
podział na część funkcjonalną i zdarzeniową implikuje imple-
mentację za pomocą konkretnej klasy Adaptera, zdarzeniowej
części interfejsu wymaganego.
Powróćmy do analizowanego przykładu. Zastanówmy się
nad użyciem wzorca Adapter do rozwiązania przedstawio-
nego tam problemu. Załóżmy, że dysponujemy klasą
Button
oraz
ButtonClient
, które to pełnią rolę owej pary interfejsów
pojedynczego komponentu. Identyfi kujemy
Button
z Klientem
a
ButtonClient
z interfejsem Celu (Listing 1).
Obiekty klasy
Button
wymagają, by podać w ich konstruk-
torze odpowiednią implementację interfejsu
ButtonClient
, któ-
ra to udostępni implementację wirtualnej metody
onPush
wy-
woływanej przez
Button
w reakcji GUI na interakcję z użyt-
kownikiem. Implementacja ta jest konkretną implementacją in-
terfejsu wymaganego komponentu Button-ButtonClient.
Aby mieć możliwość przeprowadzenia symulacji działa-
nia naszego rozwiązania, wyposażmy dodatkowo klasę
Button
w funkcję
Button::simulatePush
, która umożliwi zasymulowa-
nie wciśnięcia przycisku poprzez odwołanie się do zdarzenio-
wej części interfejsu wymaganego – interfejsu Celu – w tym
przypadku abstrakcyjnej klasy
ButtonClient
.
Klasą Adaptowaną (ang. Adaptee) w naszym przypadku
jest klasa
Mp3Player
, której implementacja składa się z dwóch
metod –
play
i
stop
(Listing 2).
Pozostaje nam jedynie implementacja Adapterów, które
to używając interfejsu Celu wywołają odpowiednie metody na
obiekcie klasy Adaptowanej (Listing 3).
Zwróćmy uwagę, że implementacja Adaptera dla przyci-
sku Stop różni się od implementacji adaptera przycisku Play
jedynie implementacją metody interfejsowej
ButtonClient::on-
Push
. (Listing 4).
Uwieńczmy nasze rozwiązanie małym programem testu-
jącym, konfi gurującym poszczególne elementy rozwiązania
i spinającym je w całość (listing 5).
Polimorfi zm zewnętrzny
Jak wyżej wspomniano, implementacja Adapterów obu przy-
cisków jest analogiczna, wydaje się więc naturalne, aby w pe-
wien sposób zautomatyzować ich wytwarzanie. Dokonać tego
możemy przy użyciu systemu szablonów wytwarzając gene-
ryczny Adapter, który konkretyzowany z odpowiednim typem
i wskaźnikiem na metodę może generować automatycznie od-
powiedni Adapter.
Rysunek 1.
Rozważany problem wytworzenia interfejsu
użytkownika
Mp3Player
+play()
+stop()
?
Rysunek 2.
Schemat wzorca projektowego Adapter
Client
Adapter
+Request()
Adaptee
+SpecificRequest()
<<
interfejs
>>
Target
+Request()
adaptee->SpecificRequest()
1
Listing 1.
Elementy interfejsu komponentu przycisku
class
ButtonClient
{
public
:
virtual
void
onPush
()
=
0
;
}
;
class
Button
{
public
:
explicit
Button
(
ButtonClient
*
client
)
:
client_
(
client
)
{}
...
//funkcja pozwalajaca na symulacje wcisniecia przycisku
void
simulatePush
()
{
client_
->
onPush
();
}
private
:
ButtonClient
*
client_
;
}
;
Listing 2.
Klasa Mp3Player
#include
<iostream>
class
Mp3Player
{
public
:
void
play
()
{
std
::
cout
<<
"PLAY"
<<
std
::
endl
;
}
void
stop
()
{
std
::
cout
<<
"STOP"
<<
std
::
endl
;
}
}
;
36
Inżynieria
oprogramowania
www.sdjournal.org
Software Developer’s Journal 04/2007
Tak zautomatyzowane podejście jest podstawą wzorca
polimorfi zmu zewnętrznego. Szablon generycznego Adapte-
ra dla interfejsu
ButtonClient
konkretyzowany z typem klasy
Adaptowanej, w konstruktorze pobierze wskaźnik na obiekt
Adaptowany oraz wskaźnik na metodę. Wskaźnik na ową me-
todę zostanie użyty w automatycznie wygenerowanej imple-
mentacji metody interfejsu Celu –
ButtonClient::onPush
, któ-
ra winna być wołana w odpowiedzi na zdarzenie generowane
przez obiekt klasy Klienckiej –
Button
(Listing 6).
Ta zmiana pociąga za sobą modyfi kację naszego progra-
mu testowego. Główna jego część pozostanie bez zmian, lecz
nie potrzebujemy już redundantnych implementacji Adapte-
rów. Konkretyzujemy je dopiero w momencie rzeczywistej po-
trzeby, w naszym przypadku z klasą Mp3Playera, podając od-
powiednie wskaźniki do metod (Listing 7).
Generyczny Adapter może być postrzegany jako konstrukt
pozwalający na polimorfi czne używanie klasy Adaptowanej
bez ingerencji w samą strukturę owej klasy, stając się swo-
istym pryzmatem, przez który możemy patrzeć na ową klasę.
Innej mówiąc: możemy uważać taki generyczny Adapter za
rodzaj polimorfi cznego interfejsu klasy Adaptowanej, który po-
zwala dowolnemu źródłu na używanie tej klasy bez potrzeby
jej modyfi kacji. Stąd wynika nazwa wzorca projektowego Ze-
wnętrznego Polimorfi zmu.
Następnym krokiem w ewolucji wzorca Zewnętrznego Po-
limorfi zmu jest zautomatyzowanie Generycznego Adaptera do
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-
nie również interfejsu Celu. Takowy Generyczny Adapter nie-
jako delegowałby wywołanie metody w kontekście pewnego
obiektu, dlatego nazywamy go Delegatem (
Delegate
). Delegat
powinien służyć do adaptowania dowolnych metod – o różnej
liczbie i typach argumentów - w tym również metod statycz-
nych, my jednak zajmiemy się szczegółowo przypadkiem me-
tod niestatycznych.
Pracę rozpocznijmy od stworzenia szablonu generyczne-
go interfejsu klasy Celu (Listing 8).
Listing 3.
Implementacja adaptera dla przycisku Play
class
Mp3PlayerPlayButtonAdapter
:
public
ButtonClient
{
public
:
explicit
Mp3PlayerPlayButtonAdapter
(
Mp3Player
*
player
)
:
player_
(
player
)
{}
virtual
void
onPush
()
{
player_
->
play
();
}
private
:
Mp3Player
*
player_
;
}
;
Listing 4.
Implementacja adaptera dla przycisku Stop
class
Mp3PlayerStopButtonAdapter
:
public
ButtonClient
{
...
virtual
void
onPush
()
{
player_
->
stop
();
}
...
}
;
Listing 5.
Program testujący
void
main
(
char
*
args
[]
,
int
argc
)
{
Mp3Player
aMp3Player
;
Mp3PlayerPlayButtonAdapter
playButtonClient
(&
aMp3Player
);
Mp3PlayerStopButtonAdapter
stopButtonClient
(&
aMp3Player
);
Button
playButton
(&
playButtonClient
);
Button
stopButton
(&
stopButtonClient
);
// symulacja wciskania przyciskow
playButton
.
simulatePush
();
stopButton
.
simulatePush
();
}
Listing 6.
Generyczny Adapter przycisku
template
<
class
T
>
class
ExternallyPolymorphicButtonClient
:
public
ButtonClient
{
public
:
ExternallyPolymorphicButtonClient
(
T
*
t
,
void
(
T
::*
mth
)())
:
t_
(
t
)
,
mth_
(
mth
)
{}
;
virtual
void
onPush
()
{
(
t_
->*
PtrToMth
)();
}
private
:
T
*
t_
;
void
(
T
::*
mth_
)();
}
;
Listing 7.
Automatycznie generowane Adaptery
przycisków
void
main
(
char
*
args
[]
,
int
argc
)
{
...
ExternallyPolymorphicButtonClient
<
Mp3Player
>
playButtonCli
ent
(&
aMp3Player
,
&
Mp3Player
::
play
);
ExternallyPolymorphicButtonClient
<
Mp3Player
>
stopButtonCli
ent
(&
aMp3Player
,
&
Mp3Player
::
stop
);
...
}
Ewolucja wzorca polimorfi zmu zewnętrznego w C++
37
www.sdjournal.org
Software Developer’s Journal 04/2007
Generyczny Adapter, implementujący interfejs generycz-
nego Celu powinien mieć dostęp do klasy adaptowanej, więc
szablon, za pomocą którego zrealizujemy ów konstrukt będzie
miał dodatkowy parametr odpowiadający owej Adaptowanej
klasie (Listing 9).
Sama w sobie klasa Delegata będzie zarządzała życiem
generycznego Adaptera pozwalając na jego naturalne używa-
nie poprzez semantykę kopiowania, dodatkowo wyposażając
go w operator wywołania funkcji (Listing 10).
Aby uprościć syntaktycznie użycie delegatów wspomo-
żemy się mechanizmem automatycznej dedukcji typów ar-
gumentów szablonów funkcji. Szablony funkcji mają tą wła-
sność, iż argumenty szablonu nie muszą być podawane jaw-
nie. Kompilator może je wydedukować z argumentów funkcji.
Tak, więc zamiast
int i,j,k;
k=max<int>(i,j);
możemy napisać
int i,j,k;
k=max(i,j)
Kompilator dopasuje sygnatury funkcji do parametrów jej wy-
wołania i automatycznie dokona odpowiedniej konkretyzacji –
w tym przypadku przyjmie jako parametr typ
int
.
Taki narzędziowy szablon funkcji nazwiemy
delegate _
cast
. Będzie on zwracał delegata wypełnionego odpowiednią
implementacją przyjmując za parametry: szablon, który obsłu-
guje, wskaźnik na adaptowany obiekt oraz odpowiednią meto-
dę tego obiektu, do której będzie odbywać się delegacja wy-
wołania (Listing 11).
Wróćmy do przykładu. Implementacja upraszcza się
w stopniu zaskakującym. Patrz Listing 12.
Zwróćmy uwagę, że nie ma już potrzeby tworzenia kla-
sy interfejsowej
ButtonClient
. Zamiast niej specyfi kujemy
miejsce podpięcia się do samej klasy
Button
poprzez użycie
obiektu
Delegat
w postaci publicznego atrybutu. Co więcej,
nie mamy potrzeby tworzenia odpowiednich Adapterów. Za-
miast tego podpinamy się bezpośrednio do obiektu klasy
But-
ton
z funkcjonalnościami udostępnianymi przez klasę
Mp3Play-
er
. Warto zauważyć, że ciało metody
Button::simulatePush
po-
sługuje się zdefi niowanym w klasie
Delegate
operatorem wy-
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ę-
dziemy nazywali Odpaleniem Delegata.
Rysunek 3.
Rozwiązanie przykładowego problemu z pomocą wzorca Adapter
Button
Mp3Player
+play()
+stop()
<<interfejs>>
ButtonClient
+onPush()
-client
-player
-player
Mp3PlayerPlayButtonAdapter
+onPush()
player->play()
player->play()
Mp3PlayerStopButtonAdapter
+onPush()
1
1
Rysunek 4.
Generyczny Adapter przycisku
ExternallyPolymorphicButtonClient<Mp3Player>
-mth_=&Mp3Player::stop
ExternallyPolymorphicButtonClient<Mp3Player>
-mth_=&Mp3Player::play
ExternallyPolymorphicButtonClient
-T* t_
- void (T::*mth_) ()
+onPush()
Mp3Player
+play()
+stop()
Button
<<
interfejs
>>
ButtonClient
+onPush()
(t_->*mth_)()
T
1
1
-client
38
Inżynieria
oprogramowania
www.sdjournal.org
Software Developer’s Journal 04/2007
Mechanizm metod wirtualnych wyposażając C++ we
wsparcie dla polimorfi zmu dynamicznego, pozwolił na wyeli-
minowanie wielu konstrukcji składających się z garści rozga-
łęzień logicznych typu
if
/
else if
/
else
lub
swich(...){...}
po
znaczniku typu. Spowodowało to znaczne zwiększenie zro-
zumiałości algorytmów. Mając do dyspozycji Delegaty wypo-
sażajmy zestaw naszych narzędzi w polimorfi zm zewnętrz-
ny, pozwalając na znaczne rozluźnienie wewnętrznych relacji
interfejsów miedzyobiektowych. W ten sposób udało nam się
wzbogacić język narzędzi podstawowych o bardzo silne na-
rzędzie pozwalające na separację interfejsów.
Aby się o tym przekonać zobaczmy jak wygląda doda-
nie funkcjonalności prezentacji bieżącej pozycji odtwarzane-
go utworu na odtwarzaczu mp3. Dodajmy do zbioru kontrolek
klasę
ProgressBar
(Listing 13).
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::
setProgress
.
Wzbogaćmy klasę
Mp3Player
w dodatkowego Delegata,
który odpala się z taką informacją, gdy bieżąca pozycja od-
twarzanego utworu ulega zmianie (Listing 14).
Uważny czytelnik zauważy, że metoda
Mp3Player::simu-
lateProgress
używa Delegata
Mp3Player::eventProgress
ja-
ko funktora podawanego do algorytmu standardowego
std::
for _ each
. Kompatybilność Delegatów ze standardowymi al-
gorytmami jest możliwa właśnie dzięki przeciążeniu operato-
ra wywołania funkcji, a co za tym idzie konkretny Delegat jest
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
template
<
class
T
,
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
DelegateImpl
:
public
virtual
IDelegate
<
RetVal
,
Arg1
,
Arg2
,
Arg3
,
Arg4
,
Arg5
>
{
public
:
typedef
typename
ICallable
<
RetVal
,
Arg1
,
Arg2
,...
>
::
MethodType
<
T
>::
V
methodType
;
DelegateImpl
(
T
*
obj
,
methodType
method
)
:
obj_
(
obj
)
,
method_
(
method
)
{}
RetVal
call
(
Arg1
arg1
,
Arg2
arg2
, ...
)
const
{
return
(
obj_
->*
method_
)(
arg1
,
arg2
, ...
);
}
protected
:
methodType
method_
;
T
*
obj_
;
}
;
Listing 10.
Implementacja Delegata
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
Delegate
{
public
:
explicit
Delegate
(
IDelegate
<
RetVal
,
Arg1
,
Arg2
,
Arg3
,
Arg4
,
Ar
g5
>*
ptr
=
0
)
:
ptr_
(
ptr
)
{}
...
RetVal
operator
()(
Arg1
arg1
,
Arg2
arg2
)
const
{
return
ptr_
->
call
(
arg1
,
arg2
);
}
private
:
IDelegate
<
RetVal
,
Arg1
,
Arg2
,
Arg3
,
Arg4
,
Arg5
>*
ptr_
;
}
;
Listing 11.
Pomocniczy narzędziowy szablon funkcji
template
<
template
<
typename
,
typename
,...
>
class
O
,
class
T
,
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
O
<
RetVal
,
Arg1
,
Arg2
>:
HandleType
delegate_cast
(
T
*
obj
,
RetVal
(
T
::*
methodType
)
(
Arg1
,
Arg2
,...
))
{
return
O
<
RetVal
,
Arg1
,
Arg2
>::
HandleType
<
RetVal
,
Arg1
,
Arg2
,...
>
(
new
O
<
RetVal
,
Arg1
,
Arg2
>
DelegateImlp
<
T
>
(
obj
,
methodType
));
}
Listing 8.
Interfejs Celu jako szablon.
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
ICallable
<
RetVal
,
Arg1
,
Arg2
,...
>
{
public
:
ICallable
()
{}
virtual
~
ICallable
()
{}
virtual
RetVal
call
(
Arg1
arg1
,
Arg2
arg2
,...
)
const
=
0
;
...
template
<
typename
T
>
struct
MethodType
{
typedef
RetVal
(
T
::*
V
)
(
Arg1
,
Arg2
);
}
;
typedef
RetVal
(*
StaticType
)
(
Arg1
,
Arg2
);
}
;
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
IDelegate
:
public
ICallable
<
RetVal
,
Arg1
,
Arg2
,...
>
{
public
:
virtual
bool
isEqual
(
const
IDelegate
<
RetVal
,
Arg1
,
Arg2
,...
>
&
h
)
const
=
0
;
virtual
IDelegate
*
clone
()
const
=
0
;
}
;
39
Ewolucja wzorca polimorfi zmu zewnętrznego w C++
www.sdjournal.org
Software Developer’s Journal 04/2007
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ą
„wtyczką” nie może działać poprawnie, jeśli nikt się do owej
Rysunek 5.
Schemat Delegata
DelegateImpl
-T* obj_
-RetVal (T::*mth_) (Arg1, Arg2, Arg3, ...)
+RetVal()
+bool isEqua;()
+clone()
automatycznie generowana implementacja
delegat
abstrakcyjny interfejs delegata
T, RetVal, Arg1, Arg2, Arg3, ...
Delegate
+RetVal operator (Arg1, Arg2, Arg3,...)()
+bool operator==()
+operator=()
IDelegate
+RetVal call()
+bool isEqual()
+clone()
RetVal, Arg1, Arg2, Arg3, ...
RetVal, Arg1, Arg2, Arg3, ...
Listing 12.
Implementacja przykładu przy użyciu
Delegatów
class
Button
{
public
:
Delegate
<
void
>
eventPush
;
// function used by
Framework
to
generate
event
void
simulatePush
()
{
eventPush
();
}
}
;
void
main
(
char
*
args
[]
,
int
argc
)
{
Mp3Player
aMp3Player
;
Button
playButton
;
Button
stopButton
;
playButton
.
eventPush
=
delegate
_cast
<
Delegate
>(&
aMp3Player
,
&
Mp3Player
::
play
);
stopButton
.
eventPush
=
delegate
_cast
<
Delegate
>(&
aMp3Player
,
&
Mp3Player
::
stop
);
// symulacja wcisniecia klawiszy
playButton
.
simulatePush
();
stopButton
.
simulatePush
();
}
Listing 13.
Prosta kontrolka ProgressBar
class
ProgressBar
{
public
:
void
setProgress
(
int
position
)
{
std
::
cout
<<
"PROGRESS:"
<<
position
<<
std
::
endl
;
}
}
;
Rysunek 6.
Scenariusz interakcji Klient-Serwer w przypadku
Delegata
Server
Klient
request=onRequest
request()
response()
run
run
onRequest
40
Inżynieria
oprogramowania
www.sdjournal.org
Software Developer’s Journal 04/2007
wtyczki nie przypnie. Obiekt, którego interfejs posiada taką
wtyczkę nazwijmy Serwerem, a obiekt używający tej wtycz-
ki Klientem.
Rozluźnienie powiązań miedzyobiektowych, które moż-
na uzyskać za jej pomocą pozwala na całkowite uniezależnie-
nie Klienta od Serwera i vice-versa. Spięcie obu komponen-
tów może odbywać się na zewnątrz, w części konfi guracyjnej
systemu.
Rozgłaszanie zdarzeń
Podstawowy model interakcji pomiędzy Klientem a Serwerem
możemy opisać następująco:
• Klient jest przypinany do odpowiedniego Delegata wysta-
wionego przez Serwer;
• Serwer Odpala Delegata;
• Delegat przekazuje wywołanie do metody po stronie Klienta;
• Metoda Klienta jest uruchomiona synchronicznie w wątku
wołającego i po wykonaniu zwraca wartość poprzez Dele-
gata do Serwera.
W powyższym scenariuszu daje się zauważyć silne powią-
zanie jeden-do-jednego pomiędzy Klientem a Serwerem.
Listing 14.
Zdarzenie o zmianie pozycji w bieżącym
utworze zaimplementowane jako Delegat
#include
<algorithm>
#include
<vector>
class
Mp3Player
{
public
:
Delegate
<
void
,
int
>
eventProgress
;
// symulacja zmiany
pozycji
biezacego
utworu
void
simulateProgress
()
{
std
::
vector
<
int
>
positions
;
for
(
int
i
=
0
;
i
<=
100
;
i
+=
10
)
positions
.
push_back
(
i
);
std
::
for_each
(
positions
.
begin
()
,
positions
.
end
()
,
eventProgress
);
}
}
;
void
main
(
char
*
args
[]
,
int
argc
)
{
Mp3Player
aMp3Player
;
ProgressBar
progressBar
;
aMp3Player
.
eventProgress
=
delegate_cast
<
Delegate
>
(&
progressBar
,
&
ProgressBar
:
setProgress
);
aMp3Player
.
simulateProgress
();
}
Listing 15.
Element cyklicznej listy dwukierunkowej
template
<
int
n
>
class
DLListNode
{
public
:
DLListNode
<
n
>()
:
currentPtr_
(
0
)
{
next_
=
prev_
=
this
;
}
// automatyczne wypiecie z listy
virtual
~
DLListNode
<
n
>()
{
if
(
currentPtr_
!=
0
&&
*
currentPtr_
==
this
)
*
currentPtr_
=
prev_
;
prev_
->
next_
=
next_
;
next_
->
prev_
=
prev_
;
}
//wstawienie pomiedzy
nastepnego
a
poprzedniego
void
insert
(
DLListNode
<
n
>
*
elem
,
DLListNode
<
n
>**
currentPtr
)
{
elem
->
prev_
=
this
;
elem
->
next_
=
next_
;
elem
->
prev_
->
next_
=
elem
;
elem
->
next_
->
prev_
=
elem
;
elem
->
currentPtr_
=
currentPtr
;
}
private
:
template
<
int
n
,
typename
T
>
friend
class
DLList
;
DLListNode
<
n
>*
next_
;
//nastepny element w liscie
DLListNode
<
n
>*
prev_
;
//poprzedni element w liscie
DLListNode
<
n
>**
currentPtr_
;
//wskaznik na bieżąco iterowany element
}
;
Rysunek 7.
Wzorzec Observer
Subject
+ Attach()
+ Detach()
+ Notify()
ConcreteSubject
+ SetState()
+ GetState()
- subjectState
ConcreteObserver
+ Update()
- observerState
<<interfejs>>
Observer
+ Update()
- observer
- subject
*
observerState=
subject->GetState
return
subjectState
for all o in observers
o->update()
41
Ewolucja wzorca polimorfi zmu zewnętrznego w C++
www.sdjournal.org
Software Developer’s Journal 04/2007
Wymaga ono niejawnie, aby Klient był przypięty do Serwe-
ra zanim Serwer odpali Delegata. Z drugiej strony Klient mo-
że zwracać rezultat działania metody do Serwera pozwalając
w ten sposób na przekazanie komunikatu w kierunku przeciw-
nym od Klienta do Serwera.
Taki scenariusz nie odbiega w dużym stopniu od zwykłego
wywołania metody obiektu Klienta, więc będziemy go nazy-
wać w dalszej części Wywołaniem Delegata w odróżnieniu od
Rysunek 9.
Cykliczna lista dwukierunkowa
DLList
current
DLListNode
next
prev
currentPtr
DLListNode
next
prev
currentPtr
DLListNode
next
prev
currentPtr
DLListNode
next
prev
currentPtr
Rysunek 8.
Scenariusz Rozgłoszenia.
Server
Klient A
request=onRequest
request()
onRequest
Klient B
onRequest
Klient C
onRequest
request=onRequest
request=onRequest
request=onRequest
request()
request()
for each
Listing 16.
Cykliczna lista dwukierunkowa
template
<
int
n
,
typename
T
=
DLListNode
<
n
>
>
class
DLList
:
public
DLListNode
<
n
>
{
public
:
// poczatkowy element iteracji
void
start
()
const
{
current_
=
next_
;
}
//nastepny element iteracji
void
next
()
const
{
current_
=
current_
->
next_
;
}
//biezacy element iterowany
T
*
current
()
const
{
return
static_
cast
<
T
*>(
current_
==
this
?0
:
current_
);
}
//wstawienie elementu do listy
void
insert
(
DLListNode
<
n
>*
elem
)
{
DLListNode
<
n
>::
insert
(
elem
,
&
current_
);
}
//usuniecie elementu z listy
void
remove
(
T
*
source
)
{
for
(
DLListNode
<
n
>*
x
=
next_
;
x
!=
this
;
x
=
x
->
next_
)
{
if
(
static_cast
<
T
*>(
x
)
->
isEqual
(*
source
))
{
delete
x
;
}
//wyczyszczenie zawartosci listy
void
clear
()
{
while
(
next_
!=
this
)
delete
next_
;
}
//w destruktorze wyczysc zawartosc
virtual
~
DLList
()
{
clear
();
}
private
:
mutable
DLListNode
<
n
>*
current_
;
}
;
42
Inżynieria
oprogramowania
www.sdjournal.org
Software Developer’s Journal 04/2007
scenariusza nazwanego Rozgłaszaniem, które realizuje po-
wiązanie Server-Klient typu jeden-do-wielu.
Rozgłaszanie to, inaczej mówiąc, wysyłanie komunika-
tów do wszystkich Klientów (w szczególności nie wysyłanie do
żadnego), którzy zarejestrowali się na owe zdarzenie. W przy-
padku Rozgłoszenia nie mamy możliwości zwrócenia jedno-
znacznej wartości gdyż zależy ona od ilości zarejestrowanych
klientów.
Problem Rozgłaszania jest dotknięty we wzorcu projekto-
wym Obserwator (ang. Observer). Wzorzec Obserwator znaj-
duje zastosowanie w sytuacji, gdy jeden obiekt powinien mieć
możliwość powiadamiania innych obiektów, o których egzy-
stencji nie chce zakładać zbyt wiele, w tym nie interesuje go
ich ilość. We wzorcu Obserwator można wyróżnić dwie podsta-
wowe hierarchie klas. Hierarchię obserwowaną i hierarchię ob-
serwującą. Hierarchia obserwowana ma korzeń w postaci kla-
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 notyfi kację
Rysunek 10.
Asocjacje międzyobiektowe
DLList
current
DLListNode
next
prev
currentPtr
DLList
current
DLListNode
next
prev
currentPtr
DLList
current
DLListNode
next
prev
currentPtr
Association
DLListNode
next
prev
currentPtr
DLListNode
next
prev
currentPtr
Association
DLListNode
next
prev
currentPtr
DLListNode
next
prev
currentPtr
Association
DLListNode
next
prev
currentPtr
DLListNode
next
prev
currentPtr
Listing 17.
Interfejs Delegacji Rozgłoszenia
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
IeventDelegate
:
public
DLListNode
<
0
>
,
public
Icallable
<
RetVal
,
Arg1
,
Arg2
,...
>
{
public
:
virtual
bool
isEqual
(
const
IeventDelegate
<
RetVal
,
Arg1
,
Arg2
,...
>&
h
)
const
=
0
;
virtual
IeventDelegate
*
clone
()
const
=
0
;
}
;
Ewolucja wzorca polimorfi zmu zewnętrznego w C++
43
www.sdjournal.org
Software Developer’s Journal 04/2007
o zmianie ze strony klasy konkretnej klasy obserwowanej
Con-
creteSubject
zapytać o jej bieżący stan i na tej podstawie zak-
tualizować lokalne informacje na temat klasy obserwowanej.
Patrząc z boku na wzorzec Obserwator zauważamy duże
podobieństwo w jego strukturze do wzorca Adapter, z tą jed-
nak różnicą, że o ile Adapter stowarzysza obserwatora i ob-
serwowanego w stosunku jedne-do-jednego, to Obserwator
pozwala na skojarzenie jeden-do-wielu. Rozszerzenie kon-
cepcji Delegata w taki sposób, aby można było łączyć je w re-
lacje jeden-do-wielu dawałoby w rezultacie – jak się można
spodziewać – wzorzec Rozgłoszenia. Niestety naiwna imple-
mentacja Rozgłoszenia jako kolekcji delegatów, najeżona jest
pułapkami. Podczas iteracji po kolekcji Delegatów obserwato-
rzy w odpowiedzi na wywołanie mogą ową kolekcję zmodyfi -
kować, mamy więc do wyboru – albo kosztem wydajności –
przed samą iteracją skopiować kolekcję delegatów a iterację
przeprowadzić na tej właśnie kopii, lub zbudować taką „spryt-
ną kolekcję”, która zagwarantuje sama swoją spójność w cza-
sie iteracji.
Listing 19.
Delegat Rozgłoszenia
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
EventDelegate
{
public
:
explicit
EventDelegate
(
IeventDelegate
<
RetVal
,
Arg1
,
Arg2
,
Arg3
,
Arg4
,
Arg5
>*
ptr
=
0
)
:
ptr_
(
ptr
)
{}
RetVal
operator
()(
Arg1
arg1
,
Arg2
arg2
)
const
{
return
ptr_
->
call
(
arg1
,
arg2
);
}
private
:
IeventDelegate
<
RetVal
,
Arg1
,
Arg2
,
Arg3
,
Arg4
,
Arg5
>*
ptr_
;
}
;
Listing 18.
Implementacja Delegacji Rozgłoszenia
template
<
class
T
,
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
EventDelegateImpl
:
public
virtual
IeventDelegate
<
RetVal
,
Arg1
,
Arg2
,
Arg3
,
Arg4
,
Arg5
>
{
public
:
typedef
typename
Icallable
<
RetVal
,
Arg1
,
Arg2
,...
>
MethodType
<
T
>::
V
methodType
;
EventDelegateImpl
(
T
*
obj
,
methodType
method
)
:
obj_
(
obj
)
,
method_
(
method
)
{}
RetVal
call
(
Arg1
arg1
,
Arg2
arg2
, ...
)
const
return
(
obj_
->*
method_
)
(
arg1
,
arg2
, ...
);
}
protected
:
methodType
method_
;
T
*
obj_
;
}
;
Listing 20.
Rozgłoszenie
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
Event
:
public
DLList
<
0,
IeventDelegate
<
RetVal
,
Arg1
,
Arg2
, ...
>
>
{
private
:
template
<
typename
T
>
struct
Tsel
{}
;
template
<
typename
R
>
R
callMethod
(
TSel
<
R
>
,
Arg1
arg1
,
Arg2
arg2
,...
)
{
assert
(!
empty
());
return
elem
()->
call
(
arg1
,
arg2
);
}
template
<
typename
R
>
void
callMethod
(
TSel
<
void
>
,
Arg1
arg1
,
Arg2
arg2
,...
)
{
for
(
start
();;
next
())
{
if
(
current
()==
0
)
break
;
current
()->
call
(
arg1
,
arg2
);
}
}
public
:
void
operator
+=
(
EventDelegate
<
RetVal
,
Arg1
,
Arg2
,...
>
method
)
{
IEventDelegate
<
void
,
Arg1
,
Arg2
,...
>
*
clone
=
method
.
get
()->
clone
();
insert
(
clone
);
void
operator
-=
(
EventDelegate
<
RetVal
,
Arg1
,
Arg2
,...
>
method
)
{
remove
(
method
.
get
());
}
RetVal
operator
()(
Arg1
arg1
,
Arg2
arg2
,...
)
{
return
callMethod
<
RetVal
>
(
TSel
<
RetVal
>()
,
arg1
,
arg2
,...
);
}
}
;
44
Inżynieria
oprogramowania
www.sdjournal.org
Software Developer’s Journal 04/2007
Sprytna kolekcja ma swoje początki w implementacji
sprytnego-wskaźnika opartego o cykliczną listę dwukierun-
kową [Alex01]. Elementy tej kolekcji w trakcie niszczenia –
w czasie wywoływania destruktorów – potrafi ą się automa-
tycznie z listy wypiąć. Co więcej, taka „sprytna kolekcja” po-
trafi również odpowiednio skorygować iterator przesuwa-
jąc go w razie potrzeby na odpowiednią prawidłową pozy-
cję. Kolekcja, na której się skupimy będzie bazowała na ta-
kim rozwiązaniu.
Przejdźmy do implementacji. Element takiej listy zadekla-
rujemy jako szablon. Nietypowy parametr tego szablonu przy-
da nam się później (Listing 15).
Sama lista dwukierunkowa. Przedstawia ją Listing 16.
Dysponując taką listą możemy przejść do implementa-
cji generycznego Obserwatora – czy inaczej mówiąc Rozgło-
szenia. Abstrakcyjny interfejs Delegata, który może się pod-
piąć do rozgłoszenia odziedziczymy po elemencie listy, a co
za tym idzie będziemy mogli takie Delegaty dodawać do sa-
mej listy. Patrz Listing 17.
Implementacja z kolei jest identyczna z implementacją De-
legata. Przedstawia to Listing 18.
Podobnie jak klasa
EventDelegate
, zarządzająca życiem
delegata i wyposażająca go w operator wywołania funkcji (Li-
sting 19).
Przejdźmy teraz do klasy samego Rozgłoszenia odziedzi-
czonej po sprytnej kolekcji. Przedstawia to Listing 20.
Listing 21.
Implementacja przykładu przy użyciu
Rozgłoszeń
class
Button
{
public
:
Event
<
void
>
eventPush
;
// function used by
Framework
to
generate
event
void
simulatePush
()
{
std
::
cout
<<
"
PUSHING BUTTON"
<<
std
::
endl
;
eventPush
();
}
}
;
class
Mp3Player
{
public
:
virtual
~
Mp3Player
()
{
std
::
cout
<<
"MP3Player IS
DEAD"
<<
std
::
endl
;
}
void
play
()
{
std
::
cout
<<
"PLAY"
<<
std
::
endl
;
}
void
stop
()
{
std
::
cout
<<
"STOP"
<<
std
::
endl
;
}
Event
<
void
,
int
>
eventProgress
;
void
simulateProgress
()
{
std
::
vector
<
int
>
positions
;
for
(
int
i
=
0
;
i
<=
100
;
i
+=
10
)
positions
.
push_back
(
i
);
std
::
for_each
(
positions
.
begin
()
,
positions
.
end
()
,
eventProgress
);
}
}
;
Listing 22.
Konfi guracja rozgłoszeń w przykładzie
void
main
(
char
*
args
[]
,
int
argc
)
{
std
::
auto_ptr
<
Mp3Player
>
aMp3Player
(
new
Mp3Player
);
Button
playButton
;
Button
stopButton
;
ProgressBar
progressBar
;
playButton
.
eventPush
+=
delegate
cast
<
Event
>(
aMp3Player
.
get
()
,
&
Mp3Player
::
play
);
stopButton
.
eventPush
+=
delegate
cast
<
Event
>(
aMp3Player
.
get
()
,
&
Mp3Player
::
stop
);
aMp3Player
->
eventProgress
+=
delegate_cast
<
Event
>
(&
progressBar
,
&
ProgressBar
setProgress
);
// done by framework
playButton
.
simulatePush
();
stopButton
.
simulatePush
();
aMp3Player
->
simulateProgress
();
playButton
.
eventPush
-=
delegate
cast
<
Event
>(
aMp3Player
.
get
()
,
&
Mp3Player
::
play
);
// kill mp3 player
aMp3Player
.
reset
();
// done by framework
playButton
.
simulatePush
();
// fail
stopButton
.
simulatePush
();
}
Listing 23.
Interfejs Delegacji Automatycznego
Rozgłoszenia
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
IAutoEventDelegate
:
public
DLListNode
<
0
>
,
public
DLListNode
<
1
>
,
public
Icallable
<
RetVal
,
Arg1
,
Arg2
,...
>
{
public
:
virtual
bool
isEqual
(
const
IautoEventDelegate
<
RetVal
Arg1
,
Arg2
,...
>&
h
)
const
=
0
;
virtual
IautoEventDelegate
*
clone
()
const
=
0
;
virtual
void
connect
()
=
0
;
}
;
46
Inżynieria
oprogramowania
www.sdjournal.org
Software Developer’s Journal 04/2007
Komentarza wymaga zastosowana tu sztuczka, zwana ty-
pe-selection. Pozwala ona na symulacje przeciążania szablo-
nów metod i szerzej jest opisana w pracy [Alex01]. Dzięki niej
otrzymujemy specjalizację dla przypadku, gdy interesuje nas
wartość zwracana i dla przypadku, gdy takiej wartości nie po-
trzebujemy.
Wróćmy do naszego przykładu z klasą
Mp3Player
i jej inter-
fejsem użytkownika.
Przy użyciu Rozgłaszania możemy ten problem zaimple-
mentować tak jak przedstawia to Listing 21.
A część konfi guracyjną przedstawia Listing 22.
Niestety ten przykład powoduje błąd czasu wykonywania.
Mp3Player
jest niszczony przed odpisaniem się od przycisku
stop. Symulacja wciśnięcia tego przycisku powoduje odwoła-
nie się do nieistniejącego obiektu. Aby temu zapobiec musi-
my zautomatyzować proces odpinania się obserwatorów od
obiektu obserwowanego. Jesteśmy już na to gotowi dysponu-
jąc powyższa implementacją listy dwukierunkowej.
Automatyczne wyrejestrowywanie
się w czasie destrukcji obiektu
Każdy Delegat wiążący obserwatora z obiektem obserwo-
wanym jest swoistą relacją i powinien być związany z dwoma
obiektami jednocześnie. W momencie śmierci jednego z nich
cała relacja powinna być niszczona powodując automatyczne
wyrejestrowanie się obserwatora z obiektu obserwowanego
(Rysunek 10 i Listing 23).
Interfejs dziedziczy po dwóch typach elementów listy dwu-
kierunkowej, ponadto dodaje wirtualną metodę
connect
, któ-
ra w implementacji wpina odpowiedni element do drugiej listy
związanej z obiektem obserwatora (Listing 24).
Ostatnim etapem jest stworzenie klasy automatycznego
generycznego Obserwatora. Patrz Listing 25.
Podmieniając w naszym przykładzie klasę
Event
na
Auto-
Event
, nie doświadczamy już błędu czasu wykonania, gdyż
Listing 24.
Implementacja Delegacji Automatycznego
Rozgłoszenia
class
AutoEventDelegateImpl
:
public
IautoEventDelegate
<
RetVal
,
Arg1
,
Arg2
,...
>
{
public
:
virtual
void
connect
()
{
obj
->
DLListNode
<
1
>
insert
(
this
,0
);
}
}
;
Listing 25.
Przykład przy wykorzystaniu Automatycznych
Rozgłoszeń
template
<
typename
RetVal
,
typename
Arg1
,
typename
Arg2
,...
>
class
AutoEvent
:
public
DLList
<
0,
Iauto
EventDelegate
<
RetVal
,
Arg1
,
Arg2
, ...
>
>
{
public
:
void
operator
+=
(
Auto
EventDelegate
<
RetVal
,
Arg1
,
Arg2
,...
>
method
)
{
IautoEventDelegate
<
void
,
Arg1
,
Arg2
,...
>*
clone
=
method
.
get
()->
clone
();
clone
->
connect
();
insert
(
clone
);
}
void
operator
-=
(
AutoEvent
Delegate
<
RetVal
,
Arg1
,
Arg2
,...
>
method
)
{
remove
(
method
.
get
());
}
RetVal
operator
()(
Arg1
arg1
,
Arg2
arg2
,...
)
{
return
callMethod
<
RetVal
>
(
TSel
<
RetVal
>()
,
arg1
,
arg2
,...
);
}
}
;
Tabela 1.
Porównanie funkcjonalne narzędzi wspierających wzorzec polimorfi zmu zewnętrznego
Cecha
Delegat
Event z wartością
zwracaną
Event bez wartości zwracanej
Separacja interfejsów
tak
tak
Tak
Bezpieczeństwo w przypadku
„śmierci” Klienta lub Serwera
nie
Tak
Tak
Relacja Klient-Serwer
1-1
1-1
1-N
Wartość zwracana
tak
tak
Nie
Koszt
a) virtual call
b) pointer to method
c) memory allocation
for method binder
a) virtual call
b) pointer to method
c) association extraction
d) memory allocation for
method binder
e) memory allocation for
association
a) for-each virtual call
b) for-each pointer to method
c) for-each association extraction
d) for-each memory allocation for me-
thod binder
e) for-each memory allocation per as-
sociation
47
Ewolucja wzorca polimorfi zmu zewnętrznego w C++
www.sdjournal.org
Software Developer’s Journal 04/2007
klasa
Mp3Player
będąc niszczona automatycznie wyrejestruje
wszystkich przypiętych do siebie obserwatorów w tym przy-
padku przycisk Stop.
Problemy w aplikacjach
wielowątkowych
Przedstawione tu mechanizmy zakładały, że cała rzecz od-
bywa się w kontekście pojedynczego wątku. Powstaje pyta-
nie: jaki wpływ ma próba użycia owych mechanizmów w apli-
kacjach wielowątkowych? Aby odpowiedzieć sobie na to pyta-
nie musimy spojrzeć jeszcze raz na interfejs komponentu ja-
ko na twór składający się z części udostępnianej i wymaga-
nej. Część udostępniana komponentu może być zaprojekto-
wana z myślą o bezpiecznym dostępie z wielu wątków użyt-
kowników tego komponentu. Z kolei z drugiej strony kompo-
nent zgłaszający jakieś zgłoszenie może działać w jeszcze in-
nym wątku. W przypadku użycia Delegatów, czy Rozgłoszeń
musimy o tym pamiętać.
Po pierwsze do Delegata lub Rozgłoszenie użytkownik
może się przypiąć z innego wątku niż wątek w którym Dele-
gat lub Rozgłoszenie działa, ponadto Rozgłoszenie lub Dele-
gat może się odpalać się w kontekście wątku innego niż wą-
tek użytkownika komponentu. Musimy więc chronić w jakiś
sposób zarówno proces przypinania/odpinania się od Dele-
gata czy Rozgłoszenia, jak również pilnować tego aby w trak-
cie odpalania Delegata lub Rozgłoszenia inne wątki zaczeka-
ły z przypinaniem/odpinaniem. Owe zabezpieczenie dość ła-
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-
xsie chroniącym cały interfejs udostępniany komponentu.
Podsumowanie
Artykuł ten miał na celu w główniej mierze pokazanie w ja-
ki sposób można w języku C++ uzyskać jednolity styl archi-
tektoniczny bazujący na koncepcji polimorfi zmu zewnętrzne-
go. Separowalność interfejsów zmniejsza w znacznym stop-
niu zależności pomiędzy obiektami, a więc zmniejsza wiel-
kość szczepienia (ang. coupling) tak wytworzonej struktury,
jednocześnie dając szansę na zwiększenie spójności logicz-
nej (ang. cohesion) poszczególnych komponentów, co jest
celem każdej dobrej architektury systemu informatycznego.
Funktory, którymi są
Delegate
i
Event
nie są same w sobie re-
wolucją. Z problematyką polimorfi zmu zewnętrznego zderzy-
li się chociażby twórcy modelu COM w fi rmie Microsoft, chcąc
ujednolicić zdarzeniowy interfejs komponentów. W najnow-
szej implementacji zarządzanego C++ są Delegaty są expli-
cite wymienione jako konstrukcje podstawowe [MSDN], dzia-
łają one jednak na innej zasadzie i są o wiele bardziej zasobo-
żerne. Przedstawione tu konstrukcje zainspirowane są praca-
mi [Clee97], [Clug05] oraz [Jaku97]. Koncepcja cyklicznej listy
dwukierunkowej pochodzi natomiast z pracy Risto Lankinen
opublikowanej na grupach dyskusyjnych w listopadzie 1995
a opisanej szerzej w [Alex01]. Przedstawione tu konstrukcje
zostały zweryfi kowane w rzeczywistych projektach komercyj-
nych cechując się dużą wydajnością i zapewniając znaczne
zwiększenie jakości kodu programistycznego.
Bibliografi a
• [Abra04] David Abrahams, Aleksey Gurtovoy. C++ Template
Metaprogramming. ISBN: 0321227255: Addison-Wesley, 2004;
• [Alex01] Andrei Alexandrescu. Modern C++ Design: Ge-
neric Programming and Design Patterns Applied. ISBN:
0201704315, Boston, MA: Addison-Wesley, 2001;
• [Clee97] Cleeland, Chris, Douglas C. Schmidt, Tim Harrison.
External Polymorphism”, PATTERN LANGUAGES OF PRO-
GRAM DESIGN, volume 3, edited by Robert Martin, Frank Bu-
schmann, and Dirke Riehle:Addison-Wesley, Reading MA, 1997.
• http://www.cs.wustl.edu/~schmidt/PDF/External-Polymor-
phism.pdf;
• [Clug05] Don Clugston. Member Function Pointers and the
Fastest Possible C++ Delegates. http://www.codeproject.com/
cpp/FastDelegate.asp;
• [Gamm95] E. Gamma, R. Helm, R. Johnson, and J. Vlissides.
Design Patterns: Elements of Reusable Object-Oriented So-
ftware. Reading, MA: Addison-Wesley, 1995.
• [Jaku97] Paul Jakubik. „Callback Implementations in C++”
• http://www.newty.de/jakubik/callback.pdf, 1997;
• [Lipp96] Stanley B. Lippman. Inside the C++ Object Model.
ISBN: 0201834545: Addison-Wesley, 1996;
• [MSDN] Generic Delegates (C++) http://msdn2.microsoft.com/
en-us/library/213x8e7w.aspx;
• [Schm00] Douglas Schmidt, Michael Stal, Hans Rohnert,
Frank Buschmann. Pattern-Oriented Software Architecture,
Volume 2, Patterns for Concurrent and Networked Objects
ISBN: 9780471606956: Addison-Wesley, 2000.
R
E
K
L
A
M
A