Прототип
Прототип — это порождающий паттерн проектирования, который позволяет копировать объекты, не вдаваясь в подробности их реализации.
Проблема
У вас есть объект, который нужно скопировать. Как это сделать? Нужно создать пустой объект такого же класса, а затем поочерёдно скопировать значения всех полей из старого объекта в новый.
Прекрасно! Но есть нюанс. Не каждый объект удастся скопировать таким образом, ведь часть его состояния может быть приватной, а значит — недоступной для остального кода программы.
Но есть и другая проблема. Копирующий код станет зависим от классов копируемых объектов. Ведь, чтобы перебрать все поля объекта, нужно привязаться к его классу. Из-за этого вы не сможете копировать объекты, зная только их интерфейсы, а не конкретные классы.
Решение
Паттерн Прототип поручает создание копий самим копируемым объектам. Он вводит общий интерфейс для всех объектов, поддерживающих клонирование. Это позволяет копировать объекты, не привязываясь к их конкретным классам. Обычно такой интерфейс имеет всего один метод clone .
Реализация этого метода в разных классах очень схожа. Метод создаёт новый объект текущего класса и копирует в него значения всех полей собственного объекта. Так получится скопировать даже приватные поля, так как большинство языков программирования разрешает доступ к приватным полям любого объекта текущего класса.
Объект, который копируют, называется прототипом (откуда и название паттерна). Когда объекты программы содержат сотни полей и тысячи возможных конфигураций, прототипы могут служить своеобразной альтернативой созданию подклассов.
В этом случае все возможные прототипы заготавливаются и настраиваются на этапе инициализации программы. Потом, когда программе нужен новый объект, она создаёт копию из приготовленного прототипа.
Аналогия из жизни
В промышленном производстве прототипы создаются перед основной партией продуктов для проведения всевозможных испытаний. При этом прототип не участвует в последующем производстве, отыгрывая пассивную роль.
Прототип на производстве не делает копию самого себя, поэтому более близкий пример паттерна — деление клеток. После митозного деления клеток образуются две совершенно идентичные клетки. Оригинальная клетка отыгрывает роль прототипа, принимая активное участие в создании нового объекта.
Структура
Базовая реализация
- Интерфейс прототипов описывает операции клонирования. В большинстве случаев — это единственный метод clone .
- Конкретный прототип реализует операцию клонирования самого себя. Помимо банального копирования значений всех полей, здесь могут быть спрятаны различные сложности, о которых не нужно знать клиенту. Например, клонирование связанных объектов, распутывание рекурсивных зависимостей и прочее.
- Клиент создаёт копию объекта, обращаясь к нему через общий интерфейс прототипов.
Реализация с общим хранилищем прототипов
- Хранилище прототипов облегчает доступ к часто используемым прототипам, храня набор предварительно созданных эталонных, готовых к копированию объектов. Простейшее хранилище может быть построено с помощью хеш-таблицы вида имя-прототипа → прототип . Но для удобства поиска прототипы можно маркировать и другими критериями, а не только условным именем.
Псевдокод
В этом примере Прототип позволяет производить точные копии объектов геометрических фигур, не привязываясь к их классам.
Все фигуры реализуют интерфейс клонирования и предоставляют метод для воспроизводства самой себя. Подклассы используют метод клонирования родителя, а затем копируют собственные поля в получившийся объект.
// Базовый прототип. abstract class Shape is field X: int field Y: int field color: string // Обычный конструктор. constructor Shape() is // . // Конструктор прототипа. constructor Shape(source: Shape) is this() this.X = source.X this.Y = source.Y this.color = source.color // Результатом операции клонирования всегда будет объект из // иерархии классов Shape. abstract method clone():Shape // Конкретный прототип. Метод клонирования создаёт новый объект // текущего класса, передавая в его конструктор ссылку на // собственный объект. Благодаря этому операция клонирования // получается атомарной — пока не выполнится конструктор, нового // объекта ещё не существует. Но как только конструктор завершит // работу, мы получим полностью готовый объект-клон, а не пустой // объект, который нужно ещё заполнить. class Rectangle extends Shape is field width: int field height: int constructor Rectangle(source: Rectangle) is // Вызов родительского конструктора нужен, чтобы // скопировать потенциальные приватные поля, объявленные // в родительском классе. super(source) this.width = source.width this.height = source.height method clone():Shape is return new Rectangle(this) class Circle extends Shape is field radius: int constructor Circle(source: Circle) is super(source) this.radius = source.radius method clone():Shape is return new Circle(this) // Где-то в клиентском коде. class Application is field shapes: array of Shape constructor Application() is Circle circle = new Circle() circle.X = 10 circle.Y = 10 circle.radius = 20 shapes.add(circle) Circle anotherCircle = circle.clone() shapes.add(anotherCircle) // anotherCircle будет содержать точную копию circle. Rectangle rectangle = new Rectangle() rectangle.width = 10 rectangle.height = 20 shapes.add(rectangle) method businessLogic() is // Плюс Прототипа в том, что вы можете клонировать набор // объектов, не зная их конкретные классы. Array shapesCopy = new Array of Shapes. // Например, мы не знаем, какие конкретно объекты // находятся внутри массива shapes, так как он объявлен // с типом Shape. Но благодаря полиморфизму, мы можем // клонировать все объекты «вслепую». Будет выполнен // метод clone того класса, которым является этот // объект. foreach (s in shapes) do shapesCopy.add(s.clone()) // Переменная shapesCopy будет содержать точные копии // элементов массива shapes.
Применимость
Когда ваш код не должен зависеть от классов копируемых объектов.
Такое часто бывает, если ваш код работает с объектами, поданными извне через какой-то общий интерфейс. Вы не можете привязаться к их классам, даже если бы хотели, поскольку их конкретные классы неизвестны.
Паттерн прототип предоставляет клиенту общий интерфейс для работы со всеми прототипами. Клиенту не нужно зависеть от всех классов копируемых объектов, а только от интерфейса клонирования.
Когда вы имеете уйму подклассов, которые отличаются начальными значениями полей. Кто-то мог создать все эти классы, чтобы иметь возможность легко порождать объекты с определённой конфигурацией.
Паттерн прототип предлагает использовать набор прототипов, вместо создания подклассов для описания популярных конфигураций объектов.
Таким образом, вместо порождения объектов из подклассов, вы будете копировать существующие объекты-прототипы, в которых уже настроено внутреннее состояние. Это позволит избежать взрывного роста количества классов в программе и уменьшить её сложность.
Шаги реализации
- Создайте интерфейс прототипов с единственным методом clone . Если у вас уже есть иерархия продуктов, метод клонирования можно объявить непосредственно в каждом из её классов.
- Добавьте в классы будущих прототипов альтернативный конструктор, принимающий в качестве аргумента объект текущего класса. Этот конструктор должен скопировать из поданного объекта значения всех полей, объявленных в рамках текущего класса, а затем передать выполнение родительскому конструктору, чтобы тот позаботился о полях, объявленных в суперклассе. Если ваш язык программирования не поддерживает перегрузку методов, то вам не удастся создать несколько версий конструктора. В этом случае копирование значений можно проводить и в другом методе, специально созданном для этих целей. Конструктор удобнее тем, что позволяет клонировать объект за один вызов.
- Метод клонирования обычно состоит всего из одной строки: вызова оператора new с конструктором прототипа. Все классы, поддерживающие клонирование, должны явно определить метод clone , чтобы использовать собственный класс с оператором new . В обратном случае результатом клонирования станет объект родительского класса.
- Опционально, создайте центральное хранилище прототипов. В нём удобно хранить вариации объектов, возможно, даже одного класса, но по-разному настроенных. Вы можете разместить это хранилище либо в новом фабричном классе, либо в фабричном методе базового класса прототипов. Такой фабричный метод должен на основании входящих аргументов искать в хранилище прототипов подходящий экземпляр, а затем вызывать его метод клонирования и возвращать полученный объект. Наконец, нужно избавиться от прямых вызовов конструкторов объектов, заменив их вызовами фабричного метода хранилища прототипов.
Преимущества и недостатки
- Позволяет клонировать объекты, не привязываясь к их конкретным классам.
- Меньше повторяющегося кода инициализации объектов.
- Ускоряет создание объектов.
- Альтернатива созданию подклассов для конструирования сложных объектов.
- Сложно клонировать составные объекты, имеющие ссылки на другие объекты.
Отношения с другими паттернами
- Многие архитектуры начинаются с применения Фабричного метода (более простого и расширяемого через подклассы) и эволюционируют в сторону Абстрактной фабрики, Прототипа или Строителя (более гибких, но и более сложных).
- Классы Абстрактной фабрики чаще всего реализуются с помощью Фабричного метода, хотя они могут быть построены и на основе Прототипа.
- Если Команду нужно копировать перед вставкой в историю выполненных команд, вам может помочь Прототип.
- Архитектура, построенная на Компоновщиках и Декораторах, часто может быть улучшена за счёт внедрения Прототипа. Он позволяет клонировать сложные структуры объектов, а не собирать их заново.
- Прототип не опирается на наследование, но ему нужна сложная операция инициализации. Фабричный метод, наоборот, построен на наследовании, но не требует сложной инициализации.
- Снимок иногда можно заменить Прототипом, если объект, состояние которого требуется сохранять в истории, довольно простой, не имеет активных ссылок на внешние ресурсы либо их можно легко восстановить.
- Абстрактная фабрика, Строитель и Прототип могут быть реализованы при помощи Одиночки.
Примеры реализации паттерна
Не втыкай в транспорте
Лучше почитай нашу книгу о паттернах проектирования.
Теперь это удобно делать даже во время поездок в общественном транспорте.
Эта статья является частью нашей электронной книги Погружение в Паттерны Проектирования.
- Премиум контент
- Книга о паттернах
- Курс по рефакторингу
- Введение в рефакторинг
- Чистый код
- Технический долг
- Когда рефакторить
- Как рефакторить
- Раздувальщики
- Длинный метод
- Большой класс
- Одержимость элементарными типами
- Длинный список параметров
- Группы данных
- Операторы switch
- Временное поле
- Отказ от наследства
- Альтернативные классы с разными интерфейсами
- Расходящиеся модификации
- Стрельба дробью
- Параллельные иерархии наследования
- Комментарии
- Дублирование кода
- Ленивый класс
- Класс данных
- Мёртвый код
- Теоретическая общность
- Завистливые функции
- Неуместная близость
- Цепочка вызовов
- Посредник
- Неполнота библиотечного класса
- Составление методов
- Извлечение метода
- Встраивание метода
- Извлечение переменной
- Встраивание переменной
- Замена переменной вызовом метода
- Расщепление переменной
- Удаление присваиваний параметрам
- Замена метода объектом методов
- Замена алгоритма
- Перемещение метода
- Перемещение поля
- Извлечение класса
- Встраивание класса
- Сокрытие делегирования
- Удаление посредника
- Введение внешнего метода
- Введение локального расширения
- Самоинкапсуляция поля
- Замена простого поля объектом
- Замена значения ссылкой
- Замена ссылки значением
- Замена поля-массива объектом
- Дублирование видимых данных
- Замена однонаправленной связи двунаправленной
- Замена двунаправленной связи однонаправленной
- Замена магического числа символьной константой
- Инкапсуляция поля
- Инкапсуляция коллекции
- Замена кодирования типа классом
- Замена кодирования типа подклассами
- Замена кодирования типа состоянием/стратегией
- Замена подкласса полями
- Разбиение условного оператора
- Объединение условных операторов
- Объединение дублирующихся фрагментов в условных операторах
- Удаление управляющего флага
- Замена вложенных условных операторов граничным оператором
- Замена условного оператора полиморфизмом
- Введение Null-объекта
- Введение проверки утверждения
- Переименование метода
- Добавление параметра
- Удаление параметра
- Разделение запроса и модификатора
- Параметризация метода
- Замена параметра набором специализированных методов
- Передача всего объекта
- Замена параметра вызовом метода
- Замена параметров объектом
- Удаление сеттера
- Сокрытие метода
- Замена конструктора фабричным методом
- Замена кода ошибки исключением
- Замена исключения проверкой условия
- Подъём поля
- Подъём метода
- Подъём тела конструктора
- Спуск метода
- Спуск поля
- Извлечение подкласса
- Извлечение суперкласса
- Извлечение интерфейса
- Свёртывание иерархии
- Создание шаблонного метода
- Замена наследования делегированием
- Замена делегирования наследованием
- Введение в паттерны
- Что такое Паттерн?
- История паттернов
- Зачем знать паттерны?
- Критика паттернов
- Классификация паттернов
- Фабричный метод
- Абстрактная фабрика
- Строитель
- Прототип
- Одиночка
- Адаптер
- Мост
- Компоновщик
- Декоратор
- Фасад
- Легковес
- Заместитель
- Цепочка обязанностей
- Команда
- Итератор
- Посредник
- Снимок
- Наблюдатель
- Состояние
- Стратегия
- Шаблонный метод
- Посетитель
- C#
- C++
- Go
- Java
- PHP
- Python
- Ruby
- Rust
- Swift
- TypeScript
Паттерн Prototype (прототип)
Паттерн Prototype (прототип) можно использовать в следующих случаях:
- Система должна оставаться независимой как от процесса создания новых объектов, так и от типов порождаемых объектов. Непосредственное использование выражения new в коде приложения считается нежелательным (подробнее об этом в разделе Порождающие паттерны).
- Необходимо создавать объекты, точные классы которых становятся известными уже на стадии выполнения программы.
Паттерн Factory Method также делает систему независимой от типов порождаемых объектов, но для этого он вводит параллельную иерархию классов: для каждого типа создаваемого объекта должен присутствовать соответствующий класс-фабрика, что может быть нежелательно. Паттерн Prototype лишен этого недостатка.
Описание паттерна Prototype
Для создания новых объектов паттерн Prototype использует прототипы. Прототип — это уже существующий в системе объект, который поддерживает операцию клонирования, то есть умеет создавать копию самого себя. Таким образом, для создания объекта некоторого класса достаточно выполнить операцию clone() соответствующего прототипа.
Паттерн Prototype реализует подобное поведение следующим образом: все классы, объекты которых нужно создавать, должны быть подклассами одного общего абстрактного базового класса. Этот базовый класс должен объявлять интерфейс метода clone() . Также здесь могут объявляться виртуальными и другие общие методы, например, initialize() в случае, если после клонирования нужна инициализация вновь созданного объекта. Все производные классы должны реализовывать метод clone() . В языке С++ для создания копий объектов используется конструктор копирования, однако, в общем случае, создание объектов при помощи операции копирования не является обязательным.
UML-диаграмма классов паттерна Prototype
Для порождения объекта некоторого типа в системе должен существовать его прототип. Прототип представляет собой объект того же типа, единственным назначением которого является создание подобных ему объектов. Обычно для удобства все существующие в системе прототипы организуются в специальные коллекции-хранилища или реестры прототипов. Такое хранилище может иметь реализацию в виде ассоциативного массива, каждый элемент которого представляет пару «Идентификатор типа» — «Прототип». Реестр прототипов позволяет добавлять или удалять прототип, а также создавать объект по идентификатору типа. Именно операции динамического добавления и удаления прототипов в хранилище обеспечивают дополнительную гибкость системе, позволяя управлять процессом создания новых объектов.
Реализация паттерна Prototype
Приведем реализацию паттерна Prototype на примере построения армий для военной стратегии «Пунические войны». Подробное описание этой игры можно найти в разделе Порождающие паттерны. Для упрощения демонстрационного кода будем создавать военные персонажи для некой абстрактной армии без учета особенностей воюющих сторон.
Также как и для паттерна Factory Method приведем две возможные реализации паттерна Prototype, а именно:
- В виде обобщенного конструктора на основе прототипов, когда в полиморфном базовом классе Prototype определяется статический метод, предназначенный для создания объектов. При этом в качестве параметра в этот метод должен передаваться идентификатор типа создаваемого объекта.
- На базе специально выделенного класса-фабрики.
Реализация паттерна Ptototype на основе обобщенного конструктора
#include #include #include // Идентификаторы всех родов войск enum Warrior_ID < Infantryman_ID, Archer_ID, Horseman_ID >; class Warrior; // Опережающее объявление typedef map 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 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 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 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() < vectorv; 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; iinfo(); // . >
В приведенной реализации классы всех создаваемых военных единиц, таких как лучники, пехотинцы и конница, являются подклассами абстрактного базового класса Warrior. В этом классе определен обобщенный конструктор в виде статического метода createWarrior(Warrior_ID id). Передавая в этот метод в качестве параметра тип боевой единицы, можно создавать воинов нужных родов войск. Для этого обобщенный конструктор использует реестр прототипов, реализованный в виде ассоциативного массива std::map, каждый элемент которого представляет собой пару «идентификатор типа воина» — «его прототип».
Добавление прототипов в реестр происходит автоматически. Сделано это следующим образом. В подклассах Infantryman, Archer, Horseman, прототипы определяются в виде статических членов данных тех же типов. При создании такого прототипа будет вызываться конструктор с параметром типа Dummy, который и добавит этот прототип в реестр прототипов с помощью метода addPrototype() базового класса Warrior. Важно, чтобы к этому моменту сам объект реестра был полностью сконструирован, именно поэтому он выполнен в виде singleton Мэйерса.
Для приведенной реализации паттерна Prototype можно отметить следующие особенности:
- Создавать новых воинов можно только при помощи обобщенного конструктора. Их непосредственное создание невозможно, так как соответствующие конструкторы объявлены со спецификатором доступа private.
- Отсутствует недостаток реализации на базе обобщенного конструктора для паттерна Factory Method, а именно базовый класс Warrior ничего не знает о своих подклассах.
Реализация паттерна Prototype с помощью выделенного класса-фабрики
#include #include // Иерархия классов игровых персонажей // Полиморфный базовый класс 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 private: Infantryman() <> >; class Archer: public Warrior < friend class PrototypeFactory; public: Warrior* clone() < return new Archer( *this); >void info() < cout private: Archer() <> >; class Horseman: public Warrior < friend class PrototypeFactory; public: Warrior* clone() < return new Horseman( *this); >void info() < cout 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; vectorv; v.push_back( factory.createInfantrman()); v.push_back( factory.createArcher()); v.push_back( factory.createHorseman()); for(int i=0; iinfo(); // . >
В приведенной реализации для упрощения кода реестр прототипов не ведется. Воины всех родов войск создаются при помощи соответствующих методов фабричного класса PrototypeFactory, где и определены прототипы в виде статических переменных.
Результаты применения паттерна Prototype
Достоинства паттерна Prototype
- Для создания новых объектов клиенту необязательно знать их конкретные классы.
- Возможность гибкого управления процессом создания новых объектов за счет возможности динамических добавления и удаления прототипов в реестр.
Недостатки паттерна Prototype
- Каждый тип создаваемого продукта должен реализовывать операцию клонирования clone(). В случае, если требуется глубокое копирование объекта (объект содержит ссылки или указатели на другие объекты), это может быть непростой задачей.
Tech blog by @dizballanze
Следующий порождающий объекты шаблон, который мы рассмотрим — прототип. Данный шаблон является более гибкой версией шаблона Abstract Factory.
Описание
Часто приходится отказываться от затеи использовать шаблон абстрактная фабрика из-за необходимости поддерживать дополнительные иерархии классов. С этой проблемой нам может помочь шаблон prototype, который основан на использовании средства clone языка php для создания копий объектов.
Данный шаблон подразумевает наличие класса, объекты которого хранят внутри себя, как свойство некоторые эталонные экземпляры порождаемых классов. При этом условием начала работы с объектом-прототипом есть инициализация его этими эталонными объектами, как правило они передаются в конструктор, но могут быть установлены и при помощи других методов прототипа. После инициализации прототипа к нему можно обращаться непосредственно для генерации объектов, что приведет к созданию полностью идентичной копии эталонного объекта и возврату его вызывающему коду. Так как мы используем готовые объекты для инициализации прототипа, то нам больше не нужно создавать параллельные иерархии классов. Но это не единственно преимущество данного шаблона. Если копнуть глубже, то можно найти много новых возможностей, например из-за того что мы получаем точную копию эталонного объекта, а не новый объект, как в шаблоне абстрактная фабрика, мы получаем дополнительные возможности в виде настройки эталонного объекта (изменение значения свойств). Также гибкость достигается и в не обязательном соответствии комбинаций взаимосвязанных классов, так как это было в шаблоне абстрактная фабрика.
Реализация
Давайте разберем простой пример реализации данного шаблона. Представим, что нам нужно спроектировать иерархию классов которая буде отвечать за стиль отображения страницы документа. Ограничимся настройкой стилей параграфов, заголовков и списков. Для каждого из элементов у нас будет своя иерархия классов которая позволит выводить данный элемент скажем на принтер и браузер. Теперь собственно пример такой иерархии (без реализации, т.к. нас интересует только логические понятия, а не сам функционал):
abstract class Paragraph abstract public function format($data); > abstract class Header abstract public function header_big($data); abstract public function header_small($data); > abstract class Lists abstract public function numeric($data); abstract public function alpha($data); > class PrintedParagraph extends Paragraph public function format($data) // Some operations > > class PrintedHeader extends Header public function header_big($data) // Some operations > public function header_small($data) // Some operations > > class PrintedLists extends Lists public function numeric($data) // Some operations > public function alpha($data) // Some operations > > class BrowsedParagraph extends Paragraph public function format($data) // Some operations > > class BrowsedHeader extends Header public function header_big($data) // Some operations > public function header_small($data) // Some operations > > class BrowsedLists extends Lists public function numeric($data) // Some operations > public function alpha($data) // Some operations > > class DocumentPrototype protected $_paragraph; protected $_header; protected $_list; public function __construct(Paragraph $paragraph, Header $header, Lists $list) $this->_paragraph = $paragraph; $this->_header = $header; $this->_list = $list; > public function getParagraph() return clone $this->_paragraph; > public function getHeader() return clone $this->_header; > public function getList() return clone $this->_list; > >
Итак, что мы тут видим. У нас имеется 3 иерархии классов, которые позволяют форматировать различные элементы документа и нам необходимо создавать экземпляры этих классов в процессе работы программы, экземпляры каких именно классов нам нужно создавать заранее не известно. По-этому мы реализовываем ещё один класс — DocumentPrototype, который как вы уже наверное догадались, представляет собой паттерн прототип. Как вы видите данный класс принимает в конструкторе эталонные экземпляры классов-элементов документа и потом умеет создавать их копии при помощи соответствующих методов. Рассмотрим пример использования данного класса:
// . $document = new DocumentPrototype(new BrowsedParagraph(), new BrowsedHeader(), new BrowsedLists()); $list = $document->getList(); $list->numeric(array('first','second', 'third')); $header = $document->getHeader(); $header->header_big('Test');
Мы получаем возможность создавать объекты без поддержки дополнительных иерархий классов. Теперь давайте представим, что нам нужно создать документ у которого бы параграфы выводились так, как если бы они выводились на печать, а заголовки и списки, так, как на web-странице:
// . $document = new DocumentPrototype(new PrintedParagraph(), new BrowsedHeader(), new BrowsedLists());
Всё довольно просто, мы всего лишь заменили один аргумент конструктора на другой и получили такой отличный результат. К примеру каждый из этих классов может принимать массив параметров в котором указываются какие-то настройки стиля отображения элементов, теперь нам для того что-бы скажем увеличить размер шрифта заголовка нужно всего лишь изменить эталонный объект следующим образом:
// . $header = new BrowsedHeader(array('font-size'=>'200%')); $document = new DocumentPrototype(new PrintedParagraph(), $header, new BrowsedLists());
Вот таким простым способом мы увеличили размер шрифта всех заголовков документа.
В подведении итогов перечислим преимущества паттерна Prototype:
- Нет необходимости сопровождать дополнительные иерархии классов
- Гибкость настройки эталонных объектов
- Гибкость композиции
- Простое масштабирование порождаемой иерархии
- Гибкость во времени выполнения программы
Спасибо за внимание, до встречи в мире ООП 😉
Прототип(Prototype)
Первый раз я узнал о существовании слова «прототип» из Паттернов проектирования. Сейчас это слово достаточно популярно. Но обычно его используют без привязки к шаблону проектирования GoF . Мы еще к этому вернемся, но для начала я хочу показать вам другое, более интересные области, где можно встретить термин «прототип» и стоящую за ним концепцию. А для начала давайте рассмотрим оригинальный шаблон.
Я умышленно не пишу здесь «оригинальный». Паттерны проектирования цитируют легендарный проект Sketchpad 1963-го года за авторством Ивана Сазерленда, который можно считать первым примером применения шаблона в природе. Когда все остальные слушали Дилана и Битлз, Сазерленд был занят всего-навсего изобретением базовых концепций CAD , интерактивной графики и объектно-ориентированного программирования.
Можете посмотреть демо и впечатлиться.
Шаблон проектирования прототип
Давайте представим, что мы делаем игру в стиле Gauntlet. У нас есть всякие существа и демоны, роящиеся вокруг героя и норовящие откусить кусочек его плоти. Эти незваные сотрапезники появляются через «спаунеры» (spawners, тут, источники-генераторы создания существ, прим.пер.) и для каждого типа врагов есть отдельный тип спаунера.
Для упрощения примера давайте сделаем предположение, что для каждого типа монстра в игре имеется отдельный тип. Т.е. у нас есть C++ классы для Ghost , Demon , Sorcerer и т.д.:
class Monster < // Stuff. >; class Ghost : public Monster <>; class Demon : public Monster <>; class Sorcerer : public Monster <>;
Спаунер конструирует экземпляры одного из типов монстров. Для поддержки всех монстров в игре мы можем использовать прямолинейный подход и заведем класс спаунер для каждого класса монстра. В результате получится следующая иерархия:
Я должен был выкопать пыльную книгу по UML, чтобы сделать эту схему. означает «наследуется».
Реализация будет выглядеть так:
class Spawner < public: virtual ~Spawner() <> virtual Monster* spawnMonster() = 0; >; class GhostSpawner : public Spawner < public: virtual Monster* spawnMonster() < return new Ghost(); > >; class DemonSpawner : public Spawner < public: virtual Monster* spawnMonster() < return new Demon(); > >; // Ну вы поняли.
Если вам конечно не платят за каждую строчку кода, использовать такой подход совсем не весело. Куча классов, куча похожего кода, куча избыточности, куча дублей, куча самоповторов.
Шаблон Прототип предлагает решение. Ключевой мыслью является создание объекта, который может порождать объекты, похожие на себя. Если у вас есть один призрак, вы можете с его помощью получить кучу призраков. Если есть демон, можно сделать больше демонов. Любого монстра можно трактовать как прототипируемого монстра, используемого для генерации новых версий его самого.
Для реализации этой идеи, мы дадим нашему базовому классу Monster абстрактный метод clone() :
class Monster < public: virtual ~Monster() <> virtual Monster* clone() = 0; // Другие вещи. >;
Каждый подкласс монстра предоставляет свою реализацию, которая возвращает объект, идентичный по классу и состоянию ему самому. Например:
class Ghost : public Monster < public: Ghost(int health, int speed) : health_(health), speed_(speed) <> virtual Monster* clone() < return new Ghost(health_, speed_); > private: int health_; int speed_; >;
Как только все монстры будут его поддерживать, нам больше не нужен будет отдельный класс спаунер для каждого класса монстров. Вместо этого мы обойдемся всего одним:
class Spawner < public: Spawner(Monster* prototype) : prototype_(prototype) <> Monster* spawnMonster() < return prototype_->clone(); > private: Monster* prototype_; >;
Внутри себя он содержит монстра, скрытого извне, который используется спаунером в качестве шаблона для штамповки новых монстров ему подобных. Получается нечто наподобие матки пчел, никогда не покидающей своего улья.
Для создания спаунера призраков, мы просто создаем прототипируемый экземпляр призрака и затем создаем спаунер, который будет хранить этот прототип:
Monster* ghostPrototype = new Ghost(15, 3); Spawner* ghostSpawner = new Spawner(ghostPrototype);
Интересна одна особенность этого шаблона заключается в том, что он не просто клонирует класс прототипа, но и клонирует его состояние. Это значит, что мы можем сделать спаунер для быстрых призраков, для слабых, для медленных, просто создавая соответствующего прототипируемого призрака.
На мой взгляд этот шаблон одновременно и элегантен и удивителен. Я не могу представить, чтобы дошел до него своим умом, но теперь я просто не могу себе представить, что я мог бы о нем не знать.
Насколько хорошо он работает?
Итак, нам не нужно создавать отдельный класс спаунер для каждого монстра и это хорошо. Но при этом нам нужно реализовывать метод clone() в каждом классе монстров. Кода там примерно столько же сколько и в спаунере.
К сожалению, если вы попытаетесь написать корректную реализацию clone() , вы быстро наткнетесь на несколько подводных камней. Должен это быть глубокий клон или приблизительный? Другими словами, если демон держит вилы, должен ли клонированный демон тоже держать вилы?
Это не просто выглядит как надуманная проблема, это действительно надуманная проблема. Нужно принять как должное то, что у нас есть отдельные классы для каждого монстра. В наше время так игровые движки писать не принято.
Большинство из нас не раз убеждались на собственном опыте, что поддержка такой организации иерархии классов крайне болезненна, поэтому вместо этого для моделирования различных сущностей без отведения под каждую отдельного класса мы используем шаблоны наподобие Компонент (Component) или Тип объекта (Type Object).
Функции спаунера
Даже если у нас для каждого типа монстра имеется свой класс, есть другой способ «поймать кота». Вместо того, чтобы делать отдельный класс спаунер для каждого монстра, можно организовать функцию спаунер:
Monster* spawnGhost() < return new Ghost(); >
Это уже не настолько примитивный подход, как создание отдельного класса для каждого нового типа монстров. Теперь единственный класс-спаунер может просто хранить указатель на функцию:
typedef Monster* (*SpawnCallback)(); class Spawner < public: Spawner(SpawnCallback spawn) : spawn_(spawn) <> Monster* spawnMonster() < return spawn_(); > private: SpawnCallback spawn_; >;
И для создания спаунера призраков нужно будет всего лишь вызвать:
Spawner* ghostSpawner = new Spawner(spawnGhost);
Шаблоны (Templates)
Сейчас большинство C++ разработчиков знакомы с концепцией шаблонов. Нашему классу спаунеру нужно создать экземпляр определенного класса, но мы не хотим жестко прописывать в коде определенный класс монстра. Естественным решением этой задачи будет воспользоваться возможностями шаблонов и добавить параметр типа:
class Spawner < public: virtual ~Spawner() <> virtual Monster* spawnMonster() = 0; >; template class T> class SpawnerFor : public Spawner < public: virtual Monster* spawnMonster() < return new T(); > >;
Я не могу утверждать, что программисты C++ научились их любить или что некоторых они настолько пугают, что люди просто отказываются от C++ . В любом случае, все кто сегодня использует C++ , используют и шаблоны тоже.
Применение выглядит следующим образом:
Spawner* ghostSpawner = new SpawnerFor();
Класс Spawner в данном коде не интересуется, какой тип монстра он будет создавать. Он просто работает с указателем на Monster .
Если бы у нас был только класс SpawnerFor , у нас не было бы ни одного экземпляра супертипа, разделяемого между шаблонами так что любому коду, работающему со спаунерами разных типов монстров, тоже пришлось бы принимать в качестве параметров шаблоны.
Класс первого типа
Предыдущие два решения требовали от нас иметь класс Spawner , параметризируемый типом. В C++ классы в общем не являются объектами первого класса, так что это требует некоторых усилий. А вот если вы используете язык с динамическими типами наподобие JavaScript , Python или Ruby , где классы — это просто обычные объекты, которые можно как угодно передать, задача решается гораздо проще.
В некотором роде шаблон Объект тип (Type Object) — это очередной способ обхода проблемы отсутствия класса первого типа. В языке с таким типом он тоже может быт полезен, потому что позволяет вам самостоятельно определять что такое «тип». Вам может пригодится семантика отличная от той, что предоставляют встроенные классы.
Если вам нужно соорудить спаунер — просто передайте ему класс монстра, которых он должен клонировать, т.е. по сути обычный объект, представляющий класс монстра. Проще пареной репы.
Имея столько возможностей, я не могу припомнить случай, в котором паттерн проектирования Прототип был бы лучшим вариантом. Может ваш опыт немного отличается от моего, но давайте лучше перейдем к следующей теме: прототипу как языковой парадигме.
Прототип, как языковая парадигма
Многие думают, что «объектно-ориентированное программирование» — это синоним слова «классы». Определения ООП напоминают кредо совершенно противоположных религий. Единственным бесспорным фактом является признание того факта, что ООП позволяет вам определять «объект», объединяющий данные и код в единое целое. По сравнению со структурированными языками наподобие C и функциональными языками типа Scheme , ключевой особенностью ООП является способность связки состояния и поведения.
Вам может показаться что единственным способом это осуществить является использование классов, но некоторые люди, включая Дейва Унгара и Ренделла Смита думают иначе. Еще в 80-е они создали язык Self . Несмотря на то, что это ООП язык, классов в нем нет.
Self
На самом деле Self даже более объектно-ориентированный, чем языки с классами. Под ООП мы подразумеваем неразлучность состояния и поведения, а в языках с классами между ними на самом деле есть большое разделение.
Вспомните семантику своего любимого языка с классами. Чтобы получить доступ к состоянию объекта, вы ищете в памяти его экземпляр. Состояние содержится в экземпляре.
Для вызова метода вы сначала ищете класс экземпляра и затем ищете метод в нем. Поведение содержится в классе. Всегда присутствует этот уровень косвенности для доступа к методу, отделяющий поля от методов.
Например, чтобы вызвать виртуальный метод в C++ , вы ищете его через указатель на экземпляр в виртуальной таблице и затем уже ищете в нем метод.
Self убирает это различие. Чтобы найти что-угодно, вы просто ищете это в объекте. Экземпляр может хранить как состояние так и поведение. Вы можете иметь отдельный объект с совершенно уникальным для него методом.
Никто из людей не остров, кроме этого объекта (No man is an island, but this object is. Отсылка к сериалу Девочки Гилмор)
Если бы это было все, что делает Self , пользоваться им было бы довольно сложно. Наследование в языках с классами, несмотря на свои недостатки, дает вам удобный механизм для полиморфного повторного использования кода и избегания дублирования. Для получения подобных результатов в Self есть делегирование.
Чтобы получить доступ к полю или вызвать метод определенного объекта, мы сначала должны получить доступ к самому объекту. Если получилось — дальше все просто. Если нет — мы ищем родителя объекта. Это просто ссылка на другой объект. Если не удалось найти свойство у самого объекта, мы попробуем его родителя, и родителя родителя и т.д. Другими словами, неудавшийся поиск делегируется родителю объекта.
Здесь допущено небольшое упрощение. Self помимо всего прочего поддерживает еще и несколько родительских объектов. Родители — это всего лишь специальным образом помеченные поля, дающие вам возможность использовать штуки типа наследования родителей или изменять их во время работы. Такой подход называется динамическим наследованием (dynamic inheritance).
Родительский объект дает нам возможность повторно использовать поведение (и состояние!) между несколькими объектами, так что мы уже перекрыли некоторую функциональность классов. Еще одна ключевая особенность классов заключается в том, что они позволяют нам создавать экземпляры классов. Когда вам нужен новый ThingamaBob , вы просто пишете new Thingamabob() ну или нечто подобное, если используете другой язык. Класс — это фабрика экземпляров самого себя.
Как можно создать нечто без класса? А как мы на самом деле делаем обычно новые вещи? Также, как и в рассмотренном нами шаблоне проектирования, Self делает это с помощью клонирования.
В Self каждый из объектов поддерживает шаблон проектирования Прототип автоматически. Любой объект можно клонировать. Чтобы наделать кучу одинаковых объектов нужно просто:
- Привести один из объектов в нужное вас состояние. Можно просто взять за основу встроенный в систему базовый объект Object и дополнить его нужными полями и методами.
- Клонировать его и получить столько. э-э. клонов, сколько вам нужно.
Таким образом, мы получаем элегантность шаблона Прототип, но без необходимости писать реализацию clone() для каждого класса самостоятельно. Он просто встроен в систему.
Это настолько прекрасная, разумная и минималистская система, что как только я узнал об этой парадигме, я сразу принялся за написание языка на основе прототипов, просто чтобы разобраться в парадигме получше.
Я пришел к выводу, что написание языка с нуля — не лучший способ что-либо выучить, но это одна из моих странностей. Если вам любопытно, язык называется Finch .
И как оно?
Играться с языком на базе прототипов было замечательно, но как только мой собственный язык заработал, я обнаружил один малоутешительный факт: программировать на нем было не особо весело.
С тех пор я часто слышу что многие программисты на Self приходят к тому же выводу. Впрочем это не означает, что проект был совсем провальный. Self был настолько динамичен, что для того, чтобы работать с нормальной скоростью, ему реально необходимы все современные инновации в области виртуализации.
Изобретенные ими идеи относительно компиляции на ходу, сборщика мусора и оптимизации вызова методов — это именно те технологии, которые сделали (зачастую усилиями тех же самых людей) многие современные языки с динамическими типами достаточно быстрыми для того, чтобы писать на них популярные приложения.
Конечно язык был простым для реализации, но только потому, что я переложил всю сложность на плечи пользователя. Как только я начал пробовать им пользоваться, я обнаружил, что мне очень не хватает структурированности, которую дают классы. Я закончил тем, что стал пытаться компенсировать их отсутствие в самом языке написанием специальной библиотеки.
Возможно, все дело в том, что я слишком привык пользоваться языками с классами и мой мозг слишком привык к этой парадигме. Но у меня есть большое подозрение, что многим людям такой «порядок вещей» нравится.
И в продолжение истории ошеломительного успеха языков на основе классов. Посмотрите как много игр страдают от избытка классов персонажей, полного перечня различных типов врагов, предметов, навыков, каждый из которых старательно подписан. Не думаю, что вы найдете много игр, где каждый монстр представляет собой уникальную снежинку в духе «нечто среднее между троллем и гоблином и небольшой примесью змея».
Несмотря на то, что прототипы — это действительно очень мощная парадигма, и я хочу, чтобы об этом узнало как можно больше людей, я рад, что большинство из нас все таки не использует ее в повседневной работе. Потому что тот код с реализаций прототипов, что я видел, представлял из себя настолько ужасное месиво, что я так и не смог его понять.
Также это говорит о том, что на самом деле существует очень мало кода, написанного в стиле прототипирования. Я смотрел.
А что насчет JavaScript?
Ну хорошо, если языки на основе прототипов настолько недружественны, то как я могу объяснить существование JavaScript ? Ведь это язык с прототипами, которым ежедневно пользуются миллионы людей. Код JavaScript выполняет больше компьютеров, чем код на любом другом языке в мире.
Брендан Эйх, создатель JavaScript , черпал вдохновение прямиком из Self и поэтому большая часть семантики JavaScript основана на прототипах. Каждый объект может иметь произвольный набор свойств, которые в свою очередь могут быть как полями, так и «методами» (которые на самом деле просто функции, хранящиеся в виде полей). У каждого объекта может быть другой объект, называемый его «прототипом», к которому происходит делегирование если нужное поле не найдено.
Для разработчика языка привлекательной особенностью прототипов является то, что реализовывать их легче, чем классы. Эйх тоже этим пользовался: первая версия JavaScript была написана всего за десять дней.
И все-таки, несмотря на все это, я считаю что на практике у JavaScript гораздо больше общего именно с языками на основе классов, чем с основанными на прототипах. Это заметно уже хотя бы потому, что в JavaScript предпринято большое отступление от Self — ключевой операции любого языка на основе прототипов — клонирования — нигде не видно. В JavaScript не существует метода для клонирования объекта.
Самая близкая по смыслу операция из существующих — это Object.create() , позволяющая вам создать новый объект, делегирующий к уже существующему. И даже эта возможность появилась только в спецификации ECMAScript 5 , через четырнадцать лет после выхода JavaScript . Давайте я покажу, как обычно определяют типы и создают объекты в JavaScript вместо клонирования. Начинается все с функции конструктора (constructor function):
function Weapon(range, damage) < this.range = range; this.damage = damage; >
С ее помощью создается новый объект и инициализируются его поля. Вызов выглядит следующим образом:
var sword = new Weapon(10, 16);
В этом коде new вызывает тело функции Weapon() , внутри которой this связано с новым пустым объектом. Внутри тела функции к объекту добавляется куча полей, а потом новосозданный объект автоматически возвращается.
Оператор new делает за вас еще одну вещь. Когда он создает чистый объект, он сразу делает его делегатом объекта-прототипа. Доступ к объекту прототипу можно получить через Weapon.prototype .
Так как состояние добавляется в теле конструктора, для определения поведения вы обычно добавляете методы к прототипу объекта. Примерно таким образом:
Weapon.prototype.attack = function(target) < if (distanceTo(target) > this.range) < console.log("Out of range!"); > else < target.health -= this.damage; > >
Здесь мы добавляем прототипу оружия свойство attack , значением которого будет функция. И так как каждый объект, возвращаемый new Weapon() , делегируется к Weapon.prototype , вы можете теперь сделать вызов sword.attack() и он вызовет нужную нам функцию. Выглядит это примерно так:
Давайте еще раз:
- Новые объекты вы создаете с помощью операнда new , который вы вызываете используя объект, представляющий собой тип — функцию-конструктор.
- Состояние хранится в самом экземпляре.
- Поведение задается через уровень косвенности — делегирование к прототипу и хранится в виде отдельного объекта, представляющего собой набор методов, разделяемый между всеми объектами данного типа.
Вы можете назвать меня психом, но это крайне похоже на мое определение классов, которое я привел выше. Вы имеете возможность писать код в стиле прототипов в JavaScript (без клонирования), но синтаксис и идиоматика языка предполагают подход, основанный на классах.
Я лично считаю, что это хорошо. Как я уже сказал, я убедился на собственном опыте, что прототипы усложняют работу с кодом, так что мне нравится то, как JavaScript оборачивает свое ядро в более похожую на классы форму.
Прототипы для моделирования данных
Итак, я продолжаю перечислять вещи, за которые я не люблю прототипы. Депрессивная глава получается. Я задумывал эту книгу скорее как комедию, а не как трагедию, так что покончим с этим и перейдем к областям, где на мой взгляд прототипы или, говоря конкретнее, делегирование может быть полезным.
Если вы посчитаете все байты в игре, приходящиеся на код и сравните с объемом остальных данных, вы увидите, что с момента появления игр, доля данных постоянно увеличивается. Ранние игры практически все генерировали процедурно и, как следствие, могли поместиться на дискетку или картридж. В большинстве современных игр код — это всего лишь «движок», который позволяет игре работать, а сама игра полностью определена в данных.
Это конечно здорово, но перемещение контента в файлы данных вовсе не означает, что мы избавляемся от организационных сложностей большого проекта. Скорее наоборот, усложняем себе жизнь. Одной из причин почему мы используем языки программирования является то, что они предоставляют нам инструменты по снижению сложности.
Вместо того, чтобы копировать и вставлять кусок кода в десяти местах, мы помещаем его в отдельную функцию и вызываем ее по имени. Вместо того, чтобы копировать метод в кучу классов, мы просто помещаем его в отдельный класс, а остальные классы от него наследуем.
Когда объем данных в игре достигает некоторого предела, вам сразу начинает хотеться обладать подобными возможностями. Моделирование данных — это слишком большая область, чтобы обсуждать ее на поверхностном уровне, но я хочу показать вам одну из возможностей, которая пригодится вам в вашей игре: использование прототипов и делегирование для повторного использования данных.
Давайте представим себе, что мы определяем модель данных для бессовестного клона Gauntlet , о котором я писал выше. Геймдизайнеру нужны какие-то файлы, в которые он сможет поместить описание атрибутов монстров и предметов.
Я имею в виду полностью оригинальную игру, никоим образом не напоминающую хорошо известную ранее многопользовательскую аркадную игру с видом сверху. Так что не подавайте на меня в суд пожалуйста.
Можно использовать JSON : сущности данных будут представлены в виде maps или мешков со свойствами (property bags) или еще дюжиной терминов, потому что программисты просто обожают придумывать для одного и того же разные имена.
Мы так часто их переизобретаем, что Стив Йегге решил назвать их Универсальным шаблоном проектирования (“The Universal Design Pattern”).
Итак, гоблин в игре описан следующим образом:
< "name": "goblin grunt", "minHealth": 20, "maxHealth": 30, "resists": ["cold", "poison"], "weaknesses": ["fire", "light"] >
Довольно прямолинейный подход и даже не любящие писать дизайнеры могут справиться. Можно, например, добавить еще парочку сестринских описаний в славном семейном дереве зеленых гоблинов:
< "name": "goblin wizard", "minHealth": 20, "maxHealth": 30, "resists": ["cold", "poison"], "weaknesses": ["fire", "light"], "spells": ["fire ball", "lightning bolt"] >
< "name": "goblin archer", "minHealth": 20, "maxHealth": 30, "resists": ["cold", "poison"], "weaknesses": ["fire", "light"], "attacks": ["short bow"] >
Если бы это был обычный код, наше чувство прекрасного уже заставило бы нас беспокоиться. У этих сущностей слишком много общей дублирующейся информации, а хорошо натренированные программисты это просто ненавидят. Данные занимают слишком много места и требуют слишком много времени на написание. Даже для того, чтобы выяснить одинаковые ли это данные, вам нужно тщательно их прочитать. Их поддержка — настоящая головная боль. Если мы захотим сделать всех гоблинов в игре сильнее, нам нужно будет не забыть обновить значение здоровья для них всех. Плохо, плохо, плохо.
Если бы это был код, мы могли бы создать абстракцию «гоблин» и использовать ее между всему типами гоблинов. Но тупой JSON ничего об этом не знает. Давайте попробуем сделать его чуточку умнее.
Определим для каждого объекта поле » prototype » и поместим туда имя объекта, к которому он делегирует. Любые свойства, отсутствующие у первого объекта нужно будет смотреть в прототипе.
Это позволит нам упростить описание нашей оравы гоблинов:
Таким образом » prototype » переходит из разряда обычных данных в метаданные. У каждого гоблина есть бородавчатая кожа и желтые зубы. У него нет прототипа. Прототип — это свойство объекта данных, описывающего гоблина, а не самого гоблина.
< "name": "goblin grunt", "minHealth": 20, "maxHealth": 30, "resists": ["cold", "poison"], "weaknesses": ["fire", "light"] >
< "name": "goblin wizard", "prototype": "goblin grunt", "spells": ["fire ball", "lightning bolt"] >
< "name": "goblin archer", "prototype": "goblin grunt", "attacks": ["short bow"] >
Так как и лучник, и чародей имеют в качестве прототипа пехотинца, нам не нужно указывать заново здоровье, сопротивляемости и уязвимости для каждого из них. Добавленная нами в данные логика предельно проста — мы просто добавили простейшее делегирование и сразу смогли избавиться от кучи повторов.
Хочу обратить ваше внимание на то, что мы не стали добавлять четвертого «базового гоблина» в качестве абстрактного прототипа, к которому будут делегировать остальные три. Вместо этого, мы просто взяли одного из гоблинов, который является простейшим и делегируем к нему.
Такой подход является естественным для систем на основе прототипов, где каждый объект можно использовать для клонирования нового объекта с уточненными свойствами и смотрится натуральным и здесь. Применительно к игровым данным такой подход тоже удобен, потому что здесь часто приходится создавать объекты, лишь немного отличающиеся от остальных.
Подумайте о боссах и уникальных предметах. Очень часто они являются лишь немного измененной версией обыкновенных игровых объектов и прототипирование с делегированием очень хорошо подходит для их описания. Магический Sword of Head-Detaching (Меч-голова с плеч) можно описать как длинный меч с определенными бонусами:
< "name": "Sword of Head-Detaching", "prototype": "longsword", "damageBonus": "20" >
Такие дополнительные возможности для описания данных могут облегчить жизнь вашим дизайнерам и добавить больше вариативности предметам и популяции монстров в игре, а это именно то, что может понравиться игрокам.