Strukturelle Entwurfsmuster in ES6 + am Beispiel des Game of Thrones



Guten Tag, Freunde!

Strukturelle Entwurfsmuster werden verwendet, um große Beziehungssysteme zwischen Objekten aufzubauen, um Flexibilität und Effizienz aufrechtzuerhalten. Schauen wir uns einige davon mit Verweisen auf das Game of Thrones an.

In der Softwareentwicklung werden Entwurfsmuster verwendet, um die häufigsten Probleme zu lösen. Sie stellen die Best Practices dar, die Entwickler seit langem beim Testen von Anwendungen und Beheben von Fehlern entwickelt haben.

In diesem Artikel werden wir über strukturelle Muster sprechen. Sie dienen zum Entwerfen von Anwendungen, indem sie eine einfache Möglichkeit zur Interaktion mit Instanzen definieren.

Am häufigsten sind die folgenden Muster:

  • Adapter
  • Dekorateur
  • Fassade
  • Adaptiv (Leichtgewicht (Element), Fliegengewicht)
  • Proxy

Adapter


Ein Adapter ist eine Vorlage, mit der Sie die Schnittstelle einer Klasse in eine andere übersetzen (übertragen) können. Dadurch können Klassen zusammenarbeiten, was aufgrund inkompatibler Strukturen häufig nicht möglich ist.

Stellen Sie sich vor, die Targaryen haben beschlossen, mit allen ihnen zur Verfügung stehenden Kräften (Flawless and Dragons) zu kämpfen, und Daenerys sucht nach einer Möglichkeit, mit jedem von ihnen zu interagieren.

Wir werden folgendes brauchen:

  • Unbeschmutzte Klasse
  • Drachenklasse
  • Adapterklasse zum Übergeben der Brennmethode der Dragon-Klasse an die allgemeine Kill-Methode

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

Anwendungsfälle


  • Wenn neue Komponenten oder Module mit in der Anwendung vorhandenen Komponenten zusammenarbeiten sollen oder wenn ein Teil des Codes infolge von Refactoring verbessert wurde, aber mit den alten Teilen interagieren soll.
  • Wenn Sie eine Bibliothek zur Lösung sekundärer Probleme gefunden haben und diese in Ihre Anwendung integrieren möchten.

Dekorateur


Ein Dekorator ist eine Vorlage, mit der vorhandenen Klassen dynamisch Verhalten oder Funktionen hinzugefügt werden können. Dies ist eine Alternative zur Unterklassifizierung.

Angenommen, wir möchten überprüfen, ob das Getränk, das König Joffrey serviert wird, vergiftet ist.

Wir werden folgendes brauchen:

  • Königsklasse
  • König Joffrey Instanz
  • Getränkeklasse
  • Klasse Vergiftetes Getränk
  • isNotPoisoned-Funktion, um das Leben des Königs zu retten

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

Anwendungsfälle für Dekorateure


  • Wenn wir einer großen Anzahl von Klassen oder Methoden mit unterschiedlichen Kontexten eine Funktion hinzufügen möchten
  • Wenn wir eine zuvor erstellte Klasse verbessern möchten, aber keine Zeit für ein vollständiges Refactoring haben

Fassade


Fassade - eine in Bibliotheken weit verbreitete Vorlage. Es wird verwendet, um eine einheitliche und einfache Schnittstelle bereitzustellen und die Komplexität seiner Subsysteme oder Unterklassen zu verbergen.

Stellen Sie sich vor, wir wollen eine Armee in einem Kampf mit Barbaren kontrollieren. Instanzen unserer Armee haben Methoden, um Kavallerie, Soldaten und Riesen zu bewegen. Wir sind jedoch gezwungen, diese Methoden separat aufzurufen, was viel Zeit in Anspruch nimmt. Wie können wir das Armeemanagement vereinfachen?

Wir werden folgendes brauchen:

  • Armeeproben
  • ArmyFacade-Klasse

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

Anwendungsfälle


  • Wenn wir viele Codezeilen, die sich möglicherweise wiederholen, in eine einfache Funktion konvertieren möchten.

Opportunist


Adaptiv - eine Vorlage für eine effiziente Datenübertragung zwischen vielen kleinen Objekten. Es wird verwendet, um die Leistung zu verbessern und Speicherplatz zu sparen.

Angenommen, wir wollen eine Armee weißer Wanderer kontrollieren. Gleichzeitig können unsere Wanderer drei Zustände haben:

  • am Leben
  • tot
  • auferstanden

Wir werden folgendes brauchen:

  • WhiteWalker-Klasse
  • Klasse WhiteWalkerFlyweight
  • Klient für die Auferstehung der weißen Wanderer

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

Anwendungsfälle


  • Wenn wir vermeiden möchten, eine große Anzahl von Objekten zu erstellen
  • Wenn wir Objekte erstellen möchten, die viel Speicher verbrauchen
  • Wenn wir Objekte brauchen, deren Erstellung komplexe Berechnungen erfordert
  • Wenn wir nur über begrenzte Ressourcen verfügen: Rechenleistung, Speicher, Speicherplatz usw.

Proxies


Die Proxy-Vorlage wird, wie der Name schon sagt, als Add-In oder Ersatz für ein anderes Objekt verwendet, um den Zugriff auf dieses Objekt zu steuern.

Stellen Sie sich vor, Königin Cersei hat ein Dekret erlassen, das die Rekrutierung von mehr als 100 Soldaten ohne ihre Erlaubnis verbietet. Wie setzen wir das um?

Wir werden folgendes brauchen:

  • Klassensoldat
  • ArmyProxy-Klasse zur Prozesssteuerung
  • Instanzen der Cercei-Klasse zur Erlaubnis

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

Anwendungsfälle


  • Wenn das Objekt, das wir verwenden möchten, weit entfernt ist (tief verschachtelt), speichern Sie die Logik im Proxy, um den Client nicht zu beeinträchtigen
  • Wenn wir in Erwartung eines realen Ergebnisses ein ungefähres Ergebnis angeben möchten, dessen Berechnung viel Zeit in Anspruch nimmt
  • Wenn wir den Zugriff oder die Erstellung eines Objekts steuern möchten, ohne dessen Logik zu beeinträchtigen

Github- Code .

Hinweis trans: hier ist ein tolles Video über Designmuster.

Vielen Dank für Ihre Aufmerksamkeit.

All Articles