2010.02_Fabryki obiektów _[Programowanie C C++].pdf

(359 KB) Pobierz
441718469 UNPDF
Programowanie C++
Fabryki obiektów
Techniki opisane w tym artykule pozwalają tworzyć obiekty na
podstawie identyfikatorów dostarczanych w czasie działania programu,
co jest wygodniejsze niż podawanie typów w formie zrozumiałej dla
kompilatora.
Dowiesz się:
• Jak tworzyć obiekty w C++;
• Co to jest wzorzec fabryki.
Powinieneś wiedzieć:
• Jak pisać proste programy w C++;
• Co to jest dziedziczenie i funkcje wirtualne.
tworzyć obiekty na podstawie identyfikato-
ra, rolę tę pełni funkcja createObj . Po utwo-
rzeniu obiektu możemy wczytać składowe,
wykorzystując mechanizm funkcji wirtual-
nych, wołając metodę read dla utworzone-
go obiektu.
Przy zapisie obiektu do strumienia wyj-
ściowego (na przykład do pliku) nie potrze-
bujemy dodatkowych mechanizmów, któ-
re dostarczą identyfikator dla danego typu,
ponieważ możemy wykorzystać mechanizm
funkcji wirtualnych. Posiadając wskaźnik lub
referencję na klasę bazową, wołamy meto-
write , która zapisuje identyfikator klasy
konkretnej oraz jej składowe. Odczyt obiek-
tu jest o wiele bardziej złożony niż zapis ze
względu na to, że zapis posługuje się istnie-
jącymi obiektami, natomiast odczyt musi je
tworzyć.
Poziom
trudności
Funkcje fabryczne
Przykład wykorzystania fabryki obiektów
został pokazany na Listingu 2, gdzie poka-
zano funkcję create tworzącą obiekty klas
na podstawie danych zapisanych w strumie-
niu wejściowym, na przykład w pliku. Funk-
cja ta dostarcza obiekt odpowiedniego typu
konkretnego dla hierarchii Figure . Metody
zapisu ( write ) oraz odczytu ( read ) są do-
starczane przez każdą z klas konkretnych.
Jeżeli chcemy obiekt utworzyć (odczytać),
to typ obiektu jest dostarczany w czasie dzia-
łania, jest on wyznaczany przez identyfika-
tor dostarczany przez strumień. Ponieważ
tak dostarczany identyfikator nie jest ak-
ceptowany jako argument dla operacji new ,
należy wykorzystać fabrykę, która pozwoli
obiekty pośredniczą przy tworze-
niu innych obiektów. Dostarcza-
na informacja jednoznacznie identyfiku-
je konkretny typ, znany w momencie kom-
pilacji, ale nie jest to literał, więc informa-
cja o typie jest nieodpowiednia dla kom-
pilatora, na przykład jest to napis lub inny
identyfikator. Fabryka ukrywa przed użyt-
kownikiem mechanizm zamiany identyfi-
katora na literał dostarczany do operato-
ra new , upraszczając tworzenie obiektów
(patrz Rysunek 1).
W języku C++ podczas tworzenia obiektu
należy podać konkretny typ w formie zrozu-
miałej dla kompilatora (patrz Listing 1). Nie
możemy posłużyć się mechanizmem funkcji
wirtualnych, nie możemy także przekazać
identyfikatora typu w czasie działania, argu-
mentem operatora new może być tylko literał
oznaczający typ znany w momencie kompila-
cji. Po utworzeniu obiektu można się do nie-
go odwoływać poprzez wskaźnik lub referen-
cję na klasę bazową, ale przy tworzeniu nale-
ży podać rzeczywisty typ obiektu, nie można
użyć mechanizmu późnego wiązania (funk-
cji wirtualnych).
Fabryka skalowalna
Funkcja createObj jest prostą fabryką obiek-
tów, pozwala ona tworzyć obiekty na podsta-
wie informacji, która jest dostarczana w cza-
sie działania, ale ma wiele wad: mapowanie
identyfikatora na typ za pomocą instrukcji
Listing 1. Podczas tworzenia obiektu należy podać typ w odpowiedniej formie
class Bazowa { /* ... */ }; //przykładowa deinicja typu
class KonkretnaA : public Bazowa { /* ... */ };
Bazowa * p = new KonkretnaA ; //przy tworzeniu trzeba podać konkretny typ
//nazwa typu musi być zrozumiała dla kompilatora
Szybki start
Aby uruchomić przedstawione przykłady, należy mieć dostęp do kompilatora C++ oraz
edytora tekstu. Niektóre przykłady korzystają z udogodnień dostarczanych przez bi-
bliotekę boost::mpl, warunkiem ich uruchomienia jest instalacja bibliotek boost (w wer-
sji 1.36 lub nowszej) Na wydrukach pominięto dołączanie odpowiednich nagłówków
oraz udostępnianie przestrzeni nazw, pełne źródła dołączono jako materiały pomoc-
nicze.
30
02/2010
F abryka obiektów jest klasą, której
441718469.021.png 441718469.022.png 441718469.023.png 441718469.024.png 441718469.001.png 441718469.002.png 441718469.003.png 441718469.004.png 441718469.005.png 441718469.006.png 441718469.007.png
Fabryki obiektów
switch sprawia, że funkcja ta jest zależna od
wszystkich klas w hierarchii, jeżeli będzie
dodawana lub usuwana jakaś klasa, to mody-
fikacji będzie musiał podlegać także kod fa-
bryki; brak kontroli przy wiązaniu identyfi-
katora z typem sprawia, że musimy zapew-
nić, aby przy odczycie obiektu korzystać z
tego samego identyfikatora co przy zapisie.
Poza tym, zestaw identyfikatorów jest zaso-
bem globalnym, przy modyfikowaniu zbio-
ru klas w danej hierarchii musi on podlegać
modyfikacjom.
Fabryka skalowalna, przedstawiona na
Listingu 3, umożliwia tworzenie obiektów
na podstawie identyfikatorów, wprowadza
mniejszą liczbę zależności w porównaniu
z poprzednio omówionym rozwiązaniem,
ponieważ jest ona zależna tylko od klasy
bazowej, a nie od wszystkich klas konkret-
nych. Mniejsza liczba zależności wynika z
zastosowania wskaźnika na funkcję two-
rzącą obiekty danej klasy konkretnej oraz
przez użycie dynamicznej struktury prze-
chowującej mapowanie pomiędzy identy-
fikatorem a typem.
Klasa konkretna woła metodę registerFig ,
przekazując swój identyfikator oraz funkcję
tworzącą obiekty danej klasy. Metoda ta doda-
je element do kolekcji, można więc elastycznie
modyfikować zestaw klas, których obiekty bę-
dą tworzone przez fabrykę. Jeżeli chcemy usu-
wać wpisy, to należy zaimplementować meto-
unregister , która będzie usuwała elemen-
ty z kolekcji.
Tworzenie obiektów odbywa się w meto-
dzie create , która wyszukuje funkcję two-
rzącą dla danego identyfikatora. Jeżeli taka
funkcja zostanie znaleziona, to jest ona woła-
na (patrz Listing 3), a obiekt utworzonej kla-
sy jest zwracany.
Klasa konkretna musi dostarczyć funkcję
tworzącą obiekty, funkcja ta (patrz Listing 4)
może być umieszczona w module zawierają-
cym implementację klasy konkretnej, podob-
nie moduł ten może zawierać kod rejestrują-
cy dany typ w fabryce, tak jak pokazano na
Listingu 4.
Fabryka skalowalna jest bardziej elastycz-
na, ale też bardziej kosztowna niż rozwią-
zanie bezpośrednie (wybór typu w zależno-
ści od identyfikatora za pomocą switch lub
łańcucha if ... else ), ponieważ wymaga prze-
chowywania kolekcji wiążącej identyfikator z
funkcją tworzącą. Obiekt jest tworzony za po-
Listing 2. Przykład, w którym uzasadnione jest wykorzystanie fabryki
class Figure { //klasa bazowa
public :
enum Type { SQUARE , CIRCLE , /* ... */ }; //identyikatory klas konkretnych
virtual bool write ( ostream & os ) const = 0 ; //zapisuje obiekt
virtual bool read ( istream & is ) = 0 ; //odczytuje obiekt
};
class Square : public Figure { //jedna z klas konkretnych
public :
bool zapisz ( ostream & os ) { //zapisuje kwadraty
os << KWADRAT ; //zapisuje identyikator typu
//zapisuje poszczególne składowe
}
bool read ( istream & is ) { //odczytuje obiekt, zakładając, że jest to kwadrat
//odczytuje poszczególne składowe
}
};
//pozostałe klasy konkretne także dostarczają metody odczytu i zapisu
//...
Figure * createObj ( istream & is ) { //funkcja pełni rolę fabryki
Figure :: Type type ;
is >> type ; //odczytuje identyikator typu
Figure * obj ;
switch ( type ) { //zapewnia mapowanie pomiędzy identyikatorem a typem
case SQUARE: //w formie zrozumiałej dla kompilatora
return new Square (); //tworzy odpowiedni obiekt
case CIRCLE: /* ... */
}
}
//tworzy obiekt na podstawie identyikatora i odczytuje jego składowe
Figure * create ( istream & is ) {
Figure * obj = createObj ( is ); //tworzy obiekt odpowiedniego typu
obj -> read ( is ); //obiekt istnieje, może wykorzystać funkcje wirtualne
}
Listing 3. Fabryka skalowalna
class FigFactory {
public :
typedef Figure * ( * CreateFig )(); //wskaźnik na funkcję tworzącą obiekt
//rejestruje nowy typ
void registerFig ( int id , CreateFig fun ) {
creators_ . insert ( value_type ( id , fun ) ); //dodaje do kolekcji
}
//tworzy obiekt na podstawie identyikatora
Figure * create ( int id ) { //tworzy obiekt danego typu
Creators :: const_iterator i = creators_ . ind ( id );
if ( i ! = creators_ . end () ) //jeżeli znalazł odpowiedni wpis
return ( i -> second )(); //woła metodę fabryczną
return 0L ; //zwraca pusty wskaźnik, gdy nieznany identyikator
}
private :
typedef std :: map < int , CreateFig > Creators ;
Creators creators_ ; //przechowuje powiązania pomiędzy identyikatorem a funkcją
tworzącą
������
�������
���������������������
������
};
Rysunek 1. Tworzenie obiektów przez fabrykę
www.sdjournal.org
31
441718469.008.png 441718469.009.png 441718469.010.png 441718469.011.png 441718469.012.png 441718469.013.png 441718469.014.png 441718469.015.png 441718469.016.png 441718469.017.png 441718469.018.png
Programowanie C++
średnictwem tej funkcji, więc tworzenie trwa
dłużej (jeden skok więcej w porównaniu z
metodą bezpośrednią).
Aby zaimplementować w pełni funkcjo-
nalną fabrykę obiektów, należy uwzględ-
nić dodatkowe zagadnienia. Po pierw-
sze, problem dostarczania odpowiedniego
obiektu fabryki wymaganego przy rejestra-
cji klas (na wydruku 4 została użyta funk-
cja getFactory). Często stosowanym roz-
wiązaniem jest singleton. Po drugie, należy
zarządzać czasem życia powołanych obiek-
tów, fabryka tworzy obiekty na stercie, ale
kto ma je zwalniać? W tym celu warto po-
służyć się sprytnymi wskaźnikami (patrz
SDJ 11/2009). Kolejnym zadaniem jest
wiązanie identyfikatora z typem, aby wy-
kluczyć możliwości pomyłek. Można od-
powiedzialność dostarczania identyfika-
torów przenieść na fabrykę, obiekt ten ma
może tworzyć unikalne identyfikatory, zaś
klasy, które są rejestrowane w fabryce, mo-
gą ten identyfikator przechowywać w skła-
dowej statycznej (patrz Listing 5). Obiekty
klas będą wykorzystywały ten identyfika-
tor podczas zapisu, natomiast fabryka wy-
korzystuje go podczas odczytu.
Inną możliwością jest generowanie identy-
fikatora przez mechanizmy kompilatora, na
przykład używając struktury type_id , wtedy
nie trzeba zarządzać nim w fabryce.
Inicjacja fabryki skalowalnej (rejestracja
typów) może być uproszczona, jeżeli będzie
wykorzystywana kolekcja typów z biblioteki
boost::mpl (patrz SDJ 12/2009) oraz algoryt-
my, które na kolekcji operują( patrz Listing
6). Funkcja tworząca może być metodą sta-
tyczną klasy konkretnej, wtedy nie musi za-
wierać nazwy tworzonej klasy. Takie rozwią-
zanie prezentuje Listing 6.
Listing 4. Przykład funkcji tworzącej i rejestracji typu w fabryce
Figure * CreateSquare () { //funkcja tworząca dla typu konkretnego
return new Square ();
};
FigFactory & factory = getFactory (); //pobiera obiekt fabryki
factory . registerFig ( SQUARE , CreateSquare ); //rejestruje się w fabryce
Listing 5. Fabryka skalowalna zarządzająca identyikatorami
typedef shared_ptr < Figure > PFigure ; //sprytny wskaźnik
class FigFactory {
public :
typedef PFigure ( * CreateFig )(); //wskaźnik na funkcję tworzącą obiekt
int registerFig ( CreateFig fun ) { //zwraca id zarejestrowanego typu
creators_ . insert ( value_type ( currentId_ , fun ) );
return currentId_ ++ ; //zwraca identyikator
}
PFigure create ( int id ); //tworzy obiekt danego typu (patrz Listing 3)
private :
int currentId_ ; //kolejny wolny identyikator
//składowe związane z funkcjami tworzącymi
};
FigFactory & factory = FigFactory :: getInstance (); //singleton
Square :: id_ = factory . registerFig ( CreateSquare ); //ustawia identyikator
Listing 6. Rejestracja klas w fabryce skalowalnej przez algorytm biblioteki boost::mpl
class Square : public Figure {
public :
//każda klasa konkretna dostarcza metodę statyczną create
static Figure * create () { return new Square ; }
};
Podsumowanie
Opisany sposób tworzenia obiektów na pod-
stawie identyfikatora dostarczanego pod-
czas działania programu jest jednym z wzor-
ców kreacyjnych, termin został zapropono-
wany przez bandę czworga (Gamma, Helm,
Johnson, Vlissides) w książce Wzorce projek-
towe . Inne udogodnienia dotyczące tworze-
nia obiektów, takie jak fabryki prototypów
i fabryki abstrakcyjne, są tematem jednego z
kolejnych artykułów.
struct RegisterFigure { //szablon użyty do rejestracji
template < typename T > void operator ()( T ) {
T :: id_ = Factory :: getInstance (). registerFig ( T :: create );
}
};
typedef mpl :: vector < Square , Circle > Types ; //kolekcja typów dla klas konkretnych
mpl :: for_each < Types > ( RegisterFigure () ); //woła w czasie wykonania operację dla
każdego typu
W Sieci
http://www.boost.org – dokumentacja bibliotek boost ;
http://www.open-std.org – dokumenty opisujące nowy standard C++ .
Więcej w książce
Zagadnienia dotyczące współcześnie stosowanych technik w języku C++, wzorce projekto-
we, programowanie generyczne, prawidłowe zarządzanie zasobami przy stosowaniu wy-
jątków, programowanie wielowątkowe, ilustrowane przykładami stosowanymi w bibliotece
standardowej i bibliotekach boost, zostały opisane w książce Średnio zaawansowane progra-
mowanie w C++ , która ukaże się niebawem.
ROBERT NOWAK
Adiunkt w Zakładzie Sztucznej Inteligencji Insty-
tutu Systemów Elektronicznych Politechniki War-
szawskiej, zainteresowany tworzeniem aplika-
cji dla biologii i medycyny, programuje w C++ od
ponad 10 lat.
Kontakt z autorem:rno@o2.pl
32
02/2010
441718469.019.png 441718469.020.png
 
Zgłoś jeśli naruszono regulamin