Generating design patterns in ES6 + on the example of the Game of Thrones



Design patterns - ways to solve the most common problems in software development. In this article, we will look at generative patterns with references to the Game of Thrones.

Read about structural patterns here .

Generating patterns are designed to work with mechanisms for constructing objects in order to create an object in the most suitable way in this situation.

The most common generative patterns are as follows:

  • Factory (Fabric)
  • Abstraction (Abstract)
  • Constructor (Builder)
  • Prototype (Prototype)
  • Singleton (Singleton)

Factory


A factory is a pattern that uses the so-called factory methods to create objects without the need to determine the class of the created object. What does this mean?

Imagine that we want to be able to create soldiers. These soldiers can be either from the Targaryen house, or from the Lannister house.

For this we need:

  • Interface for defining soldiers
  • A class for each type of soldier to empower every home
  • Class to request the creation of a new soldier, regardless of the house to which he belongs

class Soldier {
    constructor(name) {
        this.name = name
        this.attack = this.attack.bind(this)
    }

    attack() {}
}

class LannisterSoldier extends Soldier {
    attack() {
        return 'Lannister always pays his debts'
    }
}

class TargaryenSoldier extends Soldier {
    attack() {
        return 'Fire and blond'
    }
}

class Spawn {
    constructor(type, name) {
        if (type === 'lannister') return new LannisterSoldier(name)
        else return new TargaryenSoldier(name)
    }
}

(() => {
    const lannister = new Spawn('lannister', 'soldier1')
    const targaryen = new Spawn('targaryen', 'soldier2')
    console.log(lannister.attack())
    console.log(targaryen.attack())
})()

When is it used?


  • ,
  • ,


Disclaimer: An abstract template based on objects. It is extremely difficult to use in functional programming.

This template allows you to encapsulate a group of individual factories that perform similar tasks, without the need to define specific classes. In standard use, the client software creates a specific implementation of the abstract factory and then uses the general factory interface to create specific objects as part of a single system. The client does not know (or does not matter to him) what specific objects he receives from each internal factory, since a common interface is used for this.

Imagine that we want to manage each house individually. Each house should have the opportunity to determine the heir to the throne and his beloved.

For this we need:

  • ,

//  
class HeirToTheThrone {
    conctructor(name, isActualOnTheThrone) {
        this.name = name
        this.isActualOnTheThrone = isActualOnTheThrone
        this.getTheThrone = this.getTheThrone.bind(this)
    }
    getTheThrone() {}
}

class HeirToTheThroneLannister extends HeirToTheThrone {
    getTheThrone(){
        console.log('kill all')
    }
}

class HeirToTheThroneTargaryen extends HeirToTheThrone {
    getTheThrone() {
        console.log('burn all')
    }
}

//  
class Subject {
    constructor(name) {
        this.name = name
        this.speak = this.speak.bind(this)
    }
    speak() {}
}

class SubjectLannister extends Subject {
    speak(){
        console.log('i love Cersei')
    }
}

class SubjectTargaryen extends Subject {
    speak(){
        console.log('i love Daenerys')
    }
}

//  
class House {
    constructor() {
        this.getHeir = this.getHeir.bind(this)
        this.getSubject = this.getSubject.bind(this)
    }
    getHeir(){}
    getSubject(){}
}

class Lannister extends House {
    getHeir() {
        return new HeirToTheThroneLannister('Cersei', true)
    }
    getSubject(name) {
        return new SubjectLannister(name)
    }
}

class Targaryen extends House {
    getHeir() {
        return new HeirToTheThroneTargaryen('Daenerys', true)
    }
    getSubject(name) {
        return new SubjectTargaryen(name)
    }
}

(()=>{
    const lannister = new Lannister()
    const targaryen = new Targaryen()
    lannister.getHeir().getTheThrone()
    lannister.getSubject().speak()
    targaryen.getHeir().getTheThrone()
    targaryen.getSubject().speak()
})()

?


  • ,
  • ,


The purpose of the designer is to separate a complex object from its representations. With the complexity of the object, this template allows you to separate the process of creating a new object through another object (constructor).

Imagine that we want to create a fleet for every home. Each family will have a certain number of ships and warriors. To facilitate the creation, we should be able to call the makeNavy method, which will automatically create everything we need through the settings.

For this we need:

  • Class for creating a fleet
  • Interface for constructor definition
  • The class responsible for creating the objects of our army
  • Classes responsible for creating soldiers and ships

class Lannister {
    constructor() {
        this.soldiers = []
        this.ships = []
        this.makeNavy = this.makeNavy.bind(this)
    }
    makeNavy(soldiers, ships) {
        const Build = new ConcreteBuilder()
        for (let i = 0; i < soldiers; i++) {
            this.soldiers.push(Build.createSoldier())
        }
        for (let i = 0; i < ships; i++) {
            this.ships.push(Build.createShip())
        }
    }
}

class Builder {
    createSoldier() {}
    createShip() {}
}

class ConcreteBuilder extends Builder {
    createSoldier() {
        const soldier = new Soldier()
        return soldier
    }
    createShip() {
        const ship = new Ship()
        return ship
    }
}

class Soldier {
    constructor() {
        console.log('soldier created')
    }
}

class Ship {
    constructor() {
        console.log('ship created')
    }
}

(() => {
    const lannister = new Lannister()
    lannister.makeNavy(100, 10)
})()

When is it used?


  • When the process of creating an object is very complex, it involves a large number of mandatory and optional parameters
  • When an increase in the number of constructor parameters leads to an increase in the number of constructors
  • When the client expects different views for the constructed object

Prototype


The prototype allows you to determine the types of objects created through prototype inheritance and create new objects using the scheme of an existing object. This improves performance and minimizes memory loss.

Imagine that we want to create an army of white walkers. We have no special requirements for this army. We just want a lot of them, that they have the same characteristics and that they do not take up a lot of memory space.

For this we need:

  • Class for storing information about white walkers
  • Method for cloning an instance returning the same method

class WhiteWalker {
    constructor(force, weight) {
        this.force = force
        this.weight = weight
    }

    clone() {
        console.log('cloned')
        return new WhiteWalker(this.name, this.weight)
    }
}

(()=>{
    const firstWalker = new WhiteWalker()
    const walkers = [firstWalker]
    for(let i=0;i<100;i++){
        walkers.push(firstWalker.clone())
    }
})()

Advantages and disadvantages of the prototype


Pros:

  • Helps save cost, time and productivity by eliminating the need to use a new operator to create new objects
  • Reduces the complexity of initializing an object: each class uses its own method of cloning
  • There is no need to classify and create many subclasses to initialize objects as when using an abstract template
  • Increases system flexibility by creating new objects by changing some properties of the copied object

Minuses:

  • Cloning complex objects with circular references is a non-trivial task

Singleton


Singleton allows you to make sure that the created object is the only instance of a particular class. Such an object is usually used to control several operations in the system.

Imagine that we want to be sure of having a single Dragon Mother.

For this we need:

  • Class for storing information about Mother of dragons
  • Saved instance of Dragon Mother

let INSTANCE = null

class MotherOfDragons {
    constructor(name, dragons) {
        if(!!INSTANCE) return INSTANCE
        this.dragons = dragons
        this.name = name
        this.getMyDragons = this.getMyDragons.bind(this)
        INSTANCE = this
    }

    getMyDragons(){
        console.log(`I'm ${this.name} and my dragons are`, this.dragons)
    }
}

(()=>{
    const dragonMother = new MotherOfDragons('Daenerys Targaryen', [
        'Drogon',
        'Rhaegal',
        'Viserion'
    ])
    const dragonMother2 = new MotherOfDragons('Cercei Targaryen', [
        'Tirion',
        'Jennifer',
        'Goblin'
    ])
    dragonMother.getMyDragons()
    dragonMother2.getMyDragons()
    console.log(dragonMother instanceof MotherOfDragons)
    console.log(dragonMother2 instanceof MotherOfDragons)
    console.log(dragonMother2 === dragonMother)
})()

When is it used?


  • When the resources used to create the object are limited (for example, database connection objects)
  • It’s good practice to build authorization with a singleton to improve performance.
  • When you need to create a class to configure the application
  • When you need to create a class for resource allocation

Github code .

Note trans: here is a great video on design patterns.

Thank you for attention.

All Articles