Шаблоны проектирования — способы решения наиболее часто встречающихся при разработке программного обеспечения проблем. В этой статье мы рассмотрим порождающие шаблоны с отсылками на Игру престолов.
О структурных паттернах читайте здесь.
Порождающие паттерны предназначены для работы с механизмами построения объектов с целью создания объекта наиболее подходящим в данной ситуации способом.
Самыми распространенными порождающими паттернами являются следующие:
- Фабрика (фабричный) (Fabric)
- Абстракция (абстрактный) (Abstract)
- Конструктор (Constructor, Builder)
- Прототип (прототипный) (Prototype)
- Синглтон (Singleton)
Фабрика
Фабрика — это паттерн, использующий так называемые фабричные методы для создания объектов без необходимости определения класса создаваемого объекта. Что это означает?
Представим, что хотим иметь возможность создавать солдат. Эти солдаты могут быть либо из дома Таргариенов, либо из дома Ланнистеров.
Для этого нам нужны:
- Интерфейс для определения солдат
- Класс для каждого типа солдат для расширения возможностей каждого дома
- Класс для запроса создания нового солдата независимо от дома, которому он принадлежит
class Soldier {
constructor(name) {
this.name = name
this.attack = this.attack.bind(this)
}
attack() {}
}
class LannisterSoldier extends Soldier {
attack() {
return 'Lannister always pays his debts'
}
}
class TargaryenSoldier extends Soldier {
attack() {
return 'Fire and blond'
}
}
class Spawn {
constructor(type, name) {
if (type === 'lannister') return new LannisterSoldier(name)
else return new TargaryenSoldier(name)
}
}
(() => {
const lannister = new Spawn('lannister', 'soldier1')
const targaryen = new Spawn('targaryen', 'soldier2')
console.log(lannister.attack())
console.log(targaryen.attack())
})()
Когда используется?
- Когда ожидается, что реализация интерфейса или абстрактного класса скоро изменится
- Когда существующая реализация не способна быстро адаптироваться к изменениям
- Когда процесс инициализации является простым, а конструктор принимает незначительное количество параметров
Абстракция
Дисклеймер: абстрактный шаблон основан на объектах. Его крайне сложно использовать в функциональном программировании.
Рассматриваемый шаблон позволяет инкапсулировать группу отдельных фабрик, выполняющих аналогичные задачи, без необходимости определения конкретных классов. При стандартном использовании клиентское ПО создает определенную реализацию абстрактной фабрики и затем использует общий интерфейс фабрики для создания определенных объектов как части единой системы. Клиент не знает (или для него не имеет значения), какие конкретно объекты он получает из каждой внутренней фабрики, поскольку для этого используется общий интерфейс.
Представим, что мы хотим управлять каждым домом по-отдельности. У каждого дома должна быть возможность определять наследника престола и его возлюбленную.
Для этого нам нужны:
- Интерфейс для определения наследника
- Класс для каждого наследника
- Интерфейс для определения возлюбленной
- Класс для каждой возлюбленной
- Интерфейс для определения дома
- Класс для каждого дома, возвращающий экземпляры наследника и возлюбленной
// определяем наследника
class HeirToTheThrone {
conctructor(name, isActualOnTheThrone) {
this.name = name
this.isActualOnTheThrone = isActualOnTheThrone
this.getTheThrone = this.getTheThrone.bind(this)
}
getTheThrone() {}
}
class HeirToTheThroneLannister extends HeirToTheThrone {
getTheThrone(){
console.log('kill all')
}
}
class HeirToTheThroneTargaryen extends HeirToTheThrone {
getTheThrone() {
console.log('burn all')
}
}
// определяем возлюбленную
class Subject {
constructor(name) {
this.name = name
this.speak = this.speak.bind(this)
}
speak() {}
}
class SubjectLannister extends Subject {
speak(){
console.log('i love Cersei')
}
}
class SubjectTargaryen extends Subject {
speak(){
console.log('i love Daenerys')
}
}
// определяем дом
class House {
constructor() {
this.getHeir = this.getHeir.bind(this)
this.getSubject = this.getSubject.bind(this)
}
getHeir(){}
getSubject(){}
}
class Lannister extends House {
getHeir() {
return new HeirToTheThroneLannister('Cersei', true)
}
getSubject(name) {
return new SubjectLannister(name)
}
}
class Targaryen extends House {
getHeir() {
return new HeirToTheThroneTargaryen('Daenerys', true)
}
getSubject(name) {
return new SubjectTargaryen(name)
}
}
(()=>{
const lannister = new Lannister()
const targaryen = new Targaryen()
lannister.getHeir().getTheThrone()
lannister.getSubject().speak()
targaryen.getHeir().getTheThrone()
targaryen.getSubject().speak()
})()
Когда используется?
- Когда клиент не зависит от того, как мы создаем и располагаем объекты относительно друг друга в системе
- Когда система состоит из нескольких групп объектов, и эти группы должны работать сообща
- Когда нам необходима возможность быстрого определения зависимости
Конструктор
Целью конструктора является отделение сложного объекта от его представлений. При усложнении объекта данный шаблон позволяет отделить процесс создания нового объекта посредством другого объекта (конструктора).
Представим, что мы хотим создать флот для каждого дома. У каждой семьи будет определенное количество кораблей и воинов. Для облегчения создания у нас должна быть возможность вызывать метод «makeNavy», который будет автоматически создавать все, что нам нужно через настройки.
Для этого нам нужны:
- Класс для создания флота
- Интерфейс для определения конструктора
- Класс, отвечающий за создание объектов нашей армии
- Классы, отвечающие за создание солдат и кораблей
class Lannister {
constructor() {
this.soldiers = []
this.ships = []
this.makeNavy = this.makeNavy.bind(this)
}
makeNavy(soldiers, ships) {
const Build = new ConcreteBuilder()
for (let i = 0; i < soldiers; i++) {
this.soldiers.push(Build.createSoldier())
}
for (let i = 0; i < ships; i++) {
this.ships.push(Build.createShip())
}
}
}
class Builder {
createSoldier() {}
createShip() {}
}
class ConcreteBuilder extends Builder {
createSoldier() {
const soldier = new Soldier()
return soldier
}
createShip() {
const ship = new Ship()
return ship
}
}
class Soldier {
constructor() {
console.log('soldier created')
}
}
class Ship {
constructor() {
console.log('ship created')
}
}
(() => {
const lannister = new Lannister()
lannister.makeNavy(100, 10)
})()
Когда используется?
- Когда процесс создания объекта является очень сложным, предполагает большое количество обязательных и опциональных параметров
- Когда рост количества параметров конструктора приводит к увеличению количества конструкторов
- Когда клиент ожидает различных представлений для конструируемого объекта
Прототип
Прототип позволяет определять виды создаваемых объектов через прототипное наследование и создавать новые объекты с помощью схемы существующего объекта. Это повышает производительность и сводит потери памяти к минимуму.
Представим, что мы хотим создать армию белых ходоков. У нас нет особых требований к этой армии. Мы просто хотим, чтобы их было много, чтобы у них были одинаковые характеристики и чтобы они не занимали много места в памяти.
Для этого нам нужны:
- Класс для хранения информации о белых ходоках
- Метод для клонирования экземпляра, возвращающий такой же метод
class WhiteWalker {
constructor(force, weight) {
this.force = force
this.weight = weight
}
clone() {
console.log('cloned')
return new WhiteWalker(this.name, this.weight)
}
}
(()=>{
const firstWalker = new WhiteWalker()
const walkers = [firstWalker]
for(let i=0;i<100;i++){
walkers.push(firstWalker.clone())
}
})()
Преимущества и недостатки прототипа
Плюсы:
- Помогает экономить стоимость, время и производительность за счет отсутствия необходимости использования нового оператора для создания новых объектов
- Снижает сложность инициализации объекта: каждый класс использует собственный способ клонирования
- Отсутствует необходимость классификации и создания множества подклассов для инициализации объектов как при использовании абстрактного шаблона
- Увеличивает гибкость системы за счет создания новых объектов посредством изменения некоторых свойств копируемого объекта
Минусы:
- Клонирование сложных объектов, имеющих циклические ссылки, является нетривиальной задачей
Синглтон
Синглтон позволяет убедиться в том, что создаваемый объект является единственным экземпляром определенного класса. Такой объект обычно используется для управления несколькими операциями в системе.
Представим, что мы хотим быть уверены в наличии единственной Матери драконов.
Для этого нам нужны:
- Класс для хранения информации о Матери драконов
- Сохраненный экземпляр Матери драконов
let INSTANCE = null
class MotherOfDragons {
constructor(name, dragons) {
if(!!INSTANCE) return INSTANCE
this.dragons = dragons
this.name = name
this.getMyDragons = this.getMyDragons.bind(this)
INSTANCE = this
}
getMyDragons(){
console.log(`I'm ${this.name} and my dragons are`, this.dragons)
}
}
(()=>{
const dragonMother = new MotherOfDragons('Daenerys Targaryen', [
'Drogon',
'Rhaegal',
'Viserion'
])
const dragonMother2 = new MotherOfDragons('Cercei Targaryen', [
'Tirion',
'Jennifer',
'Goblin'
])
dragonMother.getMyDragons()
dragonMother2.getMyDragons()
console.log(dragonMother instanceof MotherOfDragons)
console.log(dragonMother2 instanceof MotherOfDragons)
console.log(dragonMother2 === dragonMother)
})()
Когда используется?
- Когда ограничены ресурсы, используемые для создания объекта (например, объекты подключения к базе данных)
- Хорошей практикой является построение авторизации с помощью синглтона с целью повышения производительности
- Когда необходимо создать класс для настройки приложения
- Когда необходимо создать класс для распределения ресурсов
Код на Github.
Прим. пер.: вот отличное видео, посвященное шаблонам проектирования.
Благодарю за внимание.
Justerest
По-моему, на Рефакторинг Гуру объясняется намного лучше. Если хочется ещё подробней, то обращайтесь к Банде Четырех.
Автор статьи-оригинала использует вредные реализации, противоречащие приведенному определению паттернов. Например, фабрика — должна отвязывать клиента от конкретных классов с помощью фабричного метода… И в реализации используется конкретный класс, который возвращает другой тип из конструктора!