202.08K
Категория: ПрограммированиеПрограммирование

Паттерны (шаблоны) проектирования. Порождающие паттерны

1.

Паттерны (шаблоны)
проектирования. Порождающие
паттерны

2.

Что такое Паттерн?
• Паттерн проектирования — это часто
встречающееся решение определённой проблемы
при проектировании архитектуры программ.
Паттерны не привязаны к какому-либо конкретному
языку программирования. Это просто подход к
проектированию чего-либо. Если смотреть глубже,
то многие паттерны были созданы на основе
реальных жизненный ситуаций в проектировании
вполне себе осязаемых объектов нашего мира.

3.

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

4.

Если привести аналогии, то алгоритм — это
кулинарный рецепт с чёткими шагами, а
паттерн — инженерный чертёж, на котором
нарисовано решение, но не конкретные
шаги его реализации.

5.

Из чего состоит паттерн?
Описания паттернов обычно очень формальны и чаще всего
состоят из таких пунктов:
• проблема, которую решает паттерн (предназначение);
• мотивация к решению проблемы способом, который
предлагает паттерн;
• применимость (определяются конкретные ситуации);
• структура классов, составляющих решение;
• пример кода;
• пример применения в реальных системах
• особенности реализации в различных контекстах;
• связь с другими паттернами.
Такой формализм в описании позволил создать обширный каталог
паттернов, проверив каждый из них на состоятельность.

6.

Зачем знать паттерны?
Можно вполне успешно работать, не зная ни одного паттерна.
Вы можете забить гвоздь молотком, а можете и дрелью, если
сильно постараетесь. Но профессионал знает, что главная
фишка дрели совсем не в этом. Итак, зачем же знать паттерны?
• Проверенные решения. Вы тратите меньше времени,
используя готовые решения, вместо повторного изобретения
велосипеда. До некоторых решений вы смогли бы додуматься и
сами, но многие могут быть для вас открытием.
• Стандартизация кода. Вы делаете меньше просчётов при
проектировании, используя типовые унифицированные
решения, так как все скрытые проблемы в них уже давно
найдены.
• Общий программистский словарь. Вы произносите название
паттерна, вместо того, чтобы час объяснять другим
программистам, какой крутой дизайн вы придумали и какие
классы для этого нужны.

7.

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

8.

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

9.

Критика паттернов
• Костыли для слабого языка программирования. Нужда в
паттернах появляется тогда, когда люди выбирают для
своего проекта язык программирования с недостаточным
уровнем абстракции. В этом случае, паттерны — это
костыль, который придаёт этому языку суперспособности.
• Неэффективные решения. Паттерны пытаются
стандартизировать подходы, которые и так уже широко
используются. Эта стандартизация кажется некоторым
людям догмой и они реализуют паттерны «как в книжке»,
не приспосабливая паттерны к реалиям проекта.
• Неоправданное применение. Если у тебя в руках
молоток, то все предметы вокруг начинают
напоминать гвозди. Похожая проблема возникает у
новичков, которые только-только познакомились с
паттернами. Вникнув в паттерны, человек пытается
применить свои знания везде. Даже там, где можно было
бы обойтись кодом попроще.

10.

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

11.

Кроме того, паттерны отличаются и
предназначением. Рассмотрим три основные
группы паттернов:
• Порождающие паттерны беспокоятся о
гибком создании объектов без внесения в
программу лишних зависимостей.
• Структурные паттерны показывают различные
способы построения связей между объектами.
• Поведенческие паттерны заботятся об
эффективной коммуникации между
объектами.

12.

Порождающие паттерны
проектирования (Creational patterns)
Порождающие шаблоны проектирования – это
паттерны, которые имеют дело с механизмами
создания объекта и пытаются создать объекты
в порядке, подходящем к ситуации. Обычная
форма создания объекта может привести к
проблемам проектирования или увеличивать
сложность конструкции. Порождающие
шаблоны проектирования решают эту
проблему, определённым образом
контролируя процесс создания объекта.

13.

Пусть мы разрабатываем стратегическую игру под
названием "Пунические войны", описывающую
великое военное противостояние между Римской
Республикой и Карфагеном (264 — 146 г. до н. э.).
Персонажами игры могут быть воины трех типов:
пехота, конница и лучники. Каждый из этих видов
обладает своими отличительными характеристиками,
такими как внешний вид, боевая мощь, скорость
передвижения и степень защиты. Несмотря на такие
отличия, у всех видов боевых единиц есть общие
черты. Например, все они могут передвигаться по
игровому полю в различных направлениях, хотя
всадники делают это быстрее всех. Или каждая боевая
единица имеет свой уровень здоровья, и если он
становится равным нулю, воин погибает. При этом
уничтожить лучника значительно проще, чем другие
виды воинов.

14.

class Warrior{
public:
virtual void info() = 0;
virtual ~Warrior() {}
};
class Infantryman: public Warrior{
public:
void info() { cout << "Infantryman" << endl; }
};
class Archer: public Warrior{
public:
void info() { cout << "Archer" << endl; }
};
class Horseman: public Warrior{
public:
void info() { cout << "Horseman" << endl; }
};

15.

Полиморфный базовый класс Warrior определяет общий
интерфейс, а производные от него классы Infantryman,
Archer и Horseman реализуют особенности каждого вида
воина. Сложность заключается в том, что хотя код системы
и оперирует готовыми объектами через соответствующие
общие интерфейсы, в процессе игры требуется создавать
новые персонажи, непосредственно указывая их
конкретные типы. Если код их создания рассредоточен по
всему приложению, то добавлять новые типы персонажей
или заменять существующие будет затруднительно.
В таких случаях на помощь приходит фабрика объектов,
локализующая создание объектов. Работа фабрики
объектов напоминает функционирование виртуального
конструктора, - мы можем создавать объекты нужных
классов, не указывая напрямую их типы. В самом простом
случае, для этого используются идентификаторы типов.
Следующий пример демонстрирует простейший вариант
фабрики объектов - фабричную функцию.

16.

enum Warrior_ID { Infantryman_ID=0, Archer_ID, Horseman_ID };
Warrior * сreateWarrior( Warrior_ID id ){
Warrior * p;
switch (id){
case Infantryman_ID:
p = new Infantryman();
break;
case Archer_ID:
p = new Archer();
break;
case Horseman_ID:
p = new Horseman();
break;
default:
assert( false);
}
return p;
}

17.

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

18.

• Абстрактная фабрика (Abstract Factory)
• Строитель (Builder)
• Фабричный Метод (Factory Method)
• Пул одиночек (Multiton)
• Объектный пул (Pool)
• Прототип (Prototype)
• Простая Фабрика (Simple Factory)
• Одиночка (Singleton)
• Статическая Фабрика (Static Factory)

19.

Одиночка (Singleton)
Суть паттерна. Гарантирует, что у класса есть только один экземпляр, и
предоставляет к нему глобальную точку доступа.
Проблема. Одиночка решает сразу две проблемы, нарушая принцип
единственной ответственности класса.
• Гарантирует наличие единственного экземпляра класса. Чаще всего
это полезно для доступа к какому-то общему ресурсу, например, базе
данных.
Представьте, что вы создали объект, а через некоторое время пробуете
создать ещё один. В этом случае хотелось бы получить старый объект,
вместо создания нового.
Такое поведение невозможно реализовать с помощью обычного
конструктора, так как конструктор класса всегда возвращает новый
объект.
• Предоставляет глобальную точку доступа. Это не просто глобальная
переменная, через которую можно достучаться к определённому
объекту. Глобальные переменные не защищены от записи, поэтому
любой код может подменять их значения без вашего ведома.

20.

Описание Singleton
Архитектура паттерна Singleton основана на идее
использования глобальной переменной, имеющей
следующие важные свойства:
• Такая переменная доступна всегда. Время жизни
глобальной переменной - от запуска программы до
ее завершения.
• Предоставляет глобальный доступ, то есть, такая
переменная может быть доступна из любой части
программы.
Однако, использовать глобальную переменную
некоторого типа непосредственно невозможно, так
как существует проблема обеспечения
единственности экземпляра, а именно, возможно
создание нескольких переменных того же самого типа
(например, стековых).

21.

Для решения этой проблемы паттерн
Singleton возлагает контроль над созданием
единственного объекта на сам класс. Доступ к
этому объекту осуществляется через
статическую функцию-член класса, которая
возвращает указатель или ссылку на него.
Этот объект будет создан только при первом
обращении к методу, а все последующие
вызовы просто возвращают его адрес. Для
обеспечения уникальности объекта,
конструкторы и оператор присваивания
объявляются закрытыми.

22.

Реализация паттерна
// Singleton.h
class Singleton{
private:
static Singleton * p_instance;
// Конструкторы и оператор присваивания недоступны клиентам
Singleton() {}
Singleton( const Singleton& );
Singleton& operator=( Singleton& );
public:
static Singleton * getInstance() {
if(!p_instance)
p_instance = new Singleton();
return p_instance;
}
};
// Singleton.cpp
#include "Singleton.h"
Singleton* Singleton::p_instance = 0;

23.

Клиенты запрашивают единственный объект
класса через статическую функциючлен getInstance(), которая при первом запросе
динамически выделяет память под этот объект и
затем возвращает указатель на этот участок
памяти. Впоследcтвии клиенты должны сами
позаботиться об освобождении памяти при
помощи оператора delete.
Последняя особенность является серьезным
недостатком классической реализации шаблона
Singleton. Так как класс сам контролирует
создание единственного объекта, было бы
логичным возложить на него ответственность и
за разрушение объекта.

24.

// Singleton.h
class Singleton; // опережающее объявление
class SingletonDestroyer{
private:
Singleton* p_instance;
public:
~SingletonDestroyer();
void initialize( Singleton* p );
};
class Singleton{
private:
static Singleton* p_instance;
static SingletonDestroyer destroyer;
protected:
Singleton() { }
Singleton( const Singleton& );
Singleton& operator=( Singleton& );
~Singleton() { }
friend class SingletonDestroyer;
public:
static Singleton& getInstance();
};

25.

// Singleton.cpp
#include "Singleton.h"
Singleton * Singleton::p_instance = 0;
SingletonDestroyer Singleton::destroyer;
SingletonDestroyer::~SingletonDestroyer() {
delete p_instance;
}
void SingletonDestroyer::initialize( Singleton* p ) {
p_instance = p;
}
Singleton& Singleton::getInstance() {
if(!p_instance) {
p_instance = new Singleton();
destroyer.initialize( p_instance);
}
return *p_instance;
}

26.

Ключевой особенностью этой реализации является
наличие класса SingletonDestroyer, предназначенного
для автоматического разрушения объекта Singleton.
Класс Singleton имеет статический
член SingletonDestroyer, который инициализируется
при первом
вызове Singleton::getInstance() создаваемым
объектом Singleton. При завершении программы этот
объект будет автоматически разрушен
деструктором SingletonDestroyer (для
этого SingletonDestroyer объявлен другом
класса Singleton).
Для предотвращения случайного удаления
пользователями объекта класса Singleton, деструктор
теперь уже не является общедоступным как ранее. Он
объявлен защищенным.

27.

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

28.

Преимущества и недостатки
• «+»:
Гарантирует наличие единственного экземпляра
класса;
Предоставляет к нему глобальную точку доступа;
Реализует отложенную инициализацию объектаодиночки;
• «-»:
Нарушает принцип единственной ответственности
класса;
В случае использования нескольких взаимозависимых
одиночек их реализация может резко усложниться.

29.

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

30.

Описание Factory Method
Для того, чтобы система оставалась независимой от
различных типов объектов, паттерн Factory Method
использует механизм полиморфизма - классы всех
конечных типов наследуют от одного абстрактного
базового класса, предназначенного для полиморфного
использования. В этом базовом классе определяется
единый интерфейс, через который пользователь будет
оперировать объектами конечных типов.
Для обеспечения относительно простого добавления в
систему новых типов паттерн Factory Method локализует
создание объектов конкретных типов в специальном
классе-фабрике. Методы этого класса, посредством
которых создаются объекты конкретных классов,
называются фабричными.

31.

Существуют две разновидности паттерна Factory
Method:
• Обобщенный конструктор, когда в том же
самом полиморфном базовом классе, от
которого наследуют производные классы всех
создаваемых в системе типов, определяется
статический фабричный метод. В качестве
параметра в этот метод должен передаваться
идентификатор типа создаваемого объекта.
• Классический вариант фабричного метода,
когда интерфейс фабричных методов
объявляется в независимом классе-фабрике, а
их реализация определяется конкретными
подклассами этого класса.

32.

Классическая реализация Factory Method
#include <iostream>
#include <vector>
class Warrior{
public:
virtual void info() = 0;
virtual ~Warrior() {} };
class Infantryman: public Warrior{
public:
void info() {
cout << "Infantryman" << endl;
}; };
class Archer: public Warrior{
public:
void info() {
cout << "Archer" << endl;
};
};
class Horseman: public Warrior{
public:
void info() {
cout << "Horseman" << endl;
};
};

33.

// Фабрики объектов
class Factory{
public:
virtual Warrior* createWarrior() = 0;
virtual ~Factory() {}
};
class InfantryFactory: public Factory{
public:
Warrior* createWarrior() {
return new Infantryman;
}
};
class ArchersFactory: public Factory{
public:
Warrior* createWarrior() {
return new Archer;
}
};
class CavalryFactory: public Factory{
public:
Warrior* createWarrior() {
return new Horseman;
}
};

34.

// Создание объектов при помощи фабрик объектов
int main()
{
InfantryFactory* infantry_factory = new InfantryFactory;
ArchersFactory* archers_factory = new ArchersFactory ;
CavalryFactory* cavalry_factory = new CavalryFactory ;
vector<Warrior*> v;
v.push_back( infantry_factory->createWarrior());
v.push_back( archers_factory->createWarrior());
v.push_back( cavalry_factory->createWarrior());
for(int i=0; i<v.size(); i++)
v[i]->info();
// ...
}

35.

Классический вариант паттерна Factory Method использует
идею полиморфной фабрики. Специально выделенный
для создания объектов полиморфный базовый
класс Factory объявляет интерфейс фабричного
метода createWarrior(), а производные классы его
реализуют.
Представленный вариант паттерна Factory Method
является наиболее распространенным, но не
единственным. Возможны следующие вариации:
• Класс Factory имеет реализацию фабричного
метода createWarrior() по умолчанию.
• Фабричный
метод createWarrior() класса Factory параметризирован
типом создаваемого объекта (как и у представленного
ранее, простого варианта Factory Method) и имеет
реализацию по умолчанию. В этом случае, производные
от Factory классы необходимы лишь для того, чтобы
определить нестандартное поведение createWarrior().

36.

Преимущества и недостатки
• «+»:
Создает объекты разных типов, позволяя
системе оставаться независимой как от самого
процесса создания, так и от типов
создаваемых объектов.
• «-»:
В случае классического варианта паттерна даже
для порождения единственного объекта
необходимо создавать соответствующую
фабрику.

37.

Абстрактная фабрика (Abstract
Factory)
Суть паттерна. Абстрактная фабрика — это порождающий паттерн
проектирования, который позволяет создавать семейства связанных
объектов, не привязываясь к конкретным классам создаваемых объектов.
Проблема. Представьте, что вы пишете симулятор мебельного магазина. Ваш код
содержит:
1. Семейство зависимых продуктов: Кресло + Диван + Столик.
2. Несколько вариаций этого семейства. Например,
продукты Кресло, Диван и Столик представлены в трёх разных стилях: Ардеко, Викторианском и Модерне.
Вам нужен такой способ создавать объекты продуктов, чтобы они сочетались с
другими продуктами того же семейства. Это важно, так как клиенты
расстраиваются, если получают несочетающуюся мебель.
Кроме того, вы не хотите вносить изменения в существующий код при
добавлении новых продуктов или семейcтв в программу. Поставщики часто
обновляют свои каталоги, и вы бы не хотели менять уже написанный код
каждый раз при получении новых моделей мебели.

38.

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

39.

Далее вы создаёте абстрактную фабрику — общий
интерфейс, который содержит методы создания всех
продуктов семейства
(например, createChair(), createSofa() и createCoffeeTa
ble()).
Эти операции должны возвращать абстрактные типы
продуктов, представленные интерфейсами, которые
мы выделили ранее — Кресла, Диваны и Столики.

40.

Для каждой вариации семейства продуктов
мы должны создать свою собственную
фабрику, реализовав абстрактный
интерфейс. Фабрики создают продукты
одной вариации.
Например, ФабрикаМодерн будет
возвращать только КреслаМодерн,
ДиваныМодерн и СтоликиМодерн.
Клиентский код должен работать как с
фабриками, так и с продуктами только
через их общие интерфейсы. Это позволит
подавать в ваши классы любой тип фабрики
и производить любые продукты, ничего не
ломая.

41.

Реализация паттерна
#include <iostream>
#include <vector>
// Абстрактные базовые классы всех возможных видов воинов
class Infantryman{
public:
virtual void info() = 0;
virtual ~Infantryman() {}
};
class Archer{
public:
virtual void info() = 0;
virtual ~Archer() {}
};
class Horseman{
public:
virtual void info() = 0;
virtual ~Horseman() {}
};

42.

// Классы всех видов воинов Римской армии
class RomanInfantryman: public Infantryman{
public:
void info() {
cout << "RomanInfantryman" << endl;
}
};
class RomanArcher: public Archer{
public:
void info() {
cout << "RomanArcher" << endl;
}
};
class RomanHorseman: public Horseman{
public:
void info() {
cout << "RomanHorseman" << endl;
}
};

43.

// Классы всех видов воинов армии Карфагена
class CarthaginianInfantryman: public Infantryman{
public:
void info() {
cout << "CarthaginianInfantryman" << endl;
}
};
class CarthaginianArcher: public Archer{
public:
void info() {
cout << "CarthaginianArcher" << endl;
}
};
class CarthaginianHorseman: public Horseman{
public:
void info() {
cout << "CarthaginianHorseman" << endl;
}
};

44.

// Абстрактная фабрика для производства воинов
class ArmyFactory{
public:
virtual Infantryman* createInfantryman() = 0;
virtual Archer* createArcher() = 0;
virtual Horseman* createHorseman() = 0;
virtual ~ArmyFactory() {}
};
// Фабрика для создания воинов Римской армии
class RomanArmyFactory: public ArmyFactory{
public:
Infantryman* createInfantryman() {
return new RomanInfantryman;
}
Archer* createArcher() {
return new RomanArcher;
}
Horseman* createHorseman() {
return new RomanHorseman;
}
};

45.

// Фабрика для создания воинов армии Карфагена
class CarthaginianArmyFactory: public ArmyFactory{
public:
Infantryman* createInfantryman() {
return new CarthaginianInfantryman;
}
Archer* createArcher() {
return new CarthaginianArcher;
}
Horseman* createHorseman() {
return new CarthaginianHorseman;
}};
// Класс, содержащий всех воинов той или иной армии
class Army {
public:
~Army() {
int i;
for(i=0; i<vi.size(); ++i) delete vi[i];
for(i=0; i<va.size(); ++i) delete va[i];
for(i=0; i<vh.size(); ++i) delete vh[i];
}
void info() {
int i;
for(i=0; i<vi.size(); ++i) vi[i]->info();
for(i=0; i<va.size(); ++i) va[i]->info();
for(i=0; i<vh.size(); ++i) vh[i]->info();
}
vector<Infantryman*> vi;
vector<Archer*> va;
vector<Horseman*> vh; };

46.

// Здесь создается армия той или иной стороны
class Game{
public:
Army* createArmy( ArmyFactory& factory ) {
Army* p = new Army;
p->vi.push_back( factory.createInfantryman());
p->va.push_back( factory.createArcher());
p->vh.push_back( factory.createHorseman());
return p;
}
};
int main(){
Game game;
RomanArmyFactory ra_factory;
CarthaginianArmyFactory ca_factory;
Army * ra = game.createArmy( ra_factory);
Army * ca = game.createArmy( ca_factory);
cout << "Roman army:" << endl;
ra->info();
cout << "\nCarthaginian army:" << endl;
ca->info();
// ...
}

47.

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

48.

Преимущества и недостатки
• «+»:
Гарантирует сочетаемость создаваемых продуктов.
Избавляет клиентский код от привязки к конкретным
классам продуктов.
Выделяет код производства продуктов в одно место,
упрощая поддержку кода.
Упрощает добавление новых продуктов в программу.
Реализует принцип открытости/закрытости.
• «-»:
Усложняет код программы из-за введения множества
дополнительных классов.
Требует наличия всех типов продуктов в каждой
вариации.

49.

Строитель (Builder)
Суть паттерна. Позволяет создавать сложные объекты
пошагово. Строитель даёт возможность
использовать один и тот же код строительства для
получения разных представлений объектов.
Проблема. Представьте сложный объект, требующий
кропотливой пошаговой инициализации множества
полей и вложенных объектов. Код инициализации
таких объектов обычно спрятан внутри
монструозного конструктора с десятком
параметров. Либо ещё хуже — распылён по всему
клиентскому коду.

50.

51.

Подумаем о том, как создать объект Дом. Чтобы построить стандартный
дом, нужно поставить 4 стены, установить двери, вставить пару окон и
положить крышу. Но что, если вы хотите дом побольше да посветлее,
имеющий сад, бассейн и прочее добро?
Самое простое решение — расширить класс Дом, создав подклассы для
всех комбинаций параметров дома. Проблема такого подхода — это
громадное количество классов, которые вам придётся создать.
Каждый новый параметр, вроде цвета обоев или материала кровли,
заставит вас создавать всё больше и больше классов для
перечисления всех возможных вариантов.
Чтобы не плодить подклассы, вы можете подойти к решению с другой
стороны. Вы можете создать гигантский конструктор Дома,
принимающий уйму параметров для контроля над создаваемым
продуктом. Действительно, это избавит вас от подклассов, но
приведёт к другой проблеме.
Большая часть этих параметров будет простаивать, а вызовы
конструктора будут выглядеть монструозно из-за длинного списка
параметров. К примеру, далеко не каждый дом имеет бассейн,
поэтому параметры, связанные с бассейнами, будут простаивать
бесполезно в 99% случаев.

52.

Решение. Паттерн Строитель предлагает вынести конструирование
объекта за пределы его собственного класса, поручив это дело
отдельным объектам, называемым строителями. Паттерн
предлагает разбить процесс конструирования объекта на отдельные
шаги (например, построитьСтены, вставитьДвери и другие).
Чтобы создать объект, вам нужно поочерёдно вызывать методы
строителя. Причём не нужно запускать все шаги, а только те, что
нужны для производства объекта определённой конфигурации.
Зачастую один и тот же шаг строительства может отличаться для разных
вариаций производимых объектов. Например, деревянный дом
потребует строительства стен из дерева, а каменный — из камня.
В этом случае вы можете создать несколько классов строителей,
выполняющих одни и те же шаги по-разному. Используя этих
строителей в одном и том же строительном процессе, вы сможете
получать на выходе различные объекты - один строитель делает стены
из дерева и стекла, другой из камня и железа, третий из золота и
бриллиантов. Вызвав одни и те же шаги строительства, в первом
случае вы получите обычный жилой дом, во втором — маленькую
крепость, а в третьем — роскошное жилище.

53.

Вы можете пойти дальше и выделить вызовы методов
строителя в отдельный класс, называемый директором.
В этом случае директор будет задавать порядок шагов
строительства, а строитель — выполнять их.
Отдельный класс директора не является строго
обязательным. Вы можете вызывать методы строителя
и напрямую из клиентского кода. Тем не менее,
директор полезен, если у вас есть несколько способов
конструирования продуктов, отличающихся порядком и
наличием шагов конструирования. В этом случае вы
сможете объединить всю эту логику в одном классе.
Такая структура классов полностью скроет от клиентского
кода процесс конструирования объектов. Клиенту
останется только привязать желаемого строителя к
директору, а затем получить у строителя готовый
результат.

54.

Для того чтобы не нагромождать код
лишними подробностями будем полагать,
что такие рода войск как пехота, лучники и
конница для обеих армий идентичны. А с
целью демонстрации возможностей
паттерна Builder введем новые виды
боевых единиц:
• Катапульты для армии Рима.
• Боевые слоны для армии Карфагена.

55.

class Catapult{
public:
void info() {
cout << "Catapult" << endl;
}
};
class Elephant{
public:
void info() {
cout << "Elephant" << endl;
}
};

56.

// Класс "Армия", содержащий все типы боевых единиц
class Army {
public:
vector<Infantryman> vi;
vector<Archer> va;
vector<Horseman> vh;
vector<Catapult> vc;
vector<Elephant> ve;
void info() {
int i;
for(i=0; i<vi.size(); ++i) vi[i].info();
for(i=0; i<va.size(); ++i) va[i].info();
for(i=0; i<vh.size(); ++i) vh[i].info();
for(i=0; i<vc.size(); ++i) vc[i].info();
for(i=0; i<ve.size(); ++i) ve[i].info();
}
};

57.

// Базовый класс ArmyBuilder объявляет интерфейс для поэтапного
// построения армии и предусматривает его реализацию по
умолчанию
class ArmyBuilder{
protected:
Army* p;
public:
ArmyBuilder(): p(0) {}
virtual ~ArmyBuilder() {}
virtual void createArmy() {}
virtual void buildInfantryman() {}
virtual void buildArcher() {}
virtual void buildHorseman() {}
virtual void buildCatapult() {}
virtual void buildElephant() {}
virtual Army* getArmy() { return p; }
};

58.

// Римская армия имеет все типы боевых единиц кроме боевых слонов
class RomanArmyBuilder: public ArmyBuilder{
public:
void createArmy() { p = new Army; }
void buildInfantryman() { p->vi.push_back( Infantryman()); }
void buildArcher() { p->va.push_back( Archer()); }
void buildHorseman() { p->vh.push_back( Horseman()); }
void buildCatapult() { p->vc.push_back( Catapult()); }
};
// Армия Карфагена имеет все типы боевых единиц кроме катапульт
class CarthaginianArmyBuilder: public ArmyBuilder{
public:
void createArmy() { p = new Army; }
void buildInfantryman() { p->vi.push_back( Infantryman()); }
void buildArcher() { p->va.push_back( Archer()); }
void buildHorseman() { p->vh.push_back( Horseman()); }
void buildElephant() { p->ve.push_back( Elephant()); }
};

59.

// Класс-распорядитель, поэтапно создающий армию той или иной стороны.
// Именно здесь определен алгоритм построения армии.
class Director{
public:
Army* createArmy( ArmyBuilder & builder )
{
builder.createArmy();
builder.buildInfantryman();
builder.buildArcher();
builder.buildHorseman();
builder.buildCatapult();
builder.buildElephant();
return( builder.getArmy());
}
};
int main(){
Director dir;
RomanArmyBuilder ra_builder;
CarthaginianArmyBuilder ca_builder;
Army * ra = dir.createArmy( ra_builder);
Army * ca = dir.createArmy( ca_builder);
cout << "Roman army:" << endl;
ra->info();
cout << "\nCarthaginian army:" << endl;
ca->info();
// ...
return 0;
}

60.

Применимость
• Когда вы хотите избавиться от «телескопического
конструктора».
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
}
• Когда ваш код должен создавать разные
представления какого-то объекта. Например,
деревянные и железобетонные дома.

61.

Преимущества и недостатки
• «+»
Позволяет создавать продукты пошагово.
Позволяет использовать один и тот же код для создания
различных продуктов.
Изолирует сложный код сборки продукта от его основной
бизнес-логики.
• «-»
Усложняет код программы из-за введения
дополнительных классов.
Клиент будет привязан к конкретным классам строителей,
так как в интерфейсе строителя может не быть метода
получения результата.
English     Русский Правила