Jak poprawnie wdrożyć fabryczny wzorzec metody w C ++

329

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 constlub 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.

Kos
źródło
7
@Zac, chociaż tytuł jest bardzo podobny, rzeczywiste pytania są IMHO różne.
Péter Török
2
Dobry duplikat, ale tekst tego pytania jest cenny sam w sobie.
dmckee --- były moderator kociak
7
Dwa lata po zadaniu tego mam kilka punktów do dodania: 1) To pytanie dotyczy kilku wzorców projektowych (fabryka [streszczenie], konstruktor, nazywacie to, nie lubię zagłębiać się w ich taksonomię). 2) Rzeczywistym zagadnieniem omawianym tutaj jest „jak w sposób czysty oddzielić przydział pamięci obiektu od konstrukcji obiektu?”.
Kos,
1
@Dennis: tylko jeśli tego nie zrobisz 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).
Boris Dalstein,
1
@Boris @Dennis można również wyrazić to bardzo wyraźnie, zwracając unique_ptr<T>zamiast T*.
Kos

Odpowiedzi:

107

Przede wszystkim zdarzają się przypadki, gdy konstrukcja obiektu jest wystarczająco złożonym zadaniem, aby uzasadnić jego ekstrakcję do innej klasy.

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.

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

Można to łatwo obejść:

struct Cartesian {
  inline Cartesian(float x, float y): x(x), y(y) {}
  float x, y;
};
struct Polar {
  inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
  float angle, magnitude;
};
Vec2(const Cartesian &cartesian);
Vec2(const Polar &polar);

Jedyną wadą jest to, że wygląda na trochę pełną:

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));

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:

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();

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.

Siergiej Tachenov
źródło
21
+1 dla struktur kartezjańskich i polarnych. Zasadniczo najlepiej jest tworzyć klasy i struktury, które bezpośrednio reprezentują dane, dla których są przeznaczone (w przeciwieństwie do ogólnej struktury Vec). Twoja fabryka jest również dobrym przykładem, ale twój przykład nie ilustruje, kto jest właścicielem wskaźnika „a”. Jeśli fabryka „f” jest jej właścicielem, prawdopodobnie zostanie zniszczona, gdy „f” opuści zakres, ale jeśli „f” nie jest jej właścicielem, ważne jest, aby deweloper pamiętał o zwolnieniu tej pamięci, inaczej wyciek pamięci może pojawić się.
David Peterson
1
Oczywiście obiekt można usunąć przez odniesienie! Zobacz stackoverflow.com/a/752699/404734 To oczywiście rodzi pytanie, czy mądrze jest zwrócić pamięć dynamiczną przez odniesienie, z powodu problemu potencjalnego przypisania wartości zwracanej przez kopię (osoba dzwoniąca może oczywiście również zrobić coś jak int a = * returnAPoninterToInt (), a następnie napotkałby ten sam problem, jeśli zostanie zwrócona pamięć dynamicznie alcoated, jak w przypadku referencji, ale w wersji wskaźnikowej użytkownik musi jawnie się wyrejestrować zamiast po prostu zapomnieć o jawnym odwołaniu, aby się pomylić) .
Kaiserludi
1
@Kaiserludi, nice point. Nie myślałem o tym, ale wciąż jest to „zły” sposób na robienie różnych rzeczy. Zredagowałem moją odpowiedź, aby to odzwierciedlić.
Siergiej Tachenov
A co z tworzeniem różnych klas niepolimorficznych, które są niezmienne? Czy wzorzec fabryczny jest odpowiedni do użycia w C ++?
daaxix
@daaxix, dlaczego potrzebowałbyś fabryki do tworzenia instancji klasy niepolimorficznej? Nie rozumiem, co ma wspólnego z niezmiennością.
Siergiej Tachenov
49

Prosty przykład fabryki:

// Factory returns object and ownership
// Caller responsible for deletion.
#include <memory>
class FactoryReleaseOwnership{
  public:
    std::unique_ptr<Foo> createFooInSomeWay(){
      return std::unique_ptr<Foo>(new Foo(some, args));
    }
};

// Factory retains object ownership
// Thus returning a reference.
#include <boost/ptr_container/ptr_vector.hpp>
class FactoryRetainOwnership{
  boost::ptr_vector<Foo>  myFoo;
  public:
    Foo& createFooInSomeWay(){
      // Must take care that factory last longer than all references.
      // Could make myFoo static so it last as long as the application.
      myFoo.push_back(new Foo(some, args));
      return myFoo.back();
    }
};
Martin York
źródło
2
@LokiAstari Ponieważ użycie inteligentnych wskaźników jest najprostszym sposobem na utratę kontroli nad pamięcią. Kontrola nad którymi języki C / C ++ są najwyższe w porównaniu z innymi językami i z których zyskują największą przewagę. Nie wspominając o tym, że inteligentne wskaźniki generują narzut pamięci podobny do innych zarządzanych języków. Jeśli chcesz wygody automatycznego zarządzania pamięcią, zacznij programować w Javie lub C #, ale nie umieszczaj tego bałaganu w C / C ++.
luke1985
45
@ lukasz1985 unique_ptrw 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ą.
TheCppZoo
1
@EdMaster: Nie odpowiedziałem wcześniej, ponieważ oczywiście trollował. Proszę nie karmić trolla.
Martin York,
17
@LokiAstari może być trollem, ale to, co mówi, może dezorientować ludzi
TheCppZoo
1
@yau: Tak. Ale: 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.
Martin York,
41

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:

struct linear {
    linear(float x, float y) : x_(x), y_(y){}
    float x_;
    float y_;
};

struct polar {
    polar(float angle, float magnitude) : angle_(angle),  magnitude_(magnitude) {}
    float angle_;
    float magnitude_;
};


struct Vec2 {
    explicit Vec2(const linear &l) { /* ... */ }
    explicit Vec2(const polar &p) { /* ... */ }
};

Co pozwala pisać takie rzeczy jak:

Vec2 v(linear(1.0, 2.0));

Opcja 2:

możesz używać „znaczników”, tak jak STL robi to z iteratorami i tym podobne. Na przykład:

struct linear_coord_tag linear_coord {}; // declare type and a global
struct polar_coord_tag polar_coord {};

struct Vec2 {
    Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
    Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
};

To drugie podejście pozwala napisać kod, który wygląda następująco:

Vec2 v(1.0, 2.0, linear_coord);

co jest również miłe i wyraziste, pozwalając jednocześnie mieć unikalne prototypy dla każdego konstruktora.

Evan Teran
źródło
29

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.

#ifndef FACTORY_H
#define FACTORY_H

#include <QMap>
#include <QString>

template <typename T>
class Factory
{
public:
    template <typename TDerived>
    void registerType(QString name)
    {
        static_assert(std::is_base_of<T, TDerived>::value, "Factory::registerType doesn't accept this type because doesn't derive from base class");
        _createFuncs[name] = &createFunc<TDerived>;
    }

    T* create(QString name) {
        typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name);
        if (it != _createFuncs.end()) {
            return it.value()();
        }
        return nullptr;
    }

private:
    template <typename TDerived>
    static T* createFunc()
    {
        return new TDerived();
    }

    typedef T* (*PCreateFunc)();
    QMap<QString,PCreateFunc> _createFuncs;
};

#endif // FACTORY_H

Przykładowe użycie:

Factory<BaseClass> f;
f.registerType<Descendant1>("Descendant1");
f.registerType<Descendant2>("Descendant2");
Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1"));
Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2"));
BaseClass *b1 = f.create("Descendant1");
BaseClass *b2 = f.create("Descendant2");
Mabg
źródło
17

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:

  • Zwraca wyniki metody fabrycznej według wartości oraz
  • Podaj tani konstruktor ruchu .

Przykład:

struct sandwich {
  // Factory methods.
  static sandwich ham();
  static sandwich spam();
  // Move constructor.
  sandwich(sandwich &&);
  // etc.
};

Następnie możesz konstruować obiekty na stosie:

sandwich mine{sandwich::ham()};

Jako podobiekty innych rzeczy:

auto lunch = std::make_pair(sandwich::spam(), apple{});

Lub dynamicznie przydzielane:

auto ptr = std::make_shared<sandwich>(sandwich::ham());

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.

mbrcknl
źródło
1
Użyłem tego szeroko podczas pakowania zasobów OpenGL. Usunięte konstruktory kopiowania i przypisanie kopii wymuszające użycie semantyki przenoszenia. Następnie stworzyłem kilka statycznych metod fabrycznych do tworzenia każdego rodzaju zasobów. Było to o wiele bardziej czytelne niż środowisko uruchomieniowe OpenGL oparte na wyliczaniu, które często ma kilka nadmiarowych parametrów funkcji w zależności od przekazanego wyliczenia. To bardzo przydatny wzór, zaskoczony, że ta odpowiedź nie jest wyżej.
Fibbles
11

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).

Jerry Coffin
źródło
1
Nawet jeśli jest przestarzały (co kwestionuję), nadal jest w pełni sprawny. Nadal używam Fabryki opartej na MC ++ D w nowym projekcie C ++ 14, aby uzyskać doskonały efekt! Co więcej, wzory Factory i Singleton są prawdopodobnie najmniej przestarzałymi częściami. Podczas gdy fragmenty Lokiego lubią Functionmanipulacje typu można zastąpić std::functioni, <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.
metal
5

Nie staram się odpowiadać na wszystkie moje pytania, ponieważ uważam, że jest on zbyt szeroki. Tylko kilka notatek:

zdarzają się przypadki, gdy konstrukcja obiektu jest wystarczająco złożonym zadaniem, aby uzasadnić jego ekstrakcję do innej klasy.

Ta klasa jest w rzeczywistości Konstruktorem , a nie Fabryką.

W ogólnym przypadku nie chcę zmuszać użytkowników fabryki do ograniczania się do alokacji dynamicznej.

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.

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.

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.

Péter Török
źródło
Dziękuję za odpowiedź! Ale czy mógłbyś wyjaśnić, w jaki sposób użycie inteligentnego wskaźnika uwolniłoby ograniczenie dynamicznej alokacji? Nie całkiem zrozumiałem tę część.
Kos
@Kos, dzięki inteligentnym wskaźnikom możesz ukryć przydział / dezalokację rzeczywistego obiektu przed użytkownikami. Widzą tylko enkapsulujący inteligentny wskaźnik, który dla świata zewnętrznego zachowuje się jak obiekt przydzielony statycznie.
Péter Török,
@ Ko, nie w ścisłym tego słowa znaczeniu, AFAIR. Podajesz obiekt, który chcesz owinąć, który w pewnym momencie prawdopodobnie przypisałeś dynamicznie. Następnie inteligentny wskaźnik przejmuje go na własność i zapewnia, że ​​zostanie on odpowiednio zniszczony, gdy nie jest już potrzebny (o czasie decyduje inaczej dla różnych rodzajów inteligentnych wskaźników).
Péter Török
3

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.

template <class base, class... params> class factory {
public:
  factory() {}
  factory(const factory &) = delete;
  factory &operator=(const factory &) = delete;

  auto create(const std::string name, params... args) {
    auto key = your_hash_func(name.c_str(), name.size());
    return std::move(create(key, args...));
  }

  auto create(key_t key, params... args) {
    std::unique_ptr<base> obj{creators_[key](args...)};
    return obj;
  }

  void register_creator(const std::string name,
                        std::function<base *(params...)> &&creator) {
    auto key = your_hash_func(name.c_str(), name.size());
    creators_[key] = std::move(creator);
  }

protected:
  std::unordered_map<key_t, std::function<base *(params...)>> creators_;
};

Przykład użycia.

class base {
public:
  base(int val) : val_(val) {}

  virtual ~base() { std::cout << "base destroyed\n"; }

protected:
  int val_ = 0;
};

class foo : public base {
public:
  foo(int val) : base(val) { std::cout << "foo " << val << " \n"; }

  static foo *create(int val) { return new foo(val); }

  virtual ~foo() { std::cout << "foo destroyed\n"; }
};

class bar : public base {
public:
  bar(int val) : base(val) { std::cout << "bar " << val << "\n"; }

  static bar *create(int val) { return new bar(val); }

  virtual ~bar() { std::cout << "bar destroyed\n"; }
};

int main() {
  common::factory<base, int> factory;

  auto foo_creator = std::bind(&foo::create, std::placeholders::_1);
  auto bar_creator = std::bind(&bar::create, std::placeholders::_1);

  factory.register_creator("foo", foo_creator);
  factory.register_creator("bar", bar_creator);

  {
    auto foo_obj = std::move(factory.create("foo", 80));
    foo_obj.reset();
  }

  {
    auto bar_obj = std::move(factory.create("bar", 90));
    bar_obj.reset();
  }
}
DAG
źródło
Ładnie mi wygląda. Jak zaimplementowałbyś (może trochę makro magii) rejestrację statyczną? Wyobraź sobie, że klasa podstawowa jest klasą obsługującą obiekty. Klasy pochodne zapewniają specjalny rodzaj obsługi tych obiektów. I chcesz stopniowo dodawać różne rodzaje usług, dodając klasę pochodną bazową dla każdego z tych rodzajów usług.
St0fF,
2

Wzór fabryczny

class Point
{
public:
  static Point Cartesian(double x, double y);
private:
};

A jeśli Twój kompilator nie obsługuje Optymalizacji wartości zwrotnej, porzuć go, prawdopodobnie wcale nie zawiera dużej optymalizacji ...

Matthieu M.
źródło
Czy to naprawdę można uznać za wdrożenie fabrycznego wzorca?
Dennis
1
@Dennis: Wydaje mi się, że to zdegenerowana sprawa. Problem Factorypolega 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.
Matthieu M.
Gdyby tylko zmiana kompilatora byłaby tak łatwa, jak się wydaje: :)
rozina
@rozina: :) Działa dobrze w Linuksie (gcc / clang są wyjątkowo kompatybilne); Przyznaję, że Windows jest wciąż stosunkowo zamknięty, choć powinien być lepszy na platformie 64-bitowej (mniej patentów, jeśli dobrze pamiętam).
Matthieu M.
A potem masz cały osadzony świat z kilkoma subkompilatorami .. :) Pracuję z takim, który nie ma optymalizacji wartości zwrotnej. Chciałbym, żeby tak było. Niestety zmiana nie jest w tej chwili opcją. Mam nadzieję, że w przyszłości zostanie zaktualizowany lub dokonamy zamiany na coś innego :)
rozina