Структурные шаблоны проектирования в ES6+ на примере Игры престолов

от автора

Доброго времени суток, друзья!

Структурные шаблоны проектирования используются для построения больших систем отношений между объектами с целью сохранения гибкости и эффективности. Давайте рассмотрим некоторые из них с отсылками на Игру престолов.

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

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

Наиболее распространенными являются следующие шаблоны:

  • Адаптер (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`) })() 

Случаи использования

  • Когда объект, который мы хотим использовать, находится далеко (глубоко вложен) и сохранить логику в прокси, чтобы не влиять на клиента
  • Когда мы хотим выдавать приблизительный результат в ожидании реального результата, вычисление которого занимает много времени
  • Когда мы хотим контролировать доступ или создание объекта без вмешательства в его логику

Прим. пер.: вот отличное видео, посвященное шаблонам проектирования.

Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/496148/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *