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. tworzyć obiekty na podstawie identyfikato- Dowiesz się: Powinieneś wiedzieć: ra, rolę tę pełni funkcja createObj. Po utwo- " Jak tworzyć obiekty w C++; " Jak pisać proste programy w C++; rzeniu obiektu możemy wczytać składowe, " Co to jest wzorzec fabryki. " Co to jest dziedziczenie i funkcje wirtualne. 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ó- Funkcje fabryczne re dostarczą identyfikator dla danego typu, Przykład wykorzystania fabryki obiektów ponieważ możemy wykorzystać mechanizm Poziom został pokazany na Listingu 2, gdzie poka- funkcji wirtualnych. Posiadając wskaznik lub trudności zano funkcję create tworzącą obiekty klas referencję na klasę bazową, wołamy meto- na podstawie danych zapisanych w strumie- dę write, która zapisuje identyfikator klasy niu wejściowym, na przykład w pliku. Funk- konkretnej oraz jej składowe. Odczyt obiek- cja ta dostarcza obiekt odpowiedniego typu tu jest o wiele bardziej złożony niż zapis ze abryka obiektów jest klasą, której konkretnego dla hierarchii Figure. Metody względu na to, że zapis posługuje się istnie- obiekty pośredniczą przy tworze- zapisu (write) oraz odczytu (read) są do- jącymi obiektami, natomiast odczyt musi je Fniu innych obiektów. Dostarcza- starczane przez każdą z klas konkretnych. tworzyć. na informacja jednoznacznie identyfiku- Jeżeli chcemy obiekt utworzyć (odczytać), je konkretny typ, znany w momencie kom- to typ obiektu jest dostarczany w czasie dzia- Fabryka skalowalna pilacji, ale nie jest to literał, więc informa- łania, jest on wyznaczany przez identyfika- Funkcja createObj jest prostą fabryką obiek- cja o typie jest nieodpowiednia dla kom- tor dostarczany przez strumień. Ponieważ tów, pozwala ona tworzyć obiekty na podsta- pilatora, na przykład jest to napis lub inny tak dostarczany identyfikator nie jest ak- wie informacji, która jest dostarczana w cza- identyfikator. Fabryka ukrywa przed użyt- ceptowany jako argument dla operacji new, sie działania, ale ma wiele wad: mapowanie kownikiem mechanizm zamiany identyfi- należy wykorzystać fabrykę, która pozwoli identyfikatora na typ za pomocą instrukcji katora na literał dostarczany do operato- ra new, upraszczając tworzenie obiektów Listing 1. Podczas tworzenia obiektu należy podać typ w odpowiedniej formie (patrz Rysunek 1). W języku C++ podczas tworzenia obiektu class Bazowa { /* ... */ }; //przykładowa definicja typu należy podać konkretny typ w formie zrozu- class KonkretnaA : public Bazowa { /* ... */ }; miałej dla kompilatora (patrz Listing 1). Nie Bazowa* p = new KonkretnaA; //przy tworzeniu trzeba podać konkretny typ możemy posłużyć się mechanizmem funkcji //nazwa typu musi być zrozumiała dla kompilatora 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- Szybki start cji. Po utworzeniu obiektu można się do nie- 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- go odwoływać poprzez wskaznik lub referen- bliotekę boost::mpl, warunkiem ich uruchomienia jest instalacja bibliotek boost (w wer- cję na klasę bazową, ale przy tworzeniu nale- sji 1.36 lub nowszej) Na wydrukach pominięto dołączanie odpowiednich nagłówków ży podać rzeczywisty typ obiektu, nie można oraz udostępnianie przestrzeni nazw, pełne zródła dołączono jako materiały pomoc- użyć mechanizmu póznego wiązania (funk- nicze. cji wirtualnych). 30 02/2010 Fabryki obiektów switch sprawia, że funkcja ta jest zależna od nie moduł ten może zawierać kod rejestrują- zanie bezpośrednie (wybór typu w zależno- wszystkich klas w hierarchii, jeżeli będzie cy dany typ w fabryce, tak jak pokazano na ści od identyfikatora za pomocą switch lub dodawana lub usuwana jakaś klasa, to mody- Listingu 4. łańcucha if ... else), ponieważ wymaga prze- fikacji będzie musiał podlegać także kod fa- Fabryka skalowalna jest bardziej elastycz- chowywania kolekcji wiążącej identyfikator z bryki; brak kontroli przy wiązaniu identyfi- na, ale też bardziej kosztowna niż rozwią- funkcją tworzącą. Obiekt jest tworzony za po- katora z typem sprawia, że musimy zapew- nić, aby przy odczycie obiektu korzystać z Listing 2. Przykład, w którym uzasadnione jest wykorzystanie fabryki tego samego identyfikatora co przy zapisie. Poza tym, zestaw identyfikatorów jest zaso- class Figure { //klasa bazowa bem globalnym, przy modyfikowaniu zbio- public: ru klas w danej hierarchii musi on podlegać enum Type { SQUARE, CIRCLE, /* ... */ };//identyfikatory klas konkretnych modyfikacjom. virtual bool write(ostream& os) const = 0;//zapisuje obiekt Fabryka skalowalna, przedstawiona na virtual bool read(istream& is) = 0;//odczytuje obiekt Listingu 3, umożliwia tworzenie obiektów }; na podstawie identyfikatorów, wprowadza class Square : public Figure {//jedna z klas konkretnych mniejszą liczbę zależności w porównaniu public: z poprzednio omówionym rozwiązaniem, bool zapisz(ostream& os) {//zapisuje kwadraty ponieważ jest ona zależna tylko od klasy os << KWADRAT;//zapisuje identyfikator typu bazowej, a nie od wszystkich klas konkret- //zapisuje poszczególne składowe nych. Mniejsza liczba zależności wynika z } zastosowania wskaznika na funkcję two- bool read(istream& is) {//odczytuje obiekt, zakładając, że jest to kwadrat rzącą obiekty danej klasy konkretnej oraz //odczytuje poszczególne składowe przez użycie dynamicznej struktury prze- } chowującej mapowanie pomiędzy identy- }; fikatorem a typem. //pozostałe klasy konkretne także dostarczają metody odczytu i zapisu Klasa konkretna woła metodę registerFig, //... przekazując swój identyfikator oraz funkcję Figure* createObj(istream& is) {//funkcja pełni rolę fabryki tworzącą obiekty danej klasy. Metoda ta doda- Figure::Type type; je element do kolekcji, można więc elastycznie is >> type; //odczytuje identyfikator typu modyfikować zestaw klas, których obiekty bę- Figure* obj; dą tworzone przez fabrykę. Jeżeli chcemy usu- switch(type) { //zapewnia mapowanie pomiędzy identyfikatorem a typem wać wpisy, to należy zaimplementować meto- case SQUARE://w formie zrozumiałej dla kompilatora dę unregister, która będzie usuwała elemen- return new Square(); //tworzy odpowiedni obiekt ty z kolekcji. case CIRCLE: /* ... */ Tworzenie obiektów odbywa się w meto- } dzie create, która wyszukuje funkcję two- } rzącą dla danego identyfikatora. Jeżeli taka //tworzy obiekt na podstawie identyfikatora i odczytuje jego składowe funkcja zostanie znaleziona, to jest ona woła- Figure* create(istream& is) { na (patrz Listing 3), a obiekt utworzonej kla- Figure* obj = createObj(is); //tworzy obiekt odpowiedniego typu sy jest zwracany. obj->read(is);//obiekt istnieje, może wykorzystać funkcje wirtualne Klasa konkretna musi dostarczyć funkcję } tworzącą obiekty, funkcja ta (patrz Listing 4) Listing 3. Fabryka skalowalna może być umieszczona w module zawierają- cym implementację klasy konkretnej, podob- class FigFactory { public: typedef Figure* (*CreateFig)(); //wskaznik 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 identyfikatora klient fabryka Figure* create(int id) { //tworzy obiekt danego typu Creators::const_iterator i = creators_.find(id); if(i ! = creators_.end() ) //jeżeli znalazł odpowiedni wpis utwórz(identyfikator) obiekt return (i->second)(); //woła metodę fabryczną return 0L; //zwraca pusty wskaznik, gdy nieznany identyfikator } private: typedef std::map Creators; Creators creators_;//przechowuje powiązania pomiędzy identyfikatorem a funkcją tworzącą }; Rysunek 1. Tworzenie obiektów przez fabrykę 02/2010 www.sdjournal.org 31 Programowanie C++ średnictwem tej funkcji, więc tworzenie trwa Aby zaimplementować w pełni funkcjo- sze, problem dostarczania odpowiedniego dłużej (jeden skok więcej w porównaniu z nalną fabrykę obiektów, należy uwzględ- obiektu fabryki wymaganego przy rejestra- metodą bezpośrednią). nić dodatkowe zagadnienia. Po pierw- cji klas (na wydruku 4 została użyta funk- cja getFactory). Często stosowanym roz- wiązaniem jest singleton. Po drugie, należy Listing 4. Przykład funkcji tworzącej i rejestracji typu w fabryce zarządzać czasem życia powołanych obiek- Figure* CreateSquare() { //funkcja tworząca dla typu konkretnego tów, fabryka tworzy obiekty na stercie, ale return new Square(); kto ma je zwalniać? W tym celu warto po- }; służyć się sprytnymi wskaznikami (patrz FigFactory& factory = getFactory();//pobiera obiekt fabryki SDJ 11/2009). Kolejnym zadaniem jest factory.registerFig(SQUARE, CreateSquare); //rejestruje się w fabryce wiązanie identyfikatora z typem, aby wy- kluczyć możliwości pomyłek. Można od- Listing 5. Fabryka skalowalna zarządzająca identyfikatorami powiedzialność dostarczania identyfika- typedef shared_ptr