Что такое шаблоны проектирования и зачем их использовать?

Шаблоны проектирования - проверенный способ для решения проблем. Они не включают в себя такие очевидные вещи, как использование for loop для перебора элементов массива. Их используют для решения более сложных проблем, с которыми мы сталкиваемся при разработке больших приложений.


Типы шаблонов и примеры некоторых из них:

- Порождающие (Creational design patterns): создание новых объектов.

  • Конструктор (Constructor)
  • Фабрика (Factory)
  • Прототип (Prototype)
  • Синглтон (Singletion)

Структурные (Structural design patterns): упорядочивают объекты.

  • Адаптер (Adapter)
  • Декоратор (Decorator)
  • Фасад (Facade)

Поведенческие (Behavioral design patterns): как объекты соотносятся друг с другом.

  • Цепочка обязанностей (Chain of Responsibility)
  • Команда (Command)
  • Посредник (Mediator)
  • Стратегия (Strategy)

Constructor

// ES5
function Person(name, age, favFood) {
    this.name = name
    this.age = age
    this.favFood = favFood
}

Person.prototype.greet = function() {
    console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old, and my favorite food is ${this.favFood}`)
}

const bob = new Person('Bob', 22, 'Chicken')
bob.greet()
// Hello, my name is Bob, I'm 22 years old, and my favorite food is Chicken
// ES6
class Person {
    constructor(name, age, favFood) {
        this.name = name
        this.age = age
        this.favFood = favFood
    }

    greet() {
        console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old, and my favorite food is ${this.favFood}`)
    }
}

const bob = new Person('Bob', 22, 'Chicken')
bob.greet()
// Hello, my name is Bob, I'm 22 years old, and my favorite food is Chicken

Factory

class SimpleMembership {
    constructor(name) {
        this.name = name
        this.cost = 50
    }
}

class StandardMembership {
    constructor(name) {
        this.name = name
        this.cost = 100
    }
}

class PremiumMembership {
    constructor(name) {
        this.name = name
        this.cost = 200
    }
}

class MemberFactory {
    static list = {
        simple: SimpleMembership,
        standard: StandardMembership,
        premium: PremiumMembership
    }

    create(name, type = 'simple') {
        const Membership = MemberFactory.list[type] || MemberFactory.list.simple
        const member = new Membership(name)

        member.type = type
        member.define = function() {
            console.log(`${this.name} (${this.type}): ${this.cost}`)
        }

        return member
    }
}

const factory = new MemberFactory()

const members = [
    factory.create('John', 'simple'),
    factory.create('Doe', 'standard'),
    factory.create('Mike', 'premium'),
    factory.create('Foo')
]

members.forEach(member => {
    member.define()
})

/*
    John (simple): 50
    Doe (standard): 100
    Mike (premium): 200
    Foo (simple): 50
*/

Prototype

const car = {
    wheels: 4,

    init() {
        console.log(`Wheels: ${this.wheels} / Owner: ${this.owner}`)
    }
}

const carWithOwner = Object.create(car, {
    owner: {
        value: 'John'
    }
})

carWithOwner.init()
// Wheels: 4 / Owner: John

console.log(carWithOwner.__proto__ === car)
// true

Singletion

class Database {
    constructor(data) {
        if (Database.exists) {
            return Database.instance
        }

        Database.instance = this
        Database.exists = true
        this.data = data
    }

    getData() {
        return this.data
    }
}

const mongo = new Database('MongoDB')
console.log(mongo.getData())  // MongoDB

const mysql = new Database('MySQL')
console.log(mysql.getData())  // MongoDB

Adapter

class OldCalc {
    operations(t1, t2, operation) {
        switch (operation) {
            case 'add': return t1 + t2
            case 'sub': return t1 - t2
            default: return NaN
        }
    }
}

class NewCalc {
    add(t1, t2) {
        return t1 + t2
    }

    sub(t1, t2) {
        return t1 - t2
    }
}

class CalcAdapter {
    constructor() {
        this.calc = new NewCalc()
    }

    operations(t1, t2, operation) {
        switch (operation) {
            case 'add': return t1 + t2
            case 'sub': return t1 - t2
            default: return NaN
        }
    }
}

const oldCalc = new OldCalc()
console.log(oldCalc.operations(10, 5, 'add'))  // 15

const newCalc = new NewCalc()
console.log(newCalc.add(10, 5))  // 15

const adapter = new CalcAdapter()
console.log(adapter.operations(25, 10, 'sub'))  // 15
console.log(adapter.calc.add(10, 5))  // 15

Decorator

class Server {
    constructor(ip, port) {
        this.ip = ip
        this.port = port
    }

    get url() {
        return `https://${this.ip}:${this.port}`
    }
}

function decoratorAws(server) {
    server.isAws = true
    server.awsInfo = function() {
        return server.url
    }

    return server
}

function decoratorAzure(server) {
    server.isAzure = true
    server.port = 22

    return server
}

const s1 = decoratorAws(new Server('12.34.56.78', 8080))
console.log(s1.isAws)  // true
console.log(s1.awsInfo())  // https://12.34.56.78:8080

const s2 = decoratorAzure(new Server('87.65.43.21', 36))
console.log(s2.isAzure)  // true
console.log(s2.port)  // 22
console.log(s2.url)  // https://87.65.43.21:22

Facade

class Complaints {
    constructor() {
        this.complaints = []
    }

    reply(complaint) {}

    add(complaint) {
        this.complaints.push(complaint)
        return this.reply(complaint)
    }
}

class ProductComplaints extends Complaints {
    reply({id, customer, details}) {
        return `Product: ${id}: ${customer} (${details})`
    }
}

class ServiceComplaints extends Complaints {
    reply({id, customer, details}) {
        return `Service: ${id}: ${customer} (${details})`
    }
}

class ComplaintRegistry {
    register(customer, type, details) {
        const id = Date.now()
        let complaint

        if (type === 'service') {
            complaint = new ServiceComplaints()
        } else {
            complaint = new ProductComplaints()
        }

        return complaint.add({id, customer, details})
    }
}

const registry = new ComplaintRegistry()

console.log(registry.register('John', 'service', 'Blocked'))
// Service: 1595708989374: John (Blocked)

console.log(registry.register('Doe', 'product', 'Denied'))
// Product: 1595708989374: Doe (Denied)

Chain of Responsibility

class MySum {
    constructor(initialValue = 5) {
        this.sum = initialValue
    }

    add(value) {
        this.sum += value
        return this
    }
}

const sum1 = new MySum()
console.log(sum1.add(5).add(10).add(30).sum)  // 50

const sum2 = new MySum(0)
console.log(sum2.add(1).add(2).add(3).sum)  // 6

Command

class MyMath {
    constructor(initialValue = 0) {
        this.num = initialValue
    }

    square() {
        return this.num ** 2
    }

    cube() {
        return this.num ** 3
    }
}

class Command {
    constructor(subject) {
        this.subject = subject
        this.commandsExecuted = []
    }

    execute(command) {
        this.commandsExecuted.push(command)
        return this.subject[command]()
    }
}

const x = new Command(new MyMath(2))

console.log(x.execute('square'))  // 4
console.log(x.execute('cube'))  // 8
console.log(x.commandsExecuted)  // (2) ["square", "cube"]

Mediator

class User {
    constructor(name) {
        this.name = name
        this.room = null
    }

    send(message, to) {
        this.room.send(message, this, to)
    }

    receive(message, from) {
        console.log(`${from.name} => ${this.name}: ${message}`)
    }
}

class ChatRoom {
    constructor() {
        this.users = {}
    }

    register(user) {
        this.users[user.name] = user
        user.room = this
    }

    send(message, from, to) {
        if (to) {
            to.receive(message, from)
        } else {
            Object.keys(this.users).forEach(key => {
                if (this.users[key] !== from) {
                    this.users[key].receive(message, from)
                }
            })
        }
    }
}

const john = new User('John')
const doe = new User('Doe')
const foo = new User('Foo')

const room = new ChatRoom()

room.register(john)
room.register(doe)
room.register(foo)

john.send('Hello!', doe)
doe.send('Hello!', john)
foo.send('Hello!')

/*
    John => Doe: Hello!
    Doe => John: Hello!
    Foo => John: Hello!
    Foo => Doe: Hello!
*/

Strategy

class Vehicle {
    travelTime() {
        return this.timeTaken
    }
}

class Bus extends Vehicle {
    constructor() {
        super()
        this.timeTaken = 10
    }
}

class Taxi extends Vehicle {
    constructor() {
        super()
        this.timeTaken = 5
    }
}

class Car extends Vehicle {
    constructor() {
        super()
        this.timeTaken = 3
    }
}

class Commute {
    travel(transport) {
        return transport.travelTime()
    }
}

const commute = new Commute()

console.log(commute.travel(new Bus()))  // 10
console.log(commute.travel(new Taxi()))  // 5
console.log(commute.travel(new Car()))  // 3