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

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

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

  1. Фабрика
  2. Строитель
  3. Прототип
  4. Одиночка

Фрагменты кода, которые вы видите в этой серии статей, упрощены, а не сложны. Таким образом, вы часто видите, что я не использую ключевые слова, такие как override, final, public (при наследовании), просто для того, чтобы сделать код компактным и удобным (в большинстве случаев) в одном стандартном размере экрана. Я также предпочитаю struct вместо class просто для сохранения строки, не записывая иногда 'public:', а также намеренно пропускаю виртуальный деструктор, конструктор, копирующий конструктор, префикс std::, удаление динамической памяти. Я также считаю себя прагматичным человеком, который хочет передать идею максимально простым способом, а не стандартным способом или с использованием жаргона.

Примечание.

  • Если вы наткнулись прямо сюда, то я бы посоветовал вам пройти Что такое шаблон проектирования? во-первых, даже если это тривиально. Я считаю, что это побудит вас больше исследовать эту тему.
  • Весь этот код, с которым вы столкнетесь в этой серии статей, скомпилирован с использованием C++20 (хотя в большинстве случаев я использовал возможности Modern C++ вплоть до C++17). Поэтому, если у вас нет доступа к последнему компилятору, вы можете использовать https://wandbox.org/, на котором также предустановлена ​​библиотека boost.

Намерение

Для создания оптовых объектов в отличие от конструктора(который создает поштучно).

Мотивация

  • Допустим, у вас есть класс Point, имеющий x и y в качестве координат, которые могут быть декартовыми или полярными координатами, как показано ниже:
struct Point {
    Point(float x, float y){ /*...*/ }      // Cartesian co-ordinates
    // Not OK: Cannot overload with same type of arguments
    // Point(float a, float b){ /*...*/ }    // Polar co-ordinates
    // ... Implementation
};
  • Это невозможно, поскольку вы, возможно, знаете, что не можете создать два конструктора с одинаковыми аргументами.
  • Другой способ:
enum class PointType{ cartesian, polar };
class Point {
    Point(float a, float b, PointTypetype = PointType::cartesian) {
        if (type == PointType::cartesian) {
            x = a; b = y;
        }
        else {
            x = a * cos(b);
            y = a * sin(b);
        }
    }
};
  • Но это не изощренный способ сделать это. Скорее мы должны делегировать отдельные экземпляры отдельным методам.

Примеры шаблонов проектирования Factory на C++

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

Заводской метод

enum class PointType { cartesian, polar };
class Point {
    float       m_x;
    float       m_y;
    PointType   m_type;
    // Private constructor, so that object can't be created directly
    Point(const float x, const float y, PointType t) : m_x{x}, m_y{y}, m_type{t} { }
public:
    friend ostream& operator<<(ostream& os, const Point& obj) {
        return os << "x: " << obj.m_x << " y: " << obj.m_y;
    }
    static Point NewCartesian(float x, float y) { 
        return { x, y, PointType::cartesian }; 
    }
    static Point NewPolar(float a, float b) { 
        return { a*cos(b), a*sin(b), PointType::polar }; 
    }
};
int main() {
    // Point p{ 1,2 };  // will not work
    auto p = Point::NewPolar(5, M_PI_4);
    cout << p << endl;  // x: 3.53553 y: 3.53553
    return EXIT_SUCCESS;
}
  • Как видно из реализации. Это фактически запрещает использование конструктора и заставляет пользователей вместо этого использовать статические методы. И в этом суть фабричного метода, т. е. частного конструктора и статического метода.

Классический заводской шаблон проектирования

  • Если у вас есть выделенный код для построения, то пока нет, мы переносим его в выделенный класс. И просто сделать разделение задач, то есть Принцип единой ответственности от принципов проектирования SOLID.
class Point {
    // ... as it is from above
    friend class PointFactory;
};
class PointFactory {
public:
    static Point NewCartesian(float x, float y) {
        return { x, y };
    }
    static Point NewPolar(float r, float theta) {
        return { r*cos(theta), r*sin(theta) };
    }
};
  • Имейте в виду, что это не абстрактная фабрика, это конкретная фабрика.
  • Создавая класс друзей PointFactory для Point, мы нарушили принцип открытого-закрытого (OCP). Как ключевое слово друга, противоречащее OCP.

Внутренняя фабрика

  • В нашей Фабрике мы упустили одну важную вещь: нет сильной связи между PointFactory и Point, что сбивает пользователя с толку, когда он использует Point, просто видя, что все private.
  • Поэтому вместо того, чтобы проектировать фабрику вне класса. Мы можем просто поместить его в класс, который побуждает пользователей использовать Factory.
  • Таким образом, мы также обслуживаем вторую проблему, которая нарушает принцип открытого-закрытого. И это будет несколько более интуитивно понятным для пользователя в использовании Factory.
class Point {
    float   m_x;
    float   m_y;
    Point(float x, float y) : m_x(x), m_y(y) {}
public:
    struct Factory {
        static Point NewCartesian(float x, float y) { return { x,y }; }
        static Point NewPolar(float r, float theta) { return{ r*cos(theta), r*sin(theta) }; }
    };
};
int main() {
    auto p = Point::Factory::NewCartesian(2, 3);
    return EXIT_SUCCESS;
}

Абстрактная фабрика

Зачем нам Абстрактная Фабрика?

  • C++ поддерживает разрушение полиморфных объектов с помощью виртуального деструктора базового класса. Точно так же отсутствует эквивалентная поддержка создания и копирования объектов, поскольку С++ не поддерживает виртуальный конструктор и конструкторы копирования.
  • Более того, вы не можете создать объект, если не знаете его статический тип, потому что компилятор должен знать, сколько места ему нужно выделить. По той же причине для копии объекта также требуется, чтобы его тип был известен во время компиляции.
struct Point {
    virtual ~Point(){ cout<<"~Point\n"; }
};
struct Point2D : Point {
    ~Point2D(){ cout<<"~Point2D\n"; }
};
struct Point3D : Point {
    ~Point3D(){ cout<<"~Point3D\n"; }
};
void who_am_i(Point *who) { // Not sure whether Point2D would be passed here or Point3D
    // How to `create` the object of same type i.e. pointed by who ?
    // How to `copy` object of same type i.e. pointed by who ?
    delete who; // you can delete object pointed by who, thanks to virtual destructor
}

Пример шаблона проектирования абстрактной фабрики

  • Абстрактная фабрика полезна в ситуации, когда требуется создание множества различных типов объектов, производных от общего базового типа.
  • Абстрактная фабрика определяет метод создания объектов, которые подклассы могут затем переопределить, чтобы указать производный тип, который будет создан. Таким образом, во время выполнения соответствующий абстрактный метод фабрики будет вызываться в зависимости от типа объекта, на который ссылается/указывается, и возвращает указатель базового класса на новый экземпляр этого объекта.
struct Point {
    virtual ~Point() = default;
    virtual unique_ptr<Point> create() = 0;
    virtual unique_ptr<Point> clone()    = 0;
};
struct Point2D : Point {
    unique_ptr<Point> create() { return make_unique<Point2D>(); }
    unique_ptr<Point> clone() { return make_unique<Point2D>(*this); }
};
struct Point3D : Point {
    unique_ptr<Point> create() { return make_unique<Point3D>(); }
    unique_ptr<Point> clone() { return make_unique<Point3D>(*this); }
};
void who_am_i(Point *who) {
    auto new_who       = who->create(); // `create` the object of same type i.e. pointed by who ?
    auto duplicate_who = who->clone();    // `copy` the object of same type i.e. pointed by who ?
    delete who;
}

Функциональный подход к шаблону проектирования Factory с использованием современного C++

  • В нашем примере с абстрактной фабрикой мы следовали объектно-ориентированному подходу, но в настоящее время также возможен более функциональный подход.
  • Итак, давайте построим похожую фабрику, не полагаясь на полиморфную функциональность, поскольку она может не подойти для какого-то приложения с ограниченным временем, такого как встраиваемая система. Потому что виртуальная таблица и механизм динамической диспетчеризации могут троллить систему во время критических функций.
  • Это довольно просто, поскольку использует функциональные и лямбда-функции следующим образом:
struct Point { /* . . . */ };
struct Point2D : Point {/* . . . */};
struct Point3D : Point {/* . . . */};
class PointFunctionalFactory {
    map<PointType, function<unique_ptr<Point>() >>      m_factories;
public:
    PointFunctionalFactory() {
        m_factories[PointType::Point2D] = [] { return make_unique<Point2D>(); };
        m_factories[PointType::Point3D] = [] { return make_unique<Point3D>(); };
    }    
    unique_ptr<Point> create(PointType type) { return m_factories[type](); }  
};
int main() {
    PointFunctionalFactory pf;
    auto p2D = pf.create(PointType::Point2D);
    return EXIT_SUCCESS;
}
  • Если вы думаете, что мы переусердствуем, то имейте в виду, что наша конструкция объекта проста здесь только для демонстрации техники, как и наша лямбда-функция.
  • Когда представление вашего объекта увеличивается, требуется множество методов для вызова, чтобы правильно создать экземпляр объекта, в таком случае вам просто нужно изменить лямбда-выражение фабрики или ввести Шаблон проектирования Builder.

Преимущества шаблона проектирования Factory

  1. Единая точка/класс для создания различных объектов. Таким образом, простое в обслуживании и понимании программное обеспечение.
  2. Вы можете создать объект, даже не зная его типа, используя Abstract Factory.
  3. Он обеспечивает большую модульность. Представьте себе программирование видеоигры, в которой вы хотели бы в будущем добавить новые типы врагов, каждый из которых имеет разные функции ИИ и может обновляться по-разному. Используя фабричный метод, контроллер программы может вызывать фабрику для создания врагов без какой-либо зависимости или знаний о фактических типах врагов. Теперь будущие разработчики могут создавать новых врагов с новыми элементами управления ИИ и новыми функциями-членами рисования, добавлять их на фабрику и создавать уровень, который вызывает фабрику, запрашивая врагов по именам. Объедините этот метод с XML-описанием уровней, и разработчики смогут создавать новые уровни без перекомпиляции своей программы. Все это благодаря отделению создания объектов от использования объектов.
  4. Позволяет вам легче изменить дизайн вашего приложения, это известно как слабая связь.

Резюме по часто задаваемым вопросам

Как правильно реализовать шаблон проектирования Factory в C++?

Абстрактная фабрика и функциональная фабрика — всегда хороший выбор.

Фабрика, абстрактный фактор или функциональная фабрика?

- Фабрика: создайте объект с различными экземплярами.
- Абстрактный фактор: создайте объект, не зная его типа, и ссылайтесь, используя указатель и ссылку базового класса. Доступ с использованием полиморфных методов.
- Функциональная фабрика: Когда создание объекта является более сложным. Абстрактная Фабрика + Паттерн Строителя. Хотя я не включил Builder в пример Functional Factory.

Когда использовать шаблон проектирования Factory?

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

Есть предложения, вопросы или пожелания Hi? Снимите давление, вы на расстоянии одного клика.🖱️