Modèles de conception structurelle dans ES6 + sur l'exemple de Game of Thrones



Bonjour mes amis!

Les modèles de conception structurelle sont utilisés pour construire de grands systèmes de relations entre les objets afin de maintenir la flexibilité et l'efficacité. Regardons certains d'entre eux avec des références au Game of Thrones.

Dans le développement de logiciels, les modèles de conception sont utilisés pour résoudre les problèmes les plus courants. Ils représentent les meilleures pratiques développées depuis longtemps par les développeurs dans le processus de test des applications et de correction des bugs.

Dans cet article, nous parlerons des modèles structurels. Ils sont conçus pour concevoir des applications en définissant un moyen simple d'interagir avec les instances.

Les plus courants sont les modèles suivants:

  • Adaptateur
  • Décorateur
  • Façade
  • Adaptatif (léger (élément), poids mouche)
  • Procuration

Adaptateur


Un adaptateur est un modèle qui vous permet de traduire (transférer) l'interface d'une classe à une autre. Il permet aux classes de travailler ensemble, ce qui est souvent impossible en raison de structures incompatibles.

Imaginez que les Targaryen aient décidé de se battre avec toutes les forces à leur disposition (Flawless et Dragons), et Daenerys cherche un moyen d'interagir avec chacun d'eux.

Nous aurons besoin des éléments suivants:

  • Classe non souillée
  • Classe de dragon
  • Classe d'adaptateur pour passer la méthode de gravure de la classe Dragon à la méthode commune de destruction

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

Cas d'utilisation


  • Lorsque de nouveaux composants ou modules doivent fonctionner avec ceux existant dans l'application ou lorsqu'une partie du code a été améliorée à la suite de la refactorisation, mais doit interagir avec les anciennes parties.
  • Lorsque vous avez trouvé une bibliothèque pour résoudre des problèmes secondaires et que vous souhaitez l'intégrer dans votre application.

Décorateur


Un décorateur est un modèle conçu pour ajouter dynamiquement un comportement ou des fonctionnalités aux classes existantes. Il s'agit d'une alternative à la sous-classification.

Supposons que nous voulions vérifier si la boisson servie au roi Joffrey est empoisonnée.

Nous aurons besoin des éléments suivants:

  • Classe King
  • Instance du roi Joffrey
  • Cours de boisson
  • Classe Boisson empoisonnée
  • Fonction isNotPoisoned pour sauver la vie du roi

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

Cas d'utilisation du décorateur


  • Lorsque nous voulons ajouter une fonction à un grand nombre de classes ou de méthodes avec des contextes différents
  • Lorsque nous voulons améliorer une classe précédemment créée, mais que nous n'avons pas le temps de procéder à une refactorisation complète

Façade


Façade - un modèle largement utilisé dans les bibliothèques. Il est utilisé pour fournir une interface unifiée et simple et masquer la complexité de ses sous-systèmes ou sous-classes.

Imaginez que nous voulons contrôler une armée dans une bataille avec des barbares. Les instances de notre armée ont des méthodes pour déplacer la cavalerie, les soldats et les géants. Mais nous sommes obligés d'appeler ces méthodes séparément, ce qui prend beaucoup de temps. Comment pouvons-nous faciliter la gestion de l'armée?

Nous aurons besoin des éléments suivants:

  • spécimens de l'armée
  • 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
    })
})()

Cas d'utilisation


  • Lorsque nous voulons convertir de nombreuses lignes de code, éventuellement répétitives, en une seule fonction simple.

Opportuniste


Adaptive - un modèle conçu pour un transfert de données efficace entre de nombreux petits objets. Il est utilisé pour améliorer les performances et économiser de la mémoire.

Supposons que nous voulons contrôler une armée de marcheurs blancs. En même temps, nos marcheurs peuvent avoir trois états:

  • vivant
  • mort
  • ressuscité

Nous aurons besoin des éléments suivants:

  • Classe WhiteWalker
  • classe WhiteWalkerFlyweight
  • client pour la résurrection des marcheurs blancs

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

Cas d'utilisation


  • Quand on veut éviter de créer un grand nombre d'objets
  • Lorsque nous voulons créer des objets qui consomment une grande quantité de mémoire
  • Quand on a besoin d'objets dont la création nécessite des calculs complexes
  • Lorsque nos ressources sont limitées: puissance de calcul, mémoire, espace, etc.

Procurations


Le modèle de proxy, comme son nom l'indique, est utilisé comme complément ou substitut d'un autre objet pour contrôler l'accès à cet objet.

Imaginez que la reine Cersei a publié un décret interdisant le recrutement de plus de 100 soldats sans sa permission. Comment mettons-nous cela en œuvre?

Nous aurons besoin des éléments suivants:

  • soldat de classe
  • Classe ArmyProxy pour le contrôle des processus
  • instances de la classe Cercei pour permission

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

Cas d'utilisation


  • Lorsque l'objet que nous voulons utiliser est éloigné (profondément imbriqué) et enregistrez la logique dans le proxy afin de ne pas affecter le client
  • Lorsque nous voulons donner un résultat approximatif en prévision d'un résultat réel, dont le calcul prend beaucoup de temps
  • Quand on veut contrôler l'accès ou la création d'un objet sans interférer avec sa logique

Code Github .

Remarque trans: voici une superbe vidéo sur les modèles de conception.

Merci de votre attention.

All Articles