Шаблон проектирования Prototype. Примеры

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

Шаблон проектирования “Прототип” применяется если:

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

Статья состоит из двух частей:

  1. описан шаблон проектирования “Прототип” на наиболее общих примерах (для тех, кто не знаком с библиотекой Qt);
  2. показано каким образом можно модифицировать программу предыдущей статьи для поддержки динамической загрузки новых типов графических объектов.

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

1 Шаблон проектирования “Прототип”

Рассмотрим пару примеров задач, в которых применим шаблон проектирования “Прототип”:

  1. В вашей игре используется покадровая анимация. Каждая анимация может встречаться несколько раз. Создание анимации связано с доступом к файлу, вырезанию кадров и прочими сложными операциями. Возможно загрузить анимации в программу только один раз, а затем клонировать их – это должно быть более эффективно, чем создание анимации напрямую;
  2. Вам нужна возможность добавлять новые типы существ прямо во время игры. Все существа игры описаны в файлах, в определенном формате. Обращаться к файлу всякий раз, когда требуется добавить особь в игру – не оптимально. Эффективней загрузить нужный тип особи с файла единожды, а затем, клонировать его.

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

Фримен при описании “Прототипа” попутно рассказывает про что-то типа “Объектного пула” (говоря о том, что нужно какое-то хранилище прототипов, которое будет обрабатывать запросы на создание объекта) [1]. Выглядит этот вариант примерно следующим образом:

рис. 1 пул прототипов

рис. 1 пул прототипов

Клиент добавляет прототипы в пул, задавая их ключ методом add(). Затем, по ключу получает копии прототипов методом get(). Пул может хранить прототипы, например, в словаре. Работа пула в этом случае заключается в получении нужного прототипа по ключу и вызове его метода clone().

class Pool {
  map<string, Prototype> m_protos;
public:
  void add(Prototype* proto, string key) {
    if (m_protos.find(key) != m_protos.end()) // если прототип уже есть
      return; // добавлять еще раз не надо (можно исключение кинуть, например...)
    m_protos[key] = proto; // добавили прототип в словарь
  }
  Prototype *get(string key) {
    if (m_protos.find(key) == m_protos.end()) // нужного прототипа нет ...
      return nullptr;
    return m_protos[key].clone(); // вызываем виртуальный метод clone() прототипа
  }
};

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

Пула может и не быть, Гамма, например на этом акцента не делает [2]. В примере из второй части статьи пул тоже не используется. Но при этом, какой то класс все таки должен принимать решение о том, какой именно прототип клонировать.

2 Пример использования шаблона Prototype

Продублирую UML диаграмму классов графического редактора из предыдущей статьи, чтобы было понятно какую проблему в этом случае решает шаблон проектирования “Прототип”.

рис. 2 UML диаграмма классов графических элементов

рис. 2 диаграмма классов графических элементов ДО имплементации шаблона “Прототип”

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

Допустим, добавляемые графические элементы описываются следующим форматом:

<имя фигуры> <ширина> <высота>
<количество точек> <список точек>
  Например:
    rect 20 40 4 -10 -20 -10 20 10 20 10 -20
    rhomb 20 40 4 -10 0 0 20 10 0 0 -20
    parallelogram 20 40 4 -10 -10 -10 20 10 10 10 -20
    curveRect 20 40 6 -10 -10 -10 10 0 20 10 20 10 -20 0 -20

Каждая фигура теперь способна копировать себя (метод clone()) и возвращать свое имя (метод name()). Кроме того, она хранит набор точек, по которым может себя построить.

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

Графические объекты размещаются на графической сцене. Метод mousePressEvent() класса Scene до внедрения прототипа содержал очень не красивый switch:

switch (m_figureType) {
  case Rect:
    item = new RectFigure();
    break;
  case Rhomb:
    item = new RhombFigure();
    break;
  }

Теперь такой switch не нужен. Графическая сцена хранит имя текущего графического объекта (m_curName), выделенного в тулбаре и список прототипов (m_figures). При необходимости добавления объекта, сцена ищет в списке прототипов нужный объект и клонирует его (выполняет функции пула).

void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
  QGraphicsItem *item = nullptr;
  foreach (Figure *t, m_figures)
    if (t->name() == m_curName) {
      item = t->clone();
      break;
    }
  if (nullptr == item) return;
  item->setPos(event->scenePos());
  addItem(item);
}

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

void FiguresReader::run(QString filename) {
  QFile file(filename);
  if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    return;
  while (false == file.atEnd()) {
    Figure *newFigure = new Figure(file.readLine());
    emit figure(newFigure);
  }
}

Диаграмма классов программы после введения шаблона проектирования “Прототип” приведена на рис. 3. Пунктирными стрелками показаны зависимости, хотя большая их часть появляется во время выполнения (за счет использования QObject::connect()).

рис. 3 диаграмма классов после имплементации шаблона "Прототип"

рис. 3 диаграмма классов после имплементации шаблона “Прототип”

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

рис. 4 снимок окна программы

рис. 4 снимок окна программы

Исходный код проекта, как всегда, можно скачать: шаблон проектирования “Прототип”.

Литература:

  1. Фримен Эр. “Паттерны проектирования” Фримен Эр., Фримен Эл., Бейтс Б., Сьерра К – Питер: 2011
  2. Э. Гамма “Приемы объектно-ориентированного проектирования. Паттерны проектирования” Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес – Питер, 2012

Аннотации на эти и другие книги по теме “проектирование ПО” можно прочитать в соответствующем разделе.

Добавить комментарий