Jak poprawnie zaimplementować wzorzec metody fabrycznej w C++

Jest jedna rzecz w C++, która sprawia, że czuję się nieswojo od dłuższego czasu, ponieważ szczerze Nie wiem, jak to zrobić, mimo że brzmi to prosto: {]}

Jak poprawnie zaimplementować metodę Factory w C++?

Cel: umożliwienie klientowi utworzenia instancji jakiegoś obiektu przy użyciu metod fabrycznych zamiast konstruktorów obiektu, bez niedopuszczalnych konsekwencji i uderzenia wydajności.

By " Factory method pattern", Mam na myśli 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 do dowolnego miejsca poza konstruktorem".

Pozwól mi przejrzeć kilka możliwych odpowiedzi, które wymyśliłem.

0) nie twórz fabryk, twórz konstruktorów.

Brzmi to ładnie (i rzeczywiście często jest to najlepsze rozwiązanie), ale nie jest to ogólne lekarstwo. Przede wszystkim są przypadki, gdy budowa obiektu jest zadaniem na tyle złożonym, aby uzasadnić jego ekstrakcję do innej klasy. Ale nawet odkładanie tego faktu na bok, nawet dla prostych obiektów używanie tylko konstruktorów często nie wystarczy.

Najprostszym przykładem jaki znam jest klasa wektorowa 2-D. Takie proste, ale trudne. Chcę móc ją skonstruować 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);
    // ...
};

Które zamiast konstruktorów prowadzą mnie do stosowania statycznych metod fabrycznych... co zasadniczo oznacza, że implementuję w jakiś sposób 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 przez dwa nieprzezroczyste typy niektórych API (takich jak GUID niepowiązanych domen lub GUID i bitfield), typy semantycznie zupełnie inne (tak-w teorii - poprawne przeciążenia), ale które w rzeczywistości okazują się być tym samym - jak niepodpisane ints lub void pointers.


1) The Java Way

Java jest prosta, ponieważ mamy tylko dynamicznie przydzielane obiekty. Tworzenie fabryki jest tak trywialne 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++ przekłada się to na:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

Cool? Często. Ale wtedy-to zmusza użytkownika do używania tylko dynamicznej alokacji. Statyczna alokacja jest tym, co sprawia, że C++ jest złożony, ale jest również tym, co często czyni go potężnym. Ponadto, Ja uwierz, że istnieją pewne cele (słowo kluczowe: embedded), które nie pozwalają na dynamiczną alokację. I to nie oznacza, że użytkownicy tych platform lubią pisać czyste OOP.

W każdym razie, pomijając filozofię: w ogólnym przypadku nie chcę zmuszać użytkowników fabryki do ograniczenia dynamicznej alokacji.


2) Return-by-value

OK, więc wiemy, że 1) jest fajne, gdy chcemy dynamicznej alokacji. Dlaczego nie dodamy alokacji statycznej na to?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};
Co? Nie możemy przeciążyć przez typ powrotu? Oczywiście, że nie, więc zmienimy nazwy metod, żeby to odzwierciedlić. I tak, napisałem przykład nieprawidłowego kodu powyżej, aby podkreślić, jak bardzo nie lubię potrzeby zmiany nazwy metody, na przykład dlatego, że nie możemy poprawnie zaimplementować projektu fabrycznego, ponieważ musimy zmienić nazwy-i każdy użytkownik tego kodu będzie musiał pamiętać, że różnica implementacji od tej, która została zaimplementowana. Specyfikacja.
class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};
OK... no i mamy. Jest brzydka, ponieważ musimy zmienić nazwę metody. Jest niedoskonały, ponieważ musimy napisać ten sam kod dwa razy. Ale raz zrobione, to działa. Prawda? Zazwyczaj. Ale czasami tak nie jest. Podczas tworzenia Foo, tak naprawdę polegamy na kompilatorze, który wykona dla nas optymalizację zwracanej wartości, ponieważ standard C++ jest na tyle korzystny, że producenci kompilatorów nie określają, kiedy obiekt zostanie utworzony w miejscu i kiedy będzie kopiowane przy zwracaniu tymczasowego obiektu według wartości w C++. Jeśli więc foo jest drogie w kopiowaniu, takie podejście jest ryzykowne.

A co jeśli Foo nie jest w ogóle możliwe do naśladowania? Cóż, doh. (zauważ, że w C++17 z gwarantowaną kopiowalnością, nie-bycie-kopiowalnym nie jest już problemem dla powyższego kodu )

Wniosek: utworzenie fabryki poprzez zwrócenie obiektu jest rzeczywiście rozwiązaniem w niektórych przypadkach (np. wektor 2-D wcześniej wspomniany), ale nadal nie jest ogólnym zamiennikiem dla konstruktorów.


3) Budowa dwufazowa

Kolejną rzeczą, na którą ktoś pewnie wpadłby, jest oddzielenie kwestii alokacji obiektów od ich inicjalizacji. Zazwyczaj skutkuje to takim kodem:

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());
    // ...
}

Ktoś może pomyśleć, że to działa jak urok. Jedyna cena, za którą płacimy w naszym kodzie...

Skoro napisałam to wszystko i zostawiłam to jako ostatnie, też muszę tego nie lubić. :) Dlaczego?

Po pierwsze... Szczerze nie podoba mi się koncepcja konstrukcja dwufazowa i czuję się winny, gdy jej używam. Jeśli projektuję moje obiekty z twierdzeniem, że "jeśli istnieje, to jest w prawidłowym stanie", czuję, że mój kod jest bezpieczniejszy i mniej podatny na błędy. Podoba mi się to. Muszę porzucić tę konwencję i zmienić projekt mojego obiektu tylko po to, żeby zrobić z niego fabrykę.. cóż, nieporęczny.

Wiem, że powyższe nie przekona wielu ludzi, więc podam kilka bardziej solidnych argumentów. Stosując konstrukcję dwufazową, można nie można:

  • initialise const or reference member variables,
  • przekazuje argumenty konstruktorom klas bazowych i konstruktorom obiektów członkowskich.

I prawdopodobnie mogą być jeszcze jakieś wady, o których nie mogę teraz myśleć, a nawet nie czuję się szczególnie zobowiązany, ponieważ powyższe punkty przekonują mnie już.

Więc: nawet nie jest blisko dobrego ogólnego rozwiązania dla wdrożenia fabryki.


Wnioski:

Chcemy mają sposób instancjacji obiektu, który:

  • pozwalają na jednolitą instancję niezależnie od alokacji,
  • W ten sposób można określić, w jaki sposób metody konstrukcyjne mogą być używane, a co za tym idzie, w jaki sposób mogą być używane.]} Nie wprowadzaj znaczącego hitu wydajności i, najlepiej, znaczącego hitu nadmiarowego kodu, szczególnie po stronie klienta.]}
  • być ogólne, Jak W: możliwe do wprowadzenia dla każdej klasy.

Wierzę, że udowodniłem, że sposoby, o których wspomniałem, nie spełniają tych wymagań.

Jakieś wskazówki? Proszę podać mi rozwiązanie, nie chcę myśleć, że ten język nie pozwoli mi poprawnie wdrożyć tak banalnej koncepcji.
Author: davidhigh, 2011-02-25

10 answers

Po pierwsze, są przypadki, gdy Budowa obiektów jest kompleksem zadań wystarczy uzasadnić jego wydobycie do kolejna klasa.

Uważam, że ten punkt jest błędny. Złożoność nie ma znaczenia. Znaczenie jest to, co robi. Jeśli obiekt może być zbudowany w jednym kroku (nie tak jak we wzorze builder), konstruktor jest właściwym miejscem do tego. Jeśli naprawdę potrzebujesz innej klasy do wykonania zadania, to powinna to być Klasa pomocnicza, która jest używana z konstruktor.
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

Istnieje łatwe obejście tego problemu:

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 trochę gadatliwie:

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

Ale dobre jest to, że możesz od razu 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 udowodniono oczywiście profilowaniem), możesz użyć czegoś w rodzaju współdzielonych klas Qt, aby uniknąć kopiowania z góry.

Jako dla typu alokacji głównym powodem użycia wzorca fabrycznego jest zwykle polimorfizm. Konstruktorzy nie mogą być wirtualni, a nawet gdyby mogli, nie miałoby to większego sensu. Używając alokacji statycznej lub stosu, nie można tworzyć obiektów w sposób polimorficzny, ponieważ kompilator musi znać dokładny rozmiar. Działa więc tylko ze wskaźnikami i odniesieniami. I zwracanie referencji z fabryki też nie działa, bo o ile obiekt technicznie Może zostać usunięty przez referencję, to może to być dość mylące i podatne na błędy, zobacz czy praktyka zwracania zmiennej referencyjnej C++ jest zła? na przykład. Więc wskaźniki są jedyną rzeczą, która została, i że obejmuje inteligentne wskaźniki też. Innymi słowy, fabryki są najbardziej przydatne, gdy są używane z dynamiczną alokacją, więc możesz robić takie rzeczy: {]}

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

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

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

W innych przypadkach fabryki po prostu pomagają rozwiązać drobne problemy, takie jak te z przeciążeniami, o których wspomniałeś. Byłoby miło, gdyby można było ich używać w jednolity sposób, ale to nie boli, że jest to prawdopodobnie niemożliwe.

 83
Author: Sergei Tachenov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 16:47:54

Prosty Przykład Fabryczny:

// 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();
    }
};
 36
Author: Martin York,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-28 08:55:30

Czy myślałeś o tym, aby w ogóle nie używać fabryki, a zamiast tego dobrze korzystać z systemu typów? Mogę myśleć o dwóch różnych podejściach, które robią tego typu 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) { /* ... */ }
};

Który pozwala pisać takie rzeczy jak:

Vec2 v(linear(1.0, 2.0));

Opcja 2:

Możesz używać "tagów", tak jak STL robi to z iteratorami itp. 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 tak:

Vec2 v(1.0, 2.0, linear_coord);

Który jest również ładne i wyraziste, a jednocześnie pozwala mieć unikalne prototypy dla każdego konstruktora.

 35
Author: Evan Teran,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-28 19:44:19

Możesz przeczytać bardzo dobre rozwiązanie w: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

Najlepszym rozwiązaniem jest "komentarze i dyskusje", Patrz "nie ma potrzeby tworzenia statycznych metod".

Z tego pomysłu zrobiłem fabrykę. Zauważ, że używam Qt, ale możesz zmienić QMap i QString dla odpowiedników std.
#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");
 22
Author: mabg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-25 10:12:12

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 i
  • Zapewnij Tani konstruktor ruchu .

Przykład:

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

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

sandwich mine{sandwich::ham()};

Jako podobiekty innych rzeczy:

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

Lub przydzielane dynamicznie:

auto ptr = std::make_shared<sandwich>(sandwich::ham());
Kiedy mogę tego użyć?

Jeśli, na konstruktor publiczny, nie jest możliwe podanie sensownych inicjalizatorów dla wszystkich członków klasy bez wstępnego obliczenia, wtedy mogę przekonwertować ten konstruktor na statyczną metodę. Metoda statyczna wykonuje wstępne obliczenia, a następnie zwraca wynik wartości za pomocą prywatnego konstruktora, który po prostu inicjalizuje elementy składowe.

Mówię "Może ", ponieważ zależy to od tego, które podejście daje najczystszy kod, nie będąc niepotrzebnie nieefektywnym.

 11
Author: mbrcknl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-07 10:32:58

Loki ma zarówno metodę Factory , jak i Abstract Factory . Oba są udokumentowane (obszernie) w Modern C++ Design, przez Andei Alexandrescu. Metoda factory jest prawdopodobnie bliższa temu, co wydaje się być po, choć nadal jest nieco inna (przynajmniej jeśli pamięć służy, wymaga zarejestrowania typu, zanim fabryka będzie mogła tworzyć obiekty tego typu).

 10
Author: Jerry Coffin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-25 18:06:08

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

Są przypadki, gdy budowa obiektu jest zadaniem na tyle złożonym, aby uzasadnić jego ekstrakcję do innej klasy.

Ta klasa jest w rzeczywistości budowniczym , a nie fabryką.

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

Wtedy możesz mieć swoją fabrykę zamknijcie go w inteligentnym wskaźniku. Wierzę, że w ten sposób możesz zjeść swoje ciasto i je zjeść.

Eliminuje to również problemy związane z zwracaniem wartości.

Wniosek: utworzenie fabryki poprzez zwrócenie obiektu jest rzeczywiście rozwiązaniem w niektórych przypadkach (np. wektor 2-D wcześniej wspomniany), ale nadal nie jest ogólnym zamiennikiem dla konstruktorów.

W rzeczy samej. Wszystkie wzorce projektowe mają swoje ograniczenia i wady (specyficzne dla języka). Zaleca się używaj ich tylko wtedy, gdy pomogą Ci rozwiązać twój problem, a nie dla ich własnego dobra.

Jeśli jesteś po" idealne " wdrożenie fabryki, cóż, powodzenia.

 5
Author: Péter Török,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-25 18:06:59

Wzór Fabryczny

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

A jeśli kompilator nie obsługuje optymalizacji zwracanej wartości, porzuć ją, prawdopodobnie w ogóle nie zawiera zbyt wiele optymalizacji...

 1
Author: Matthieu M.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-25 19:29:42

Wiem, że odpowiedź na to pytanie została udzielona 3 lata temu, ale to może być to, czego szukaliście.

Google udostępniło kilka tygodni temu bibliotekę umożliwiającą łatwe i elastyczne dynamiczne przydzielanie obiektów. Oto on: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

 1
Author: Florian Richoux,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-14 16:43:02

To moje rozwiązanie w stylu c++11. parametr "base" jest dla klasy bazowej wszystkich podklas. creators, are STD:: function objects to create sub-class instances, may be a binding to your sub-class 'static member function' create (some args)'. To może nie jest idealne, ale działa dla mnie. I jest to rozwiązanie "ogólne".

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();
  }
}
 1
Author: DAG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-16 12:00:23