Patrones de dise帽o estructural en ES6 + en el ejemplo de Game of Thrones



隆Buen dia amigos!

Los patrones de dise帽o estructural se utilizan para construir grandes sistemas de relaciones entre objetos con el fin de mantener la flexibilidad y la eficiencia. Veamos algunos de ellos con referencias al Juego de Tronos.

En el desarrollo de software, los patrones de dise帽o se utilizan para resolver los problemas m谩s comunes. Representan las mejores pr谩cticas desarrolladas durante mucho tiempo por los desarrolladores en el proceso de probar aplicaciones y corregir errores.

En este art铆culo hablaremos sobre patrones estructurales. Est谩n dise帽ados para dise帽ar aplicaciones definiendo una forma simple de interactuar con las instancias.

Los m谩s comunes son los siguientes patrones:

  • Adaptador
  • Decorador
  • Fachada
  • Adaptativo (peso ligero (elemento), peso mosca)
  • Apoderado

Adaptador


Un adaptador es una plantilla que le permite traducir (transferir) la interfaz de una clase a otra. Permite que las clases trabajen juntas, lo que a menudo es imposible debido a estructuras incompatibles.

Imagina que los Targaryen decidieron luchar con todas las fuerzas a su disposici贸n (Flawless and Dragons), y Daenerys est谩 buscando una manera de interactuar con cada uno de ellos.

Necesitaremos lo siguiente:

  • Clase inmaculada
  • Clase de drag贸n
  • Clase de adaptador para pasar el m茅todo de grabaci贸n de la clase Dragon al m茅todo com煤n 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())
})()

Casos de uso


  • Cuando los nuevos componentes o m贸dulos deben funcionar con los existentes en la aplicaci贸n o cuando parte del c贸digo se ha mejorado como resultado de la refactorizaci贸n, pero deben interactuar con las partes antiguas.
  • Cuando encontr贸 una biblioteca para resolver problemas secundarios y desea integrarla en su aplicaci贸n.

Decorador


Un decorador es una plantilla dise帽ada para agregar din谩micamente comportamiento o funcionalidad a las clases existentes. Esta es una alternativa a la subclasificaci贸n.

Supongamos que queremos verificar si la bebida servida al Rey Joffrey est谩 envenenada.

Necesitaremos lo siguiente:

  • Clase King
  • Instancia del Rey Joffrey
  • Clase de bebida
  • Bebida envenenada de clase
  • Funci贸n isNotPoisoned para salvar la vida del rey

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 del decorador


  • Cuando queremos agregar una funci贸n a una gran cantidad de clases o m茅todos con diferentes contextos
  • Cuando queremos mejorar una clase creada anteriormente, pero no tenemos tiempo para una refactorizaci贸n completa

Fachada


Fachada: una plantilla ampliamente utilizada en bibliotecas. Se utiliza para proporcionar una interfaz unificada y simple y ocultar la complejidad de sus subsistemas o subclases.

Imagina que queremos controlar un ej茅rcito en una batalla con b谩rbaros. Las instancias de nuestro ej茅rcito tienen m茅todos para mover caballer铆a, soldados y gigantes. Pero nos vemos obligados a llamar a estos m茅todos por separado, lo que lleva mucho tiempo. 驴C贸mo podemos facilitar la gesti贸n del ej茅rcito?

Necesitaremos lo siguiente:

  • espec铆menes del ej茅rcito
  • Clase de 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


  • Cuando queremos convertir muchas l铆neas de c贸digo, posiblemente repitiendo, en una funci贸n simple.

Oportunista


Adaptable: una plantilla dise帽ada para la transferencia eficiente de datos entre muchos objetos peque帽os. Se utiliza para mejorar el rendimiento y ahorrar memoria.

Supongamos que queremos controlar un ej茅rcito de caminantes blancos. Al mismo tiempo, nuestros caminantes pueden tener tres estados:

  • viva
  • muerto
  • resucitado

Necesitaremos lo siguiente:

  • Clase WhiteWalker
  • clase WhiteWalkerFlyweight
  • cliente para la resurrecci贸n de caminantes blancos

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


  • Cuando queremos evitar crear una gran cantidad de objetos
  • Cuando queremos crear objetos que consuman una gran cantidad de memoria
  • Cuando necesitamos objetos cuya creaci贸n requiere c谩lculos complejos
  • Cuando tenemos un suministro limitado de recursos: potencia inform谩tica, memoria, espacio, etc.

Proxies


La plantilla de proxy, como su nombre lo indica, se utiliza como complemento o como sustituto de otro objeto para controlar el acceso a este objeto.

Imagine que la Reina Cersei emiti贸 un decreto que proh铆be el reclutamiento de m谩s de 100 soldados sin su permiso. 驴C贸mo implementamos esto?

Necesitaremos lo siguiente:

  • soldado de clase
  • Clase ArmyProxy para control de procesos
  • instancias de la clase Cercei para permiso

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


  • Cuando el objeto que queremos usar est谩 lejos (profundamente anidado) y guarda la l贸gica en el proxy para no afectar al cliente
  • Cuando queremos dar un resultado aproximado en previsi贸n de un resultado real, cuyo c谩lculo lleva mucho tiempo
  • Cuando queremos controlar el acceso o la creaci贸n de un objeto sin interferir con su l贸gica

Github c贸digo .

Nota trans: aqu铆 hay un gran video sobre patrones de dise帽o.

Gracias por su atenci贸n.

All Articles