Padrões de design estrutural no ES6 + no exemplo de Game of Thrones



Bom dia amigos

Padrões de design estrutural são usados ​​para criar grandes sistemas de relacionamentos entre objetos, a fim de manter a flexibilidade e a eficiência. Vejamos alguns deles com referências ao Game of Thrones.

No desenvolvimento de software, os padrões de design são usados ​​para resolver os problemas mais comuns. Eles representam as melhores práticas desenvolvidas por um longo tempo pelos desenvolvedores no processo de teste de aplicativos e correção de bugs.

Neste artigo, falaremos sobre padrões estruturais. Eles foram projetados para projetar aplicativos, definindo uma maneira simples de interagir com instâncias.

Os mais comuns são os seguintes padrões:

  • Adaptador
  • Decorador
  • Fachada
  • Adaptável (peso leve (elemento), peso mosca)
  • Proxy

Adaptador


Um adaptador é um modelo que permite traduzir (transferir) a interface de uma classe para outra. Ele permite que as classes trabalhem juntas, o que geralmente é impossível devido a estruturas incompatíveis.

Imagine que os Targaryen decidiram lutar com todas as forças à sua disposição (Flawless e Dragons), e Daenerys está procurando uma maneira de interagir com cada uma delas.

Vamos precisar do seguinte:

  • Classe imaculada
  • Classe dragão
  • Classe do adaptador para passar o método de gravação da classe Dragon para o método comum de eliminação

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())
})()

Casos de uso


  • Quando novos componentes ou módulos devem funcionar com os existentes no aplicativo ou quando parte do código foi aprimorada como resultado da refatoração, mas devem interagir com as partes antigas.
  • Quando você encontrou uma biblioteca para resolver problemas secundários e deseja integrá-la ao seu aplicativo.

Decorador


Um decorador é um modelo projetado para adicionar dinamicamente comportamento ou funcionalidade às classes existentes. Essa é uma alternativa à subclassificação.

Suponha que queremos verificar se a bebida servida ao rei Joffrey está envenenada.

Vamos precisar do seguinte:

  • Classe King
  • Instância do rei Joffrey
  • Aula de bebida
  • classe Bebida Envenenada
  • Função isNotPoisoned para salvar a vida do rei

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)
})()

Casos de uso do decorador


  • Quando queremos adicionar uma função a um grande número de classes ou métodos com diferentes contextos
  • Quando queremos melhorar uma aula criada anteriormente, mas não temos tempo para uma refatoração completa

Fachada


Fachada - um modelo amplamente usado em bibliotecas. É usado para fornecer uma interface unificada e simples e ocultar a complexidade de seus subsistemas ou subclasses.

Imagine que queremos controlar um exército em uma batalha com bárbaros. Instâncias de nosso exército têm métodos para mover cavalaria, soldados e gigantes. Mas somos forçados a chamar esses métodos separadamente, o que leva muito tempo. Como podemos facilitar a administração do exército?

Vamos precisar do seguinte:

  • espécimes do exército
  • Classe 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
    })
})()

Casos de uso


  • Quando queremos converter muitas linhas de código, possivelmente repetidas, em uma função simples.

Oportunista


Adaptável - um modelo projetado para transferência eficiente de dados entre muitos objetos pequenos. É usado para melhorar o desempenho e economizar memória.

Suponha que queremos controlar um exército de caminhantes brancos. Ao mesmo tempo, nossos caminhantes podem ter três estados:

  • vivo
  • morto
  • ressuscitado

Vamos precisar do seguinte:

  • Classe WhiteWalker
  • classe WhiteWalkerFlyweight
  • cliente para a ressurreição de caminhantes brancos

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())
})()

Casos de uso


  • Quando queremos evitar criar um grande número de objetos
  • Quando queremos criar objetos que consomem uma grande quantidade de memória
  • Quando precisamos de objetos cuja criação requer cálculos complexos
  • Quando temos um suprimento limitado de recursos: poder de computação, memória, espaço etc.

Proxies


O modelo de proxy, como o nome indica, é usado como um suplemento ou um substituto para outro objeto para controlar o acesso a esse objeto.

Imagine que a rainha Cersei emitiu um decreto proibindo o recrutamento de mais de cem soldados sem a permissão dela. Como implementamos isso?

Vamos precisar do seguinte:

  • soldado de classe
  • Classe ArmyProxy para controle de processos
  • instâncias da classe Cercei para permissão

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`)
})()

Casos de uso


  • Quando o objeto que queremos usar está longe (profundamente aninhado) e salva a lógica no proxy para não afetar o cliente
  • Quando queremos fornecer um resultado aproximado em antecipação a um resultado real, cujo cálculo leva muito tempo
  • Quando queremos controlar o acesso ou a criação de um objeto sem interferir com sua lógica

Código do Github .

Nota trans: aqui está um ótimo vídeo sobre padrões de design.

Obrigado pela atenção.

All Articles