Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров4K
Автор оригинала: Riccardo Canella


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

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

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

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

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

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

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

Благодарю за внимание.
Теги:
Хабы:
Всего голосов 8: ↑5 и ↓3+5
Комментарии3

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань