Как реализовать шаблон метода factory в С++ правильно

Вот эта одна вещь в С++, которая заставляла меня чувствовать себя некомфортно в течение довольно долгого времени, потому что я честно не знаю, как это сделать, хотя это звучит просто:

Как реализовать метод Factory в С++ правильно?

Цель: предоставить клиенту возможность создавать экземпляр какого-либо объекта с использованием методов Factory вместо конструкторов объектов без неприемлемых последствий и производительности.

В "шаблоне метода Factory" я имею в виду как статические методы Factory внутри объекта, так и методы, определенные в другом классе, или глобальные функции. Как правило, "концепция перенаправления нормального способа создания класса X в любом месте, кроме конструктора".

Позвольте мне просмотреть некоторые возможные ответы, о которых я думал.


0) Не создавайте фабрики, не создавайте конструкторы.

Это звучит неплохо (и действительно часто лучшее решение), но не является общим средством. Прежде всего, бывают случаи, когда построение объекта является сложным комплексом задач, чтобы оправдать его извлечение в другой класс. Но даже оставляя этот факт в стороне, даже для простых объектов, использующих только конструкторы, часто не будет.

Самый простой пример, который я знаю, - это двухмерный векторный класс. Так просто, но сложнее. Я хочу иметь возможность построить его как из декартовой, так и из полярной координат. Очевидно, я не могу:

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

Мой естественный образ мышления:

struct Vec2 {
    static Vec2 fromLinear(float x, float y);
    static Vec2 fromPolar(float angle, float magnitude);
    // ...
};

Что, вместо конструкторов, приводит меня к использованию статических методов Factory... что по существу означает, что я каким-то образом реализую шаблон Factory ( "класс становится его собственным factory" )). Это выглядит красиво (и подходит для этого конкретного случая), но в некоторых случаях это не удается, о чем я расскажу в пункте 2. Продолжайте читать.

другой случай: попытка перегрузить два непрозрачных typedefs некоторого API (например, GUID не связанных между собой доменов или GUID и бит-поле), типы семантически совершенно разные (так что в теории - допустимые перегрузки), но которые фактически оказываются быть одним и тем же - как беззнаковые int или указатели void.


1) Путь Java

У Java это просто, поскольку у нас есть только объекты с динамическим распределением. Создание Factory столь же тривиально, как:

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);
    }
}

В С++ это означает:

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

Охладить? Часто, действительно. Но тогда это заставляет пользователя использовать только динамическое распределение. Статическое распределение - это то, что делает сложный С++, но также и то, что часто делает его мощным. Кроме того, я считаю, что существуют определенные цели (ключевое слово: embedded), которые не позволяют динамического выделения. И это не означает, что пользователям этих платформ нравится писать чистый ООП.

В любом случае, философия в стороне: в общем случае я не хочу принуждать пользователей Factory к ограничениям динамического размещения.


2) Возвращаемое значение

ОК, поэтому мы знаем, что 1) классно, когда мы хотим динамического выделения. Почему мы не добавим статическое распределение поверх этого?

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

Что? Мы не можем перегружать возвращаемый тип? О, конечно, мы не можем. Поэтому позвольте изменить имена методов, чтобы это отразить. И да, я написал неверный пример кода выше, просто чтобы подчеркнуть, насколько мне не нравится необходимость изменить имя метода, например, потому что мы не можем сейчас правильно разработать язык-агностик Factory, так как нам нужно изменить имена - и каждый пользователь этого кода должен будет помнить эту разницу в реализации из спецификации.

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

ОК... там у нас это есть. Это уродливо, так как нам нужно изменить имя метода. Это несовершенно, так как нам нужно написать один и тот же код дважды. Но после этого он работает. Правильно?

Ну, обычно. Но иногда это не так. При создании Foo мы фактически полагаемся на компилятор, чтобы сделать оптимизацию возвращаемого значения для нас, потому что стандарт С++ достаточно доброжелателен, чтобы поставщики компилятора не указывали, когда будет создан объект на месте, и когда он будет скопирован при возврате временный объект по значению в С++. Поэтому, если Foo дорого копировать, этот подход является рискованным.

А что, если Foo вообще не скопируется? Ну, да. (Обратите внимание, что в С++ 17 с гарантированным копированием elision, not-being-copiable больше не проблема для кода выше)

Заключение: Создание Factory путем возврата объекта действительно является решением для некоторых случаев (например, ранее упомянутым двухмерным вектором), но все же не является общей заменой для конструкторов.


3) Двухфазная конструкция

Еще одна вещь, которую кто-то, вероятно, придумал, - это разделение проблемы выделения объекта и его инициализации. Обычно это приводит к следующему коду:

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

Можно подумать, что это работает как шарм. Единственная цена, которую мы платим в нашем коде...

Поскольку я написал все это и оставил это последним, мне тоже это не понравится.:) Почему?

Прежде всего... Я искренне не люблю концепцию двухфазной конструкции, и я чувствую себя виноватой, когда я ее использую. Если я создаю свои объекты с утверждением, что "если он существует, он находится в правильном состоянии", я считаю, что мой код более безопасен и менее подвержен ошибкам. Мне это нравится.

Необходимо отказаться от этого соглашения И изменить дизайн моего объекта только с целью сделать его Factory.. ну, громоздким.

Я знаю, что вышеупомянутое не убедит многих людей, поэтому позвольте мне дать более веские аргументы. Используя двухфазную конструкцию, вы не можете:

  • initialize const или переменные элемента ссылки,
  • передать аргументы конструкторам базового класса и конструкторам объектов-членов.

И, вероятно, может быть еще несколько недостатков, о которых я не могу сейчас думать, и я даже не чувствую себя особенно обязанными, так как вышеупомянутые пулевые моменты уже меня убеждают.

Итак: даже близко к хорошему общему решению для реализации factory.


Выводы:

Мы хотим иметь способ создания объекта, который:

  • допускает единообразный экземпляр независимо от его распределения,
  • предоставляют разные, значимые имена для методов построения (таким образом, не полагаясь на перегрузку по аргументам),
  • не вводит существенный удар производительности и, предпочтительно, значительный взлет разрастания кода, особенно на стороне клиента,
  • быть общим, как в: возможно быть введенным для любого класса.

Я считаю, что доказал, что описанные мной способы не соответствуют этим требованиям.

Любые подсказки? Пожалуйста, предоставьте мне решение, я не хочу думать, что этот язык не позволит мне правильно реализовать такую ​​тривиальную концепцию.

Ответ 1

Прежде всего, бывают случаи, когда построение объекта - комплекс задач достаточно, чтобы оправдать его извлечение другой класс.

Я считаю, что этот момент неверен. Сложность не имеет большого значения. Актуальность - это то, что делает. Если объект можно построить за один шаг (не как в шаблоне построителя), конструктор - это подходящее место для этого. Если вам действительно нужен другой класс для выполнения задания, тогда он должен быть вспомогательным классом, который все равно используется конструктором.

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

Для этого есть простой способ:

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

Единственный недостаток заключается в том, что он выглядит немного подробным:

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

Но хорошо то, что вы можете сразу увидеть, какой тип координаты вы используете, и в то же время вам не нужно беспокоиться о копировании. Если вы хотите копировать, и это дорого (как это проверено профилированием, конечно), вы можете использовать что-то вроде Qt shared classes, чтобы избежать копирования накладные расходы.

Что касается типа распределения, основной причиной использования шаблона factory является, как правило, полиморфизм. Конструкторы не могут быть виртуальными, и даже если бы они могли, это не имело бы большого смысла. При использовании статического или стекового распределения вы не можете создавать объекты полиморфным способом, потому что компилятор должен знать точный размер. Таким образом, он работает только с указателями и ссылками. И возврат ссылки из factory тоже не работает, потому что, хотя объект технически может быть удален по ссылке, он может быть довольно запутанным и подверженным ошибкам, см. Является ли практика возврата ссылочной переменной С++, например, зла?. Так что указатели - это единственное, что осталось, и это тоже включает интеллектуальные указатели. Другими словами, фабрики наиболее полезны при использовании с динамическим распределением, поэтому вы можете делать такие вещи:

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

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

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

В других случаях фабрики просто помогают решить мелкие проблемы, такие как проблемы с перегрузками, о которых вы упомянули. Было бы неплохо, если бы можно было использовать их в унифицированном виде, но это не сильно больно, что это, вероятно, невозможно.

Ответ 2

Простой Factory Пример:

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

Ответ 3

Вы думали о том, что вообще не используете factory, и вместо этого используете красивое использование системы типов? Я могу думать о двух разных подходах, которые делают такие вещи:

Вариант 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) { /* ... */ }
};

Что позволяет писать такие вещи, как:

Vec2 v(linear(1.0, 2.0));

Вариант 2:

вы можете использовать "теги", например, STL с итераторами и т.д. Например:

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 &) { /* ... */ }
};

Этот второй подход позволяет писать код, который выглядит следующим образом:

Vec2 v(1.0, 2.0, linear_coord);

который также приятный и выразительный, позволяя вам иметь уникальные прототипы для каждого конструктора.

Ответ 4

Вы можете прочитать очень хорошее решение в: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

Лучшее решение для "комментариев и обсуждений" см. в разделе "Нет необходимости в статических методах создания".

Из этой идеи я сделал factory. Обратите внимание, что я использую Qt, но вы можете изменить QMap и QString для эквивалентов 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

Использование образца:

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");

Ответ 5

У Loki есть Factory Method и Аннотация Factory. Оба документа задокументированы (широко) в Modern С++ Design, Andei Alexandrescu. Метод factory, вероятно, ближе к тому, что вам кажется после, хотя он все еще немного отличается (по крайней мере, если память обслуживается, для этого требуется зарегистрировать тип до того, как factory может создавать объекты этого типа).

Ответ 6

Я в основном согласен с принятым ответом, но есть опция С++ 11, которая не была рассмотрена в существующих ответах:

  • Возвращает метод factory по значению и
  • Предоставить дешевый конструктор перемещения.

Пример:

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

Затем вы можете создавать объекты в стеке:

sandwich mine{sandwich::ham()};

Как подобъекты других вещей:

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

Или динамически выделяется:

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

Когда я могу это использовать?

Если в публичном конструкторе невозможно дать значимые инициализаторы для всех членов класса без предварительного расчета, я могу преобразовать этот конструктор в статический метод. Статический метод выполняет предварительные вычисления, а затем возвращает результат результата через частный конструктор, который просто выполняет инициализацию по элементам.

Я говорю "может", потому что он зависит от того, какой подход дает самый четкий код без излишней неэффективности.

Ответ 7

Я не пытаюсь ответить на все мои вопросы, так как считаю, что он слишком широк. Всего несколько заметок:

бывают случаи, когда построение объекта является сложным комплексом задач, чтобы оправдать его извлечение в другой класс.

Этот класс на самом деле является Builder, а не Factory.

В общем случае я не хочу принуждать пользователей factory к динамическому распределению.

Затем вы можете включить factory в интеллектуальном указателе. Я верю, что вы можете получить свой торт и съесть его тоже.

Это также устраняет проблемы, связанные с возвратом по значению.

Заключение: Создание factory путем возврата объекта действительно является решением для некоторых случаев (например, ранее упомянутым двухмерным вектором), но все же не является общей заменой для конструкторов.

Действительно. Все шаблоны проектирования имеют свои (языковые) ограничения и недостатки. Рекомендуется использовать их только тогда, когда они помогут вам решить вашу проблему, а не ради них самих.

Если вы после "идеальной" реализации factory, ну, удачи.

Ответ 8

Factory Шаблон

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

И если вы компилятор не поддерживает Оптимизацию возвращаемого значения, то это, вероятно, не содержит никакой оптимизации вообще...

Ответ 9

Я знаю, что этот вопрос был дан 3 года назад, но это может быть то, что вы искали.

Google выпустила пару недель назад библиотеку, позволяющую легко и гибко распределять динамические объекты. Вот он: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

Ответ 10

Это мое решение стиля С++ 11. Параметр base используется для базового класса всех подклассов. создатели, являются объектами std:: function для создания экземпляров подкласса, может быть привязкой к вашей статической функции-члену подкласса create (some args). Возможно, это не идеально, но работает для меня. И это своеобразное "общее" решение.

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_;
};

Пример использования.

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