Паттерн Prototype (прототип)

Назначение паттерна Prototype

Паттерн Prototype (прототип) можно использовать в следующих случаях:

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

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

Описание паттерна Prototype

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

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

UML-диаграмма классов паттерна Prototype

Для порождения объекта некоторого типа в системе должен существовать его прототип. Прототип представляет собой объект того же типа, единственным назначением которого является создание подобных ему объектов. Обычно для удобства все существующие в системе прототипы организуются в специальные коллекции-хранилища или реестры прототипов. Такое хранилище может иметь реализацию в виде ассоциативного массива, каждый элемент которого представляет пару "Идентификатор типа" - "Прототип". Реестр прототипов позволяет добавлять или удалять прототип, а также создавать объект по идентификатору типа. Именно операции динамического добавления и удаления прототипов в хранилище обеспечивают дополнительную гибкость системе, позволяя управлять процессом создания новых объектов.

Реализация паттерна Prototype

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

Также как и для паттерна Factory Method приведем две возможные реализации паттерна Prototype, а именно:

  1. В виде обобщенного конструктора на основе прототипов, когда в полиморфном базовом классе Prototype определяется статический метод, предназначенный для создания объектов. При этом в качестве параметра в этот метод должен передаваться идентификатор типа создаваемого объекта.
  2. На базе специально выделенного класса-фабрики.

Реализация паттерна Ptototype на основе обобщенного конструктора

#include <iostream>
#include <vector>
#include <map>

// Идентификаторы всех родов войск
enum Warrior_ID { Infantryman_ID, Archer_ID, Horseman_ID };

class Warrior;  // Опережающее объявление
typedef map<Warrior_ID, Warrior*> Registry;

// Реестр прототипов определен в виде Singleton Мэйерса
Registry& getRegistry()
{
  static Registry _instance;
  return _instance;
}

// Единственное назначение этого класса - помощь в выборе нужного
// конструктора при создании прототипов
class Dummy { };

// Полиморфный базовый класс. Здесь также определен статический 
// обобщенный конструктор для создания боевых единиц всех родов войск
class Warrior
{
  public:   
    virtual Warrior* clone() = 0;
    virtual void info() = 0;        
    virtual ~Warrior() {}
    // Параметризированный статический метод для создания воинов 
    // всех родов войск
    static Warrior* createWarrior( Warrior_ID id ) {
      Registry& r = getRegistry();
      if (r.find(id) != r.end())
        return r[id]->clone();
      return 0;
    }
  protected: 
    // Добавление прототипа в множество прототипов
    static void addPrototype( Warrior_ID id, Warrior * prototype ) {
      Registry& r = getRegistry();
      r[id] = prototype;
    }
    // Удаление прототипа из множества прототипов
    static void removePrototype( Warrior_ID id ) {
      Registry& r = getRegistry();
      r.erase( r.find( id));
    }
};


// В производных классах различных родов войск в виде статических 
// членов-данных определяются соответствующие прототипы
class Infantryman: public Warrior
{
  public:   
    Warrior* clone() { 
      return new Infantryman( *this); 
    }
    void info() { 
      cout << "Infantryman" << endl; 
    }
  private:
    Infantryman( Dummy ) { 
      Warrior::addPrototype( Infantryman_ID, this); 
    }
    Infantryman() {}
    static Infantryman prototype;
};

class Archer: public Warrior
{
  public:   
    Warrior* clone() { 
      return new Archer( *this); 
    }  
    void info() { 
      cout << "Archer" << endl; 
    }
  private:
    Archer(Dummy) { 
      addPrototype( Archer_ID, this); 
    }
    Archer() {}
    static Archer prototype;
};

class Horseman: public Warrior
{
  public:    
    Warrior* clone() { 
      return new Horseman( *this); 
    }
    void info() { 
      cout << "Horseman" << endl; 
    }
  private:
    Horseman(Dummy) { 
      addPrototype( Horseman_ID, this); 
    }
    Horseman() {}
    static Horseman prototype;
};


Infantryman Infantryman::prototype = Infantryman( Dummy());
Archer Archer::prototype = Archer( Dummy());
Horseman Horseman::prototype = Horseman( Dummy());


int main()
{    
  vector<Warrior*> v;
  v.push_back( Warrior::createWarrior( Infantryman_ID));
  v.push_back( Warrior::createWarrior( Archer_ID));
  v.push_back( Warrior::createWarrior( Horseman_ID));   

  for(int i=0; i<v.size(); i++)
    v[i]->info();
  // ...
}

В приведенной реализации классы всех создаваемых военных единиц, таких как лучники, пехотинцы и конница, являются подклассами абстрактного базового класса Warrior. В этом классе определен обобщенный конструктор в виде статического метода createWarrior(Warrior_ID id). Передавая в этот метод в качестве параметра тип боевой единицы, можно создавать воинов нужных родов войск. Для этого обобщенный конструктор использует реестр прототипов, реализованный в виде ассоциативного массива std::map, каждый элемент которого представляет собой пару "идентификатор типа воина" - "его прототип".

Добавление прототипов в реестр происходит автоматически. Сделано это следующим образом. В подклассах Infantryman, Archer, Horseman, прототипы определяются в виде статических членов данных тех же типов. При создании такого прототипа будет вызываться конструктор с параметром типа Dummy, который и добавит этот прототип в реестр прототипов с помощью метода addPrototype() базового класса Warrior. Важно, чтобы к этому моменту сам объект реестра был полностью сконструирован, именно поэтому он выполнен в виде singleton Мэйерса.

Для приведенной реализации паттерна Prototype можно отметить следующие особенности:

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

Реализация паттерна Prototype с помощью выделенного класса-фабрики

#include <iostream>
#include <vector>

// Иерархия классов игровых персонажей
// Полиморфный базовый класс
class Warrior
{
  public:   
    virtual Warrior* clone() = 0;
    virtual void info() = 0;        
    virtual ~Warrior() {}
};


// Производные классы различных родов войск
class Infantryman: public Warrior
{
    friend class PrototypeFactory;
  public:   
    Warrior* clone() { 
      return new Infantryman( *this); 
    }
    void info() { 
      cout << "Infantryman" << endl; 
    } 
  private:
    Infantryman() {}    
};

class Archer: public Warrior
{
    friend class PrototypeFactory;
  public:   
    Warrior* clone() { 
      return new Archer( *this); 
    }  
    void info() { 
      cout << "Archer" << endl; 
    }
  private:  
    Archer() {} 
};

class Horseman: public Warrior
{
    friend class PrototypeFactory;
  public:    
    Warrior* clone() { 
      return new Horseman( *this); 
    }
    void info() { 
      cout << "Horseman" << endl; 
    }
  private:  
    Horseman() {}
};


// Фабрика для создания боевых единиц всех родов войск
class PrototypeFactory
{
  public:   
    Warrior* createInfantrman() {       
      static Infantryman prototype;
      return prototype.clone();
    }
    Warrior* createArcher() {       
      static Archer prototype;
      return prototype.clone();
    }
    Warrior* createHorseman() {     
      static Horseman prototype;
      return prototype.clone();
    }
};


int main()
{    
  PrototypeFactory factory;
  vector<Warrior*> v;
  v.push_back( factory.createInfantrman());
  v.push_back( factory.createArcher());
  v.push_back( factory.createHorseman());   

  for(int i=0; i<v.size(); i++)
    v[i]->info();
    // ...
}

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

Результаты применения паттерна Prototype

Достоинства паттерна Prototype

  • Для создания новых объектов клиенту необязательно знать их конкретные классы.
  • Возможность гибкого управления процессом создания новых объектов за счет возможности динамических добавления и удаления прототипов в реестр.

Недостатки паттерна Prototype

  • Каждый тип создаваемого продукта должен реализовывать операцию клонирования clone(). В случае, если требуется глубокое копирование объекта (объект содержит ссылки или указатели на другие объекты), это может быть непростой задачей.

results matching ""

    No results matching ""