Jest jedna rzecz w C ++, która od dłuższego czasu sprawia, że czuję się niekomfortowo, ponieważ szczerze mówiąc, nie wiem, jak to zrobić, chociaż brzmi to prosto:
Jak poprawnie wdrożyć metodę fabryczną w C ++?
Cel: umożliwienie klientowi utworzenia instancji jakiegoś obiektu przy użyciu metod fabrycznych zamiast konstruktorów obiektu, bez niedopuszczalnych konsekwencji i obniżenia wydajności.
Przez „wzorzec metod fabrycznych” rozumiem zarówno statyczne metody fabryczne wewnątrz obiektu lub metody zdefiniowane w innej klasie, jak i funkcje globalne. Po prostu ogólnie „koncepcja przekierowania normalnego sposobu tworzenia instancji klasy X w inne miejsce niż konstruktor”.
Pozwól mi przejrzeć kilka możliwych odpowiedzi, o których myślałem.
0) Nie twórz fabryk, konstruktorów.
Brzmi nieźle (i rzeczywiście często najlepsze rozwiązanie), ale nie jest to ogólne lekarstwo. Przede wszystkim zdarzają się przypadki, gdy konstrukcja obiektu jest wystarczająco złożonym zadaniem, aby uzasadnić jego ekstrakcję do innej klasy. Ale nawet odkładając ten fakt na bok, nawet w przypadku prostych obiektów wykorzystujących tylko konstruktory często tego nie robi.
Najprostszym przykładem, jaki znam, jest klasa 2-D Vector. Tak proste, ale trudne. Chcę być w stanie zbudować go zarówno ze współrzędnych kartezjańskich, jak i biegunowych. Oczywiście nie mogę:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Mój naturalny sposób myślenia to:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Co zamiast konstruktorów prowadzi mnie do użycia statycznych metod fabrycznych ... co w istocie oznacza, że w jakiś sposób implementuję wzorzec fabryczny („klasa staje się własną fabryką”). Wygląda to ładnie (i pasowałoby do tego konkretnego przypadku), ale w niektórych przypadkach zawodzi, co opiszę w punkcie 2. Czytaj dalej.
inny przypadek: próba przeciążenia dwoma nieprzezroczystymi typami plików niektórych API (takich jak GUID niepowiązanych domen lub GUID i pole bitowe), typy semantycznie zupełnie inne (więc - teoretycznie - prawidłowe przeciążenia), ale które w rzeczywistości okazują się to samo - jak niepodpisane inty lub puste wskaźniki.
1) The Java Way
Java ma to proste, ponieważ mamy tylko obiekty dynamicznie alokowane. Tworzenie fabryki jest tak proste, jak:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
W C ++ oznacza to:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Fajne? Rzeczywiście często. Ale to zmusza użytkownika do korzystania tylko z alokacji dynamicznej. Alokacja statyczna sprawia, że C ++ jest skomplikowany, ale także często czyni go potężnym. Ponadto uważam, że istnieją pewne cele (słowo kluczowe: osadzone), które nie pozwalają na dynamiczną alokację. A to nie oznacza, że użytkownicy tych platform lubią pisać czyste OOP.
Zresztą poza filozofią: w ogólnym przypadku nie chcę zmuszać użytkowników fabryki do ograniczania się do dynamicznej alokacji.
2) Zwrot według wartości
OK, więc wiemy, że 1) jest fajny, gdy chcemy dynamicznej alokacji. Dlaczego nie dodamy do tego statycznego przydziału?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Co? Nie możemy przeciążać według typu zwrotu? Oczywiście, że nie możemy. Zmieńmy więc nazwy metod, aby to odzwierciedlić. I tak, napisałem powyższy przykład niepoprawnego kodu, aby podkreślić, jak bardzo nie lubię potrzeby zmiany nazwy metody, na przykład dlatego, że nie możemy teraz poprawnie zaimplementować projektu fabryki niezależnego od języka, ponieważ musimy zmienić nazwy - i każdy użytkownik tego kodu będzie musiał pamiętać różnicę implementacji od specyfikacji.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK ... mamy to. To brzydkie, ponieważ musimy zmienić nazwę metody. Jest niedoskonały, ponieważ musimy dwukrotnie napisać ten sam kod. Ale po zakończeniu działa. Dobrze?
Zwykle. Ale czasami tak nie jest. Tworząc Foo, faktycznie polegamy na kompilacji, która przeprowadzi dla nas optymalizację wartości zwracanej, ponieważ standard C ++ jest wystarczająco życzliwy, aby dostawcy kompilatora nie określili, kiedy obiekt zostanie utworzony w miejscu, a kiedy zostanie skopiowany podczas zwracania obiekt tymczasowy według wartości w C ++. Więc jeśli kopiowanie Foo jest kosztowne, takie podejście jest ryzykowne.
A co, jeśli Foo nie będzie w ogóle możliwe do skopiowania? Cóż, doh. ( Zauważ, że w C ++ 17 z gwarantowanym usunięciem kopii brak możliwości kopiowania nie stanowi już problemu dla powyższego kodu )
Wniosek: Utworzenie fabryki poprzez zwrócenie obiektu jest rzeczywiście rozwiązaniem dla niektórych przypadków (takich jak wcześniej wspomniany wektor 2D), ale nadal nie jest ogólnym zamiennikiem dla konstruktorów.
3) Konstrukcja dwufazowa
Inną rzeczą, którą prawdopodobnie wymyśliłby, jest oddzielenie kwestii alokacji obiektów i jej inicjalizacji. Zwykle powoduje to taki kod:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
Można pomyśleć, że działa jak urok. Jedyna cena, za którą płacimy w naszym kodzie ...
Ponieważ napisałem to wszystko i zostawiłem to jako ostatnie, muszę też tego nie lubić. :) Dlaczego?
Przede wszystkim ... szczerze nie podoba mi się koncepcja konstrukcji dwufazowej i czuję się winny, kiedy z niej korzystam. Jeśli projektuję moje obiekty z założeniem, że „jeśli istnieje, to jest w poprawnym stanie”, czuję, że mój kod jest bezpieczniejszy i mniej podatny na błędy. Lubię to w ten sposób.
Konieczność porzucenia tej konwencji ORAZ zmiana projektu mojego obiektu tylko w celu wytworzenia fabryki jest ... no cóż, niewygodna.
Wiem, że powyższe nie przekonuje wielu ludzi, więc pozwólcie, że podam więcej solidnych argumentów. Korzystając z konstrukcji dwufazowej, nie można:
- inicjowanie
const
lub referencyjne zmienne składowe, - przekazywać argumenty do konstruktorów klas bazowych i konstruktorów obiektów członkowskich.
I prawdopodobnie może być jeszcze kilka wad, o których nie mogę teraz myśleć, i nawet nie czuję się szczególnie zobowiązany, ponieważ powyższe punkty pocisków mnie już przekonały.
Tak więc: nawet w pobliżu dobrego ogólnego rozwiązania dla wdrożenia fabryki.
Wnioski:
Chcemy mieć sposób tworzenia obiektów, który:
- pozwalają na jednolite tworzenie instancji bez względu na przydział,
- nadaj różne, znaczące nazwy metodom budowy (nie polegając na przeciążeniu argumentami),
- nie wprowadzać znaczącego trafienia wydajnościowego, a najlepiej znacznego uderzenia kodu, szczególnie po stronie klienta,
- być ogólne, jak w: możliwe do wprowadzenia dla dowolnej klasy.
Wierzę, że udowodniłem, że sposoby, o których wspomniałem, nie spełniają tych wymagań.
Jakieś wskazówki? Proszę o rozwiązanie, nie chcę myśleć, że ten język nie pozwoli mi poprawnie wdrożyć tak trywialnej koncepcji.
delete
. Tego rodzaju metody są całkowicie w porządku, o ile jest „udokumentowane” (kod źródłowy to dokumentacja ;-)), że wywołujący przejmuje na własność wskaźnik (czytaj: jest odpowiedzialny za usunięcie go, gdy jest to właściwe).unique_ptr<T>
zamiastT*
.Odpowiedzi:
Uważam, że ten punkt jest nieprawidłowy. Złożoność tak naprawdę nie ma znaczenia. Znaczenie ma to, co robi. Jeśli obiekt można zbudować w jednym kroku (inaczej niż we wzorcu konstruktora), konstruktor jest właściwym miejscem do tego. Jeśli naprawdę potrzebujesz innej klasy do wykonania zadania, powinna to być klasa pomocnicza, która i tak jest używana przez konstruktor.
Można to łatwo obejść:
Jedyną wadą jest to, że wygląda na trochę pełną:
Ale dobrą rzeczą jest to, że możesz natychmiast zobaczyć, jakiego typu współrzędnych używasz, a jednocześnie nie musisz się martwić o kopiowanie. Jeśli chcesz kopiować, a jest to kosztowne (co zostało udowodnione przez profilowanie, oczywiście), możesz użyć czegoś takiego jak wspólne klasy Qt aby uniknąć kopiowania narzutu.
Jeśli chodzi o typ alokacji, głównym powodem zastosowania wzorca fabrycznego jest zwykle polimorfizm. Konstruktorzy nie mogą być wirtualni, a nawet gdyby mogli, nie miałoby to większego sensu. Korzystając z przydziału statycznego lub alokacji stosu, nie można tworzyć obiektów w sposób polimorficzny, ponieważ kompilator musi znać dokładny rozmiar. Więc działa tylko ze wskaźnikami i referencjami. Zwracanie referencji z fabryki też nie działa, ponieważ chociaż obiekt może być technicznie usunięty przez referencję, może być raczej mylący i podatny na błędy, zobacz Czy praktyka zwracania zmiennej referencyjnej C ++ jest zła?na przykład. Tak więc wskaźniki są jedyną rzeczą, która pozostała, w tym również inteligentne wskaźniki. Innymi słowy, fabryki są najbardziej przydatne, gdy są używane z alokacją dynamiczną, dzięki czemu możesz wykonywać następujące czynności:
W innych przypadkach fabryki pomagają rozwiązać drobne problemy, takie jak wspomniane przeciążenia. Byłoby miło, gdyby można było używać ich w jednolity sposób, ale nie zaszkodzi to tak bardzo, że prawdopodobnie jest to niemożliwe.
źródło
Prosty przykład fabryki:
źródło
unique_ptr
w tym przykładzie nie ma narzutu wydajności. Zarządzanie zasobami, w tym pamięcią, jest jedną z największych zalet C ++ w stosunku do dowolnego innego języka, ponieważ można to zrobić bez utraty wydajności i deterministycznie, bez utraty kontroli, ale mówisz dokładnie odwrotnie. Niektórzy ludzie nie lubią rzeczy, które C ++ domyślnie robi, takich jak zarządzanie pamięcią za pomocą inteligentnych wskaźników, ale jeśli chcesz, aby wszystko było obligatoryjnie jawne, użyj C; kompromis jest o rząd wielkości mniej problemów. Myślę, że to niesprawiedliwe, że głosujesz za dobrą rekomendacją.boost::ptr_vector<>
jest nieco bardziej wydajny, ponieważ rozumie, że jest właścicielem wskaźnika, a nie deleguje pracę do podklasy. ALE główną zaletąboost::ptr_vector<>
jest to, że odsłania swoich członków przez odniesienie (nie wskaźnik), dlatego jest naprawdę łatwy w użyciu z algorytmami w standardowej bibliotece.Czy w ogóle myślałeś o tym, aby nie korzystać z fabryki, a zamiast tego dobrze korzystać z systemu typów? Mogę wymyślić dwa różne podejścia, które robią takie rzeczy:
Opcja 1:
Co pozwala pisać takie rzeczy jak:
Opcja 2:
możesz używać „znaczników”, tak jak STL robi to z iteratorami i tym podobne. Na przykład:
To drugie podejście pozwala napisać kod, który wygląda następująco:
co jest również miłe i wyraziste, pozwalając jednocześnie mieć unikalne prototypy dla każdego konstruktora.
źródło
Możesz przeczytać bardzo dobre rozwiązanie w: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus
Najlepszym rozwiązaniem są „komentarze i dyskusje”, patrz „Nie ma potrzeby tworzenia statycznych metod tworzenia”.
Z tego pomysłu zrobiłem fabrykę. Zauważ, że używam Qt, ale możesz zmienić QMap i QString dla standardowych odpowiedników.
Przykładowe użycie:
źródło
W większości zgadzam się z zaakceptowaną odpowiedzią, ale istnieje opcja C ++ 11, która nie została uwzględniona w istniejących odpowiedziach:
Przykład:
Następnie możesz konstruować obiekty na stosie:
Jako podobiekty innych rzeczy:
Lub dynamicznie przydzielane:
Kiedy mogę tego użyć?
Jeśli na konstruktorze publicznym nie jest możliwe podanie znaczących inicjatorów dla wszystkich członków klasy bez wstępnych obliczeń, to mogę przekonwertować ten konstruktor na metodę statyczną. Metoda statyczna wykonuje wstępne obliczenia, a następnie zwraca wynik wartości za pomocą prywatnego konstruktora, który wykonuje inicjalizację tylko dla członków.
Mówię „ może ”, ponieważ zależy to od tego, które podejście daje najczystszy kod bez niepotrzebnej nieefektywności.
źródło
Loki ma zarówno metodę fabryczną, jak i fabrykę abstrakcyjną . Oba są udokumentowane (obszernie) we współczesnym projekcie C ++ autorstwa Andei Alexandrescu. Metoda fabryczna jest prawdopodobnie bliższa temu, po czym się wydajesz, choć nadal jest nieco inna (przynajmniej jeśli pamięć służy, musisz zarejestrować typ, zanim fabryka będzie mogła tworzyć obiekty tego typu).
źródło
Function
manipulacje typu można zastąpićstd::function
i,<type_traits>
a podczas gdy lambda, nawlekanie wątków i odwoływanie się do wartości ma implikacje, które mogą wymagać drobnych poprawek, nie ma standardowego zastępowania singletonów fabryk, jak je opisuje.Nie staram się odpowiadać na wszystkie moje pytania, ponieważ uważam, że jest on zbyt szeroki. Tylko kilka notatek:
Ta klasa jest w rzeczywistości Konstruktorem , a nie Fabryką.
Wtedy fabryka może umieścić w inteligentnym wskaźniku. Wierzę, że w ten sposób możesz mieć swoje ciasto i je zjeść.
Eliminuje to również problemy związane ze zwrotem według wartości.
W rzeczy samej. Wszystkie wzorce projektowe mają swoje (specyficzne dla języka) ograniczenia i wady. Zaleca się stosowanie ich tylko wtedy, gdy pomagają rozwiązać problem, a nie dla nich samych.
Jeśli szukasz „idealnej” implementacji fabryki, to powodzenia.
źródło
To jest moje rozwiązanie w stylu c ++ 11. parametr „podstawa” dotyczy klasy podstawowej wszystkich podklas. twórcy, są obiektami std :: function służącymi do tworzenia instancji podklasy, mogą być powiązaniem z podklasową „statyczną funkcją składową” „create (niektóre argumenty)”. To może nie jest idealne, ale działa dla mnie. I to jest trochę „ogólne” rozwiązanie.
Przykład użycia.
źródło
Wzór fabryczny
A jeśli Twój kompilator nie obsługuje Optymalizacji wartości zwrotnej, porzuć go, prawdopodobnie wcale nie zawiera dużej optymalizacji ...
źródło
Factory
polega na tym, że jest dość ogólny i obejmuje wiele gruntów; fabryka może dodawać argumenty (w zależności od środowiska / konfiguracji) lub na przykład zapewniać buforowanie (związane z Flyweight / Pools), ale te przypadki mają sens tylko w niektórych sytuacjach.Wiem, że na to pytanie udzielono odpowiedzi 3 lata temu, ale być może tego właśnie szukałeś.
Google wydało kilka tygodni temu bibliotekę umożliwiającą łatwe i elastyczne przydzielanie obiektów dynamicznych. Oto on: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html
źródło