
Доброго времени суток, друзья!
Структурные шаблоны проектирования используются для построения больших систем отношений между объектами с целью сохранения гибкости и эффективности. Давайте рассмотрим некоторые из них с отсылками на Игру престолов.
В разработке программного обеспечения шаблоны проектирования используются для решения наиболее распространенных проблем. Они представляют собой лучшие практики, выработанные за долгое время разработчиками в процессе тестирования приложений и устранения ошибок.
В этой статье мы поговорим о структурных шаблонах. Они предназначены для проектирования приложений через определение простого способа взаимодействия экземпляров.
Наиболее распространенными являются следующие шаблоны:
- Адаптер (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.
Прим. пер.: вот отличное видео, посвященное шаблонам проектирования.
Благодарю за внимание.
