Как правильно реализовать шаблон фабричного метода в C ++ - PullRequest
304 голосов
/ 25 февраля 2011

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

Какправильно ли реализовать Фабричный метод в C ++?

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

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

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


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

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

Самый простой пример, который я знаю, - это 2-D класс 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 и битовое поле), семантически совершенно разные типы (то есть - в теории - допустимые перегрузки), но которые на самом деле оказываются одним и тем же - например, беззнаковые целые или 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 ++ сложным, но также и то, что часто делает его мощным.Кроме того, я считаю, что существуют некоторые цели (ключевое слово: внедренные), которые не позволяют динамическое размещение.И это не означает, что пользователям этих платформ нравится писать чистый ООП.

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


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

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

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

А что, если Foo вообще не копируется? Ну, дох ( Обратите внимание, что в C ++ 17 с гарантированным разрешением на копирование возможность не быть копируемым больше не является проблемой для кода выше )

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


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 или ссылочные переменные-члены,
  • передает аргументы конструкторам базовых классов и конструкторам объектов-членов.

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

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


Выводы:

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

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

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

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

Ответы [ 10 ]

98 голосов
/ 25 февраля 2011

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

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

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

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

45 голосов
/ 25 февраля 2011

Простой фабричный пример:

// 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();
    }
};
38 голосов
/ 28 февраля 2011

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

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

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

27 голосов
/ 15 ноября 2014

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

Лучшее решение - "комментарии и обсуждения", смотрите "Нет необходимости в статических методах создания".Из этой идеи я сделал фабрику.Обратите внимание, что я использую Qt, но вы можете изменить QMap и QString для эквивалентов стандартного ввода.

15 голосов
/ 07 сентября 2015

Я в основном согласен с принятым ответом, но есть вариант 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());

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

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

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

11 голосов
/ 25 февраля 2011

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

5 голосов
/ 25 февраля 2011

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

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

Этот класс на самом деле является Строителем , а не Фабрикой.

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

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

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

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

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

Если вам нужна «идеальная» заводская реализация, удачи.

2 голосов
/ 25 февраля 2011

Заводская модель

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

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

1 голос
/ 16 октября 2017

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

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();
  }
}
1 голос
/ 14 февраля 2014

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

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

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