Доброго времени суток, друзья!
Структурные шаблоны проектирования используются для построения больших систем отношений между объектами с целью сохранения гибкости и эффективности. Давайте рассмотрим некоторые из них с отсылками на Игру престолов.
В разработке программного обеспечения шаблоны проектирования используются для решения наиболее распространенных проблем. Они представляют собой лучшие практики, выработанные за долгое время разработчиками в процессе тестирования приложений и устранения ошибок.
В этой статье мы поговорим о структурных шаблонах. Они предназначены для проектирования приложений через определение простого способа взаимодействия экземпляров.
Наиболее распространенными являются следующие шаблоны:
- Адаптер (Adapter)
- Декоратор (Decorator)
- Фасад (Facade)
- Приспособленец (легковесный (элемент), Flyweight)
- Прокси (Proxy)
Адаптер
Адаптер — это шаблон, позволяющий транслировать (передавать) интерфейс одного класса другому. Он позволяет классам работать вместе, что часто бывает невозможным по причине несовместимости структур.
Представим, что Таргариены решили воевать всеми силами, имеющимися в их распоряжении (Безупречные и Драконы), а Дейнерис ищет способ взаимодействия с каждым из них.
Нам потребуется следующее:
- класс Unsullied
- класс Dragon
- класс Adapter для передачи метода burn класса Dragon в общий метод kill
class Unsullied {
constructor(name) {
this.name = name
this.kill = this.kill.bind(this)
}
kill() {
console.log(`Unsullied ${this.name} kill`)
}
}
class Dragon {
constructor(name) {
this.name = name
this.burn = this.burn.bind(this)
}
burn() {
console.log(`Dragon ${this.name} burn`)
}
}
class DragonAdapter extends Dragon {
kill() {
this.burn()
}
}
(() => {
const Army = [
new DragonAdapter('Rhaegal'),
new Unsullied('Grey worm'),
new DragonAdapter('Drogon')
]
Army.forEach(soldier => soldier.kill())
})()
Случаи использования адаптера
- Когда новые компоненты или модули должны работать с существующими в приложении или когда часть кода была улучшена в результате рефакторинга, но должна взаимодействовать со старыми частями.
- Когда вы нашли библиотеку для решения второстепенных задач и хотите интегрировать ее в свое приложение.
Декоратор
Декоратор — это шаблон, предназначенный для динамичного добавления поведения или функциональности в существующие классы. Это альтернатива подклассификации.
Представим, что мы хотим проверять не отравлен ли напиток, подаваемый королю Джоффри.
Нам потребуется следующее:
- класс King
- экземпляр King Joffrey
- класс Drink
- класс Poisoned Drink
- функция isNotPoisoned для сохранения жизни короля
function isNotPoisoned(t, n, descriptor) {
const original = descriptor.value
if(typeof original === 'function') {
descriptor.value = function(...args) {
const [drink] = args
if(drink.constructor.name === 'poisonedDrink') throw new Error('Someone wants to kill the king')
return original.apply(this, args)
}
}
return descriptor
}
class PoisonedDrink {
constructor(name) {
this.name = name
}
}
class Drink {
constructor(name) {
this.name = name
}
}
class King {
constructor(name) {
this.name = name
}
// здесь используется декоратор метода класса
// попытки транспиляции к успеху не привели
@isNotPoisoned
drink(drink) {
console.log(`The king drank ${drink}`)
}
}
(() => {
const joffrey = new King('Joffrey Baratheon')
const normalDrink = new Drink('water')
const poisonedDrink = new Drink('poisoned water')
joffrey.drink(normalDrink)
joffrey.drink(poisonedDrink)
})()
Случаи использования декоратора
- Когда мы хотим добавить функцию в большое количество классов или методов с разными контекстами
- Когда мы хотим улучшить ранее созданный класс, но не имеем времени на полный рефакторинг
Фасад
Фасад — широко используемый в библиотеках шаблон. Он применяется для обеспечения унифицированного и простого интерфейса и скрытия сложности составляющих его подсистем или подклассов.
Представим, что мы хотим управлять армией в битве с варварами. Экземпляры нашей армии имеют методы для передвижения конницы, солдат и гигантов. Но мы вынуждены вызывать эти методы по отдельности, что требует много времени. Как мы можем облегчить управление армией?
Нам потребуется следующее:
- экземпляры армии
- класс ArmyFacade
class Horse {
constructor(name) {
this.name = name
}
attack() {
console.log(`Infantry ${this.name} attack`)
}
}
class Soldier {
constructor(name) {
this.name = name
}
attack() {
console.log(`Soldier ${this.name} attack`)
}
}
class Giant {
constructor(name) {
this.name = name
}
attack() {
console.log(`Giant ${this.name} attack`)
}
}
class ArmyFacade {
constructor() {
// обратите внимание, в данном случае точки с запятой в конце имеют принципиальное значение
this.army = [];
(new Array(10)).fill().forEach((_, i) => this.army.push(new Horse(i + 1)));
(new Array(10)).fill().forEach((_, i) => this.army.push(new Soldier(i + 1)));
(new Array(1)).fill().forEach((_, i) => this.army.push(new Giant(i + 1)));
this.getByType = this.getByType.bind(this)
}
getByType(type, occurency) {
return this.army.filter(el => {
return el.constructor.name === type && occurency-- > 0
})
}
attack(armyInfo = {}) {
const keys = Object.keys(armyInfo)
let subArmy = []
keys.forEach(soldier => {
switch(soldier) {
case 'horse': {
subArmy = [...subArmy, ...this.getByType('Horse', armyInfo.horse)]
break;
}
case 'soldier': {
subArmy = [...subArmy, ...this.getByType('Soldier', armyInfo.soldier)]
break;
}
case 'giant': {
subArmy = [...subArmy, ...this.getByType('Giant', armyInfo.giant)]
break;
}
}
})
subArmy.forEach(soldier => soldier.attack())
}
}
(() => {
const army = new ArmyFacade()
army.attack({
horse: 3,
soldier: 5,
giant: 1
})
})()
Случаи использования
- Когда мы хотим преобразовать множество строчек кода, возможно, повторяющегося, в одну простую функцию.
Приспособленец
Приспособленец — шаблон, предназначенный для эффективной передачи данных между множеством мелких объектов. Он используется для повышения производительности и экономии памяти.
Допустим, мы хотим управлять армией белых ходоков. При этом, наши ходоки могут иметь три состояния:
- живой
- мертвый
- воскресший
Нам потребуется следующее:
- класс WhiteWalker
- класс WhiteWalkerFlyweight
- клиент для воскрешения белых ходоков
class WhiteWalker {
constructor({
sprite,
someOtherBigInformation
}) {
this.sprite = sprite
this.someOtherBigInformation = someOtherBigInformation
this.state = 'alive'
this.resurrect = this.resurrect.bind(this)
this.kill = this.kill.bind(this)
this.getState = this.getState.bind(this)
}
kill() {
this.state = 'dead'
}
resurrect() {
this.state = 'resurrected'
}
getState() {
return this.state
}
}
const whiteWalker = new WhiteWalker({
sprite: Date.now()
})
class WhiteWalkerFlyweight {
constructor(position, name) {
this.position = position
this.name = name
this.whiteWalker = whiteWalker
}
getInfo() {
console.log(`The White Walker ${this.name} whit sprite ${this.whiteWalker.sprite} is ${this.whiteWalker.state}`)
}
getFatherInstance() {
return this.whiteWalker
}
}
(() => {
const myArmy = []
for(let i = 0; i < 10; i++) {
myArmy.push(new WhiteWalkerFlyweight({
x: Math.floor(Math.random() * 200),
y: Math.floor(Math.random() * 200),
}, i + 1))
}
myArmy.forEach(soldier => soldier.getInfo())
console.log('KILL ALL')
const [onOffWhiteWalker] = myArmy
onOffWhiteWalker.getFatherInstance().kill()
myArmy.forEach(soldier => soldier.getInfo())
console.log('RESURRECT ALL')
onOffWhiteWalker.getFatherInstance().resurrect()
myArmy.forEach(soldier => soldier.getInfo())
})()
Случаи использования
- Когда мы хотим избежать создания большого количества объектов
- Когда мы хотим создать объекты, потребляющие большой объем памяти
- Когда нам нужны объекты, создание которых требует сложных вычислений
- Когда в нашем распоряжении имеется ограниченный запас ресурсов: вычислительной мощности, памяти, пространства и т.д.
Прокси
Шаблон прокси, как следует из его названия, используется как надстройка или заменитель для другого объекта для контроля за доступом к этому объекту.
Представим, что королева Серсея издала указ о запрете набора более 100 солдат без ее разрешения. Как нам это реализовать?
Нам потребуется следующее:
- класс Soldier
- класс ArmyProxy для контроля процесса
- экземпляры класса Cercei для получения разрешения
class Soldier {
constructor(name) {
this.name = name
}
attack() {
console.log(`Soldier ${this.name} attack`)
}
}
class Queen {
constructor(name) {
this.name = name
}
getConsent(casualNumber) {
return casualNumber %2 === 0
}
}
class ArmyProxy {
constructor() {
this.army = []
this.available = 0
this.queen = new Queen('Cercei')
this.getQueenConsent = this.getQueenConsent.bind(this)
}
getQueenConsent() {
return this.queen.getConsent(Math.floor(Math.random() * 200))
}
enrollSoldier() {
if(!this.available) {
const consent = this.getQueenConsent()
if(!consent) {
console.error(`The queen ${this.queen.name} deny the consent`)
return
}
this.available = 100
}
this.army.push(new Soldier(this.army.length))
this.available--
}
}
(() => {
const myArmy = new ArmyProxy()
for(let i = 0; i < 1000; i++) {
myArmy.enrollSoldier()
}
console.log(`I have ${myArmy.army.length} soldiers`)
})()
Случаи использования
- Когда объект, который мы хотим использовать, находится далеко (глубоко вложен) и сохранить логику в прокси, чтобы не влиять на клиента
- Когда мы хотим выдавать приблизительный результат в ожидании реального результата, вычисление которого занимает много времени
- Когда мы хотим контролировать доступ или создание объекта без вмешательства в его логику
Код на Github.
Прим. пер.: вот отличное видео, посвященное шаблонам проектирования.
Благодарю за внимание.