Как правильно реализовать шаблон Заводского метода в C++



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



Как правильно реализовать Заводской метод в C++?



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



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



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





0) не делают фабрики, делают конструкторы.



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



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



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


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



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





1) Путь Java



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



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


в C++, это переводится как:



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


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



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





2) возврат по значению



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



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


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



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


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



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



а что, если Фу вообще не справляется? Ну, дох. (обратите внимание, что в C++17 с гарантированным Элизии копию, не то что копировать, это не проблема больше для кода выше)



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





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


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



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



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



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



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




  • инициализации const или ссылочные переменные-члены,

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


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



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





выводы:



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




  • разрешить равномерное создание экземпляра независимо от распределения,

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

  • не вводите значительный удар по производительности и, предпочтительно, значительный удар по раздуванию кода, особенно на стороне клиента,

  • быть общим, как в: можно ввести для любого класса.


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



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

740   10  

10 ответов:

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

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

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 классы чтобы избежать копирования накладных.

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

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

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

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

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

Простой Заводской Пример:

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

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

Вариант 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);

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

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

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

от этой идеи, я сделал завод. Обратите внимание, что я использую 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");

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

  • возврат результатов Заводского метода по стоимости и
  • обеспечить дешевые переместить конструктор.

пример:

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

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

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

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

у Локи есть оба Метод Фабрики и Абстрактная Фабрика. Оба они документированы (широко) в Современный Дизайн C++, по Андрей Александреску. Фабричный метод, вероятно, ближе к тому, что вы, кажется, после, хотя это все еще немного отличается (по крайней мере, если память служит, он требует, чтобы вы зарегистрировали тип, прежде чем фабрика может создавать объекты этого типа).

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

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

этот класс на самом деле a Строитель, а не завод.

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

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

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

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

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

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

Шаблон Фабрики

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

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

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

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

Это мое решение стиля c++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();
  }
}

Comments

    Ничего не найдено.