Поиск по сайту

 

Паттерны » Порождающие паттерны

Порождающие паттерны

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

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

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

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

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

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

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

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

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

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

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

Паттерн Abstract Factory использует несколько фабричных методов и предназначен для создания целого семейства или группы взаимосвязанных объектов. Для случая нашей стратегической игры такими взаимосвязанными объектами могли бы быть объекты всех персонажей (воины и крестьяне) для конкретной расы.

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

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

Паттерн Singleton контролирует создание единственного экземпляра некоторого класса и предоставляет доступ к нему.

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

Новичкам можно рекомендовать изучение порождающих паттернов в следующей последовательности: Singleton, Factory Method, Abstract Factory, Prototype, остальные паттерны.