Шаблоны проектирования в JavaScript простыми словами

Реализуем популярные шаблоны проектирования в JavaScript и параллельно разбираемся в этой непростой теме на реальных примерах.

Шаблоны проектирования в JavaScript имеют ряд особенностей. В языке нет интерфейсов, поэтому приходится прибегать к некоторым договоренностям. Будем считать, что класс реализует определенный интерфейс, если он имеет присущие ему методы и свойства. Сами интерфейсы описаны в комментариях.

В примерах используется синтаксис ES6.

Реальные шаблоны проектирования в JavaScript также описаны в нашей статье Паттерны JavaScript: курс, который упростит разработку.

Порождающие шаблоны проектирования в JavaScript

Иногда нежелательно создавать экземпляр класса напрямую. Тогда мы прибегаем к порождающим паттернам, которые могут выбирать оптимальный механизм инстанцирования.

Простая фабрика

Самостоятельно делать двери при строительстве дома было бы довольно сложно, поэтому вы получаете их готовыми из магазина.

Паттерн Простая фабрика производит нужный экземпляр, не утруждая клиента тонкостями этого процесса.

Пример реализации

Создадим неявный интерфейс для всех дверей:

/*
Door

getWidth()
getHeight()

*/

class WoodenDoor {
  constructor(width, height){
    this.width = width
    this.height = height
  }

  getWidth(){
    return this.width
  }

  getHeight(){
    return this.height
  }
}

Организуем фабрику, которая будет их производить:

const DoorFactory = {
  makeDoor : (width, height) => new WoodenDoor(width, height)
}

Все, можно работать:

const door = DoorFactory.makeDoor(100, 200)
console.log('Width:', door.getWidth())
console.log('Height:', door.getHeight())

Паттерн полезен, если создание объекта требует некоторой логики. Имеет смысл вынести повторяющийся код в отдельную Простую фабрику.

Фабричный метод

Менеджер по подбору персонала работает с кандидатами на разные вакансии. Вместо того чтобы вникать в тонкости каждой должности, он делегирует проведение технического интервью коллегам-специалистам.

Паттерн Фабричный метод позволяет делегировать логику создания нужных экземпляров дочерним классам.

Пример реализации

Создадим несколько типов интервьюеров с одним интерфейсом:

/*
Interviewer interface

askQuestions()
*/

class Developer {
  askQuestions() {
    console.log('Asking about design patterns!')
  }
}

class CommunityExecutive {
  askQuestions() {
    console.log('Asking about community building')
  }
}

А вот и менеджер по подбору персонала:

class HiringManager {
        
    takeInterview() {
        const interviewer = this.makeInterviewer()
        interviewer.askQuestions()
    }
}

Дочерние классы расширяют его и предоставляют нужных интервьюеров:

class DevelopmentManager extends HiringManager {
    makeInterviewer() {
        return new Developer()
    }
}

class MarketingManager extends HiringManager {
    makeInterviewer() {
        return new CommunityExecutive()
    }
}

Теперь можно проводить собеседования:

const devManager = new DevelopmentManager()
devManager.takeInterview() // Asking about design patterns

const marketingManager = new MarketingManager()
marketingManager.takeInterview() // Asking about community buildng.

Паттерн полезен, если логика работы подклассов одинаковая, но конкретный подкласс выясняется только во время выполнения программы.

Абстрактная фабрика

Вернемся к дверям. Строго говоря, деревянные двери следует покупать в магазине деревянных дверей, железные – в магазине железных, а двери из ПВХ – в специализированном ПВХ-магазине. Также требуется эксперт по установке: плотник, сварщик или специальный установщик дверей из ПВХ.

Абстрактная фабрика – это интерфейс, который группирует другие фабрики, логически связанные друг с другом.

Пример реализации

У нас есть несколько дверей:

/*
Door interface :

getDescription()
*/

class WoodenDoor {
    getDescription() {
        console.log('I am a wooden door')
    }
}

class IronDoor {
    getDescription() {
        console.log('I am an iron door')
    }
}

и узкие специалисты-установщики:

/*
DoorFittingExpert interface :

getDescription()
*/

class Welder {
    getDescription() {
        console.log('I can only fit iron doors')
    }
}

class Carpenter {
    getDescription() {
        console.log('I can only fit wooden doors')
    }
}

Нужно все сгруппировать. Деревянные двери – с плотником, железные – со сварщиком.

/*
DoorFactory interface :

makeDoor()
makeFittingExpert()
*/

// Деревянная фабрика возвращает плотника и деревянную дверь
class WoodenDoorFactory {
    makeDoor(){
        return new WoodenDoor()
    }

    makeFittingExpert() {
        return new Carpenter()
    }
}

// Железная фабрика возвращает сварщика и железную дверь
class IronDoorFactory {
    makeDoor(){
        return new IronDoor()
    }

    makeFittingExpert() {
        return new Welder()
    }
}

Можно и дверьми заняться:

woodenFactory = new WoodenDoorFactory()

door = woodenFactory.makeDoor()
expert = woodenFactory.makeFittingExpert()

door.getDescription()  // I am a wooden door
expert.getDescription() // I can only fit wooden doors

ironFactory = new IronDoorFactory()

door = ironFactory.makeDoor()
expert = ironFactory.makeFittingExpert()

door.getDescription()  // I am an iron door
expert.getDescription() // I can only fit iron doors

Теперь в комплекте с каждой дверью идет нужный мастер.

Паттерн полезен, когда есть несколько классов, зависящих друг от друга.

Строитель

Вы заказали себе гамбургер в ресторане быстрого питания. Сначала кассир предлагает вам выбрать хлеб, потом котлету, сыр, соусы… Логика создания гамбургера оказалась довольно сложной, тут не обойтись без паттерна Строитель.

Этот шаблон позволяет создавать различные варианты объекта без загрязнения конструктора лишним кодом.

Пример реализации

Начнем с самого гамбургера:

class Burger {
    constructor(builder) {
        this.size = builder.size
        this.cheeze = builder.cheeze || false
        this.pepperoni = builder.pepperoni || false
        this.lettuce = builder.lettuce || false
        this.tomato = builder.tomato || false
    }
}

А вот и Строитель:

class BurgerBuilder {

    constructor(size) {
        this.size = size
    }

    addPepperoni() {
        this.pepperoni = true
        return this
    }

    addLettuce() {
        this.lettuce = true
        return this
    }

    addCheeze() {
        this.cheeze = true
        return this
    }

    addTomato() {
        this.tomato = true
        return this
    }

    build() {
        return new Burger(this)
    }
}

Вуаля! Вот наш бургер:

const burger = (new BurgerBuilder(14))
    .addPepperoni()
    .addLettuce()
    .addTomato()
    .build()

Паттерн Строитель нужен, если объект может существовать в разных вариациях или процесс инстанцирования состоит из нескольких шагов.

Синглтон

У страны должен быть единственный президент, иначе начнется беспорядок.

Паттерн Синглтон ограничивает инстанцирование и позволяет убедиться в том, что класс представлен в программе единственным экземпляром.

Пример реализации

В JavaScript Синглтоны могут быть реализованы с помощью шаблона Модуль. Приватные переменные и функции при этом прячутся в замыкании.

const president = (function(){
    const presidentsPrivateInformation = 'Super private'

    const name = 'Turd Sandwich'

    const getName = () => name

    return {
        getName
    }
}())

В этом примере частная информация скрыта от внешнего кода. Публичный метод позволяет узнать имя президента (но не изменить его):

president.getName() // 'Turd Sandwich'
president.name // undefined
president.presidentsPrivateInformation // undefined

Структурные шаблоны проектирования в JavaScript

Структурные шаблоны проектирования имеют дело со структурой объектов и связью между ними.

Адаптер

Адаптером является, например, карт-ридер, который необходим, чтобы перенести на компьютер фотографии с карты памяти.

Этот паттерн оборачивает несовместимый с чем-то объект и делает его совместимым, не изменяя исходный код.

Пример реализации

Возьмем игру, в которой герой охотится на львов.

Все львы имеют один интерфейс:

/*
Lion interface :

roar()
*/

class AfricanLion  {
    roar() {}
}

class AsianLion  {
    roar() {}
}

Игрок может охотиться только на объекты, соответствующие этому интерфейсу:

class Hunter {
    hunt(lion) {
        // ... 
        lion.roar()
        //... 
    }
}

Нужно ввести в игру дикую собаку, но у нее другой интерфейс. Чтобы совместить собаку и охотника, нужен Адаптер:

class WildDog {
    bark() {
    }
}

class WildDogAdapter {

    constructor(dog) {
        this.dog = dog;
    }
    
    roar() {
        this.dog.bark();
    }
}

Теперь можно вводить в игру нового персонажа:

wildDog = new WildDog()
wildDogAdapter = new WildDogAdapter(wildDog)

hunter = new Hunter()
hunter.hunt(wildDogAdapter)

Мост

У вас есть веб-сайт, на который вы хотите добавить темизацию. Можно создать для каждой темы копии всех существующих страниц, или изменять страницу в соответствии с выбранной темой.

Паттерн Мост позволяет отделить реализацию от самого объекта и выстроить иерархию реализаций.

Пример реализации

Вот иерархия страниц вашего веб-сайта:

/*
Webpage interface :

constructor(theme)
getContent()
*/

class About{ 
    constructor(theme) {
        this.theme = theme
    }
    
    getContent() {
        return "About page in " + this.theme.getColor()
    }
}

class Careers{
   constructor(theme) {
       this.theme = theme
   }
   
   getContent() {
       return "Careers page in " + this.theme.getColor()
   } 
}

И отдельная иерархия различных тем оформления:

/*
Theme interface :

getColor()
*/

class DarkTheme{
    getColor() {
        return 'Dark Black'
    }
}
class LightTheme{
    getColor() {
        return 'Off white'
    }
}
class AquaTheme{
    getColor() {
        return 'Light blue'
    }
}

А вот их взаимодействие:

const darkTheme = new DarkTheme()

const about = new About(darkTheme)
const careers = new Careers(darkTheme)

console.log(about.getContent() )// "About page in Dark Black"
console.log(careers.getContent() )// "Careers page in Dark Black"

Компоновщик

Любая организация состоит из людей. Некоторые работники объединяются в группы, и сама организация – это большая группа.

Шаблон Компоновщик позволяет единообразно обрабатывать отдельные объекты и их группы. Он работает с иерархией "часть-целое".

Пример реализации

У нас есть разные типы работников:

/*
Employee interface :

constructor(name, salary)
getName()
setSalary()
getSalary()
getRoles()
*/

class Developer {

    constructor(name, salary) {
        this.name = name
        this.salary = salary
    }

    getName() {
        return this.name
    }

    setSalary(salary) {
        this.salary = salary
    }

    getSalary() {
        return this.salary
    }

    getRoles() {
        return this.roles
    }

    develop() {
        /* */
    }
}

class Designer {

    constructor(name, salary) {
        this.name = name
        this.salary = salary
    }

    getName() {
        return this.name
    }

    setSalary(salary) {
        this.salary = salary
    }

    getSalary() {
        return this.salary
    }

    getRoles() {
        return this.roles
    }

    design() {
        /* */
    }
}

и сама организация:

class Organization {
    constructor(){
        this.employees = []
    }

    addEmployee(employee) {
        this.employees.push(employee)
    }

    getNetSalaries() {
        let netSalary = 0

        this.employees.forEach(employee => {
            netSalary += employee.getSalary()
        })

        return netSalary
    }
}

Теперь можно посчитать общую зарплату всех сотрудников:

const john = new Developer('John Doe', 12000)
const jane = new Designer('Jane', 10000)

const organization = new Organization()
organization.addEmployee(john)
organization.addEmployee(jane)

console.log("Net salaries: " , organization.getNetSalaries()) // 22000

Декоратор

В автомастерской предлагают несколько видов услуг. Чтобы рассчитать общий счет, нужно взять цену за одну услугу и последовательно прибавить все остальные, пока не получится окончательная стоимость. Каждая услуга является Декоратором.

Этот паттерн оборачивает объект и динамически изменяет его поведение.

Пример реализации

Возьмем для примера кофе. Самый простой кофе, реализующий соответствующий интерфейс:

/*
Coffee interface:
getCost()
getDescription()
*/

class SimpleCoffee{

    getCost() {
        return 10
    }

    getDescription() {
        return 'Simple coffee'
    }
}

Мы хотим иметь возможность добавлять в кофе различные добавки, для этого создадим некоторые декораторы:

class MilkCoffee {


    constructor(coffee) {
        this.coffee = coffee
    }

    getCost() {
        return this.coffee.getCost() + 2
    }

    getDescription() {
        return this.coffee.getDescription() + ', milk'
    }
}

class WhipCoffee {

    constructor(coffee) {
        this.coffee = coffee
    }

    getCost() {
        return this.coffee.getCost() + 5
    }

    getDescription() {
        return this.coffee.getDescription() + ', whip'
    }
}

class VanillaCoffee {

    constructor(coffee) {
        this.coffee = coffee
    }

    getCost() {
        return this.coffee.getCost() + 3
    }

    getDescription() {
        return this.coffee.getDescription() + ', vanilla'
    }
}

Теперь вы можете сделать кофе на свой вкус:

let someCoffee

someCoffee = new SimpleCoffee()
console.log(someCoffee.getCost())// 10
console.log(someCoffee.getDescription())// Простой кофе

someCoffee = new MilkCoffee(someCoffee)
console.log(someCoffee.getCost())// 12
console.log(someCoffee.getDescription())// Простой кофе, молоко

someCoffee = new WhipCoffee(someCoffee)
console.log(someCoffee.getCost())// 17
console.log(someCoffee.getDescription())// Простой кофе, молоко, сливки

someCoffee = new VanillaCoffee(someCoffee)
console.log(someCoffee.getCost())// 20
console.log(someCoffee.getDescription())// Простой кофе, молоко, сливки, ваниль

Фасад

Чтобы включить компьютер достаточно нажать кнопку. Это очень просто, но внутри включающегося компьютера происходит много сложных действий. Простой интерфейс к сложной системе – это и есть Фасад.

Пример реализации

Создадим класс компьютера:

class Computer {

    getElectricShock() {
        console.log('Ouch!')
    }

    makeSound() {
        console.log('Beep beep!')
    }

    showLoadingScreen() {
        console.log('Loading..')
    }

    bam() {
        console.log('Ready to be used!')
    }

    closeEverything() {
        console.log('Bup bup bup buzzzz!')
    }

    sooth() {
        console.log('Zzzzz')
    }

    pullCurrent() {
        console.log('Haaah!')
    }
}

и простой Фасад для его сложных функций:

class ComputerFacade
{
    constructor(computer) {
        this.computer = computer
    }

    turnOn() {
        this.computer.getElectricShock()
        this.computer.makeSound()
        this.computer.showLoadingScreen()
        this.computer.bam()
    }

    turnOff() {
        this.computer.closeEverything()
        this.computer.pullCurrent()
        this.computer.sooth()
    }
}

Так работать с компьютером намного проще:

const computer = new ComputerFacade(new Computer())
computer.turnOn() // Ouch! Beep beep! Loading.. Ready to be used!
computer.turnOff() // Bup bup buzzz! Haah! Zzzzz

Приспособленец

В поездах дальнего следования воду для горячих напитков кипятят в больших емкостях – сразу для всех. Это позволяет экономить электричество (или газ).

Шаблон Приспособленец сводит к минимуму использование памяти или вычислительные расходы, разделяя одни данные между множеством подобных объектов.

Пример реализации

У нас есть разные типы чая и большой чайник:

// Приспособленец — то, что будет кэшировано
// Типы чая — приспособленцы.
class KarakTea {
}

// Действует как фабрика и экономит чай
class TeaMaker {
    constructor(){
        this.availableTea = {}
    }

    make(preference) {
        this.availableTea[preference] = this.availableTea[preference] || (new KarakTea())
        return this.availableTea[preference]
    }
}

Теперь создадим чайный магазинчик, который будет принимать заказы:

class TeaShop {
    constructor(teaMaker) {
        this.teaMaker = teaMaker
        this.orders = []
    }

    takeOrder(teaType, table) {
        this.orders[table] = this.teaMaker.make(teaType)
    }

    serve() {
        this.orders.forEach((order, index) => {
            console.log('Serving tea to table#' + index)
        })
    }
}

Работает он следующим образом:

const teaMaker = new TeaMaker()
const shop = new TeaShop(teaMaker)

shop.takeOrder('less sugar', 1)
shop.takeOrder('more milk', 2)
shop.takeOrder('without sugar', 5)

shop.serve()
// Serving tea to table# 1
// Serving tea to table# 2
// Serving tea to table# 5

Прокси (Заместитель)

Представьте себе дверь с кодовым замком. Ее основная функциональность – открыться и пропустить вас в другое помещение, но сверху добавлен Прокси, который проверяет право доступа.

Заместитель обеспечивает какую-то дополнительную логику: ограничивает доступ к основному объекту, ведет логи или выполняет кэширование.

Пример реализации

Прежде всего создадим интерфейс двери и его реализацию:

/*
Door interface :

open()
close()
*/

class LabDoor {
    open() {
        console.log('Opening lab door')
    }

    close() {
        console.log('Closing the lab door')
    }
}

А теперь напишем прокси-класс, чтобы обеспечить безопасность нашей двери:

class Security {
    constructor(door) {
        this.door = door
    }

    open(password) {
        if (this.authenticate(password)) {
            this.door.open()
        } else {
        	console.log('Big no! It ain\'t possible.')
        }
    }

    authenticate(password) {
        return password === 'ecr@t'
    }

    close() {
        this.door.close()
    }
}

Теперь кто попало не сможет зайти:

const door = new Security(new LabDoor())
door.open('invalid') // Big no! It ain't possible.

door.open('ecr@t') // Opening lab door
door.close() // Closing lab door

Поведенческие шаблоны проектирования в JavaScript

Поведенческие шаблоны проектирования обеспечивают взаимодействие объектов и распределяют обязанности.

Цепочка обязанностей

У вас есть три счета (A, B, C) с разными суммами и разным приоритетом использования. Сначала проверяется A, если на нем достаточно денег для покупки, то цепочка прерывается. Иначе проверяется B, затем C. Цепь будет продолжаться, пока не найдет подходящий обработчик.

Паттерн Цепочка обязанностей позволяет выстроить объекты в такую цепь.

Пример реализации

Вот ваша учетная запись, которая реализует логику связи счетов:

class Account {

    setNext(account) {
        this.successor = account
    }
    
    pay(amountToPay) {
        if (this.canPay(amountToPay)) {
            console.log(`Paid ${amountToPay} using ${this.name}`)
        } else if (this.successor) {
            console.log(`Cannot pay using ${this.name}. Proceeding...`)
            this.successor.pay(amountToPay)
        } else {
            console.log('None of the accounts have enough balance')
        }
    }
    
    canPay(amount) {
        return this.balance >= amount
    }
}

class Bank extends Account {
    constructor(balance) {
        super()
        this.name = 'bank'
        this.balance = balance
    }
}

class Paypal extends Account {
    constructor(balance) {
        super()        
        this.name = 'Paypal'
        this.balance = balance
    }
}

class Bitcoin extends Account {
    constructor(balance) {
        super()        
        this.name = 'bitcoin'
        this.balance = balance
    }
}

Теперь составим цепочку:

// Сделаем такую цепочку
//      bank->paypal->bitcoin
//
// Приоритет у банка
//      Если банк не может оплатить, переходим к Paypal
//      Если Paypal не может, переходим к Bitcoin

const bank = new Bank(100)          // Bank with balance 100
const paypal = new Paypal(200)      // Paypal with balance 200
const bitcoin = new Bitcoin(300)    // Bitcoin with balance 300

bank.setNext(paypal)
paypal.setNext(bitcoin)

// Начнём с банка
bank.pay(259)

// Cannot pay using bank. Proceeding ..
// Cannot pay using paypal. Proceeding ..: 
// Paid 259 using Bitcoin!

Команда

Вы приходите в ресторан и делает заказ официанту. Он перенаправляет вашу Команду шеф-повару, который знает, что и как готовить.

Паттерн Команда инкапсулирует некоторые действия и необходимые для них данные и позволяет отделить Клиента от Получателя.

Пример реализации

Это Получатель, который умеет совершать различные действия:

// Receiver
class Bulb {
    turnOn() {
        console.log('Bulb has been lit')
    }
    
    turnOff() {
        console.log('Darkness!')
    }
}

А вот набор команд:

/*
Command interface :

    execute()
    undo()
    redo()
*/

// Command
class TurnOnCommand {
    constructor(bulb) {
        this.bulb = bulb
    }
    
    execute() {
        this.bulb.turnOn()
    }
    
    undo() {
        this.bulb.turnOff()
    }
    
    redo() {
        this.execute()
    }
}

class TurnOffCommand {
    constructor(bulb) {
        this.bulb = bulb
    }
    
    execute() {
        this.bulb.turnOff()
    }
    
    undo() {
        this.bulb.turnOn()
    }
    
    redo() {
        this.execute()
    }
}

Это код Вызывающего (официанта из примера):

// Invoker
class RemoteControl {
    submit(command) {
        command.execute()
    }
}

А вот пример использования:

const bulb = new Bulb()

const turnOn = new TurnOnCommand(bulb)
const turnOff = new TurnOffCommand(bulb)

const remote = new RemoteControl()
remote.submit(turnOn) // Bulb has been lit!
remote.submit(turnOff) // Darkness!

Итератор

У музыкальных плееров есть кнопки next и previous, которые позволяют последовательно перебирать песни или радиостанции.

Паттерн Итератор предоставляет доступ к элементам объекта, не раскрывая способ их внутреннего представления.

Пример реализации

Радиостанция:

class RadioStation {
    constructor(frequency) {
        this.frequency = frequency    
    }
    
    getFrequency() {
        return this.frequency
    }
}

Итератор:

class StationList {
    constructor(){
        this.stations = []
    }

    addStation(station) {
        this.stations.push(station)
    }
    
    removeStation(toRemove) {
        const toRemoveFrequency = toRemove.getFrequency()
        this.stations = this.stations.filter(station => {
            return station.getFrequency() !== toRemoveFrequency
        })
    }
}

Можно добавлять и удалять станции, а также получать их частоту:

const stationList = new StationList()

stationList.addStation(new RadioStation(89))
stationList.addStation(new RadioStation(101))
stationList.addStation(new RadioStation(102))
stationList.addStation(new RadioStation(103.2))

stationList.stations.forEach(station => console.log(station.getFrequency()))

stationList.removeStation(new RadioStation(89)) // Will remove station 89

Посредник

Когда вы разговариваете с кем-нибудь по мобильному телефону, то между вами есть Посредник – провайдер.

В этом паттерне объект Посредника управляет взаимодействием между двумя другими объектами (Коллегами), уменьшая связь между ними.

Пример реализации

Создадим простой чат, в котором пользователи (Коллеги) могут отправлять сообщения друг другу.

Чат выглядит так:

// Mediator
class ChatRoom {
    showMessage(user, message) {
        const time = new Date()
        const sender = user.getName()

        console.log(time + '[' + sender + ']:' + message)
    }
}

А это сами пользователи:

class User {
    constructor(name, chatMediator) {
        this.name = name
        this.chatMediator = chatMediator
    }
    
    getName() {
        return this.name
    }
    
    send(message) {
        this.chatMediator.showMessage(this, message)
    }
}

Начнем беседу:

const mediator = new ChatRoom()

const john = new User('John Doe', mediator)
const jane = new User('Jane Doe', mediator)

john.send('Hi there!')
jane.send('Hey!')

// Output will be
// Feb 14, 10:58 [John]: Hi there!
// Feb 14, 10:58 [Jane]: Hey!

Хранитель

Некоторые калькуляторы умеют сохранять в памяти последнее выражение, то есть последнее состояние вычислений.

Шаблон Хранитель захватывает текущее состояние объекта и дает возможность восстанавливать его.

Пример реализации

Создадим текстовый редактор с функцией сохранения контента.

Объект Хранителя:

class EditorMemento {
    constructor(content) {
        this._content = content
    }
    
    getContent() {
        return this._content
    }
}

Сам редактор:

class Editor {
    constructor(){
        this._content = ''
    }
    
    type(words) {
        this._content = this._content + ' ' + words
    }
    
    getContent() {
        return this._content
    }
    
    save() {
        return new EditorMemento(this._content)
    }
    
    restore(memento) {
        this._content = memento.getContent()
    }
}

Теперь смело можете писать курсовую, все данные сохранятся!

const editor = new Editor()

// что-нибудь пишем
editor.type('This is the first sentence.')
editor.type('This is second.')

// сохраняем состояние
const saved = editor.save()

// пишем что-нибудь еще
editor.type('And this is third.')

// контент до сохранения
console.log(editor.getContent())// This is the first sentence. This is second. And this is third.

// восстановление последнего состояния
editor.restore(saved)

console.log(editor.getContent()) // This is the first sentence. This is second.

Наблюдатель

Этот паттерн также известен как шаблон публикации-подписки.

На сайтах поиска работы вы можете подписаться на интересные вам параметры вакансий. Когда подходящее предложение появляется, сайт отправляет вам уведомление.

Паттерн Наблюдатель позволяет оповещать всех заинтересованных объектов о произошедших изменениях.

Пример реализации

Соискатели хотят получать уведомления:

const JobPost = title => ({
    title: title
})

class JobSeeker {
    constructor(name) {
        this._name = name
    }

    notify(jobPost) {
        console.log(this._name, 'has been notified of a new posting :', jobPost.title)
    }
}

А Доска объявлений может эти уведомления рассылать:

class JobBoard {
    constructor() {
        this._subscribers = []
    }

    subscribe(jobSeeker) {
        this._subscribers.push(jobSeeker)
    }

    addJob(jobPosting) {
        this._subscribers.forEach(subscriber => {
            subscriber.notify(jobPosting)
        })
    }
}

Подпишемся на рассылку:

// создаем подписчиков
const jonDoe = new JobSeeker('John Doe')
const janeDoe = new JobSeeker('Jane Doe')
const kaneDoe = new JobSeeker('Kane Doe')

// создаем доску объявлений
// подписываем соискателей
const jobBoard = new JobBoard()
jobBoard.subscribe(jonDoe)
jobBoard.subscribe(janeDoe)

// оповещаем подписчиков о новой вакансии
jobBoard.addJob(JobPost('Software Engineer'))

// John Doe has been notified of a new posting : Software Engineer
// Jane Doe has been notified of a new posting : Software Engineer

Посетитель

Чтобы отправиться за границу, нужно получить разрешение (визу). Но оказавшись в стране, вы можете спокойно посещать самые разные места, не спрашивая дополнительного разрешения. Достаточно лишь узнать о них.

Паттерн Посетитель позволяет добавлять объектам дополнительные операции, не изменяя их исходный код.

Пример реализации

Смоделируем зоопарк с разными видами животных:

class Monkey {
    shout() {
        console.log('Ooh oo aa aa!')
    }

    accept(operation) {
        operation.visitMonkey(this)
    }
}

class Lion {
    roar() {
        console.log('Roaaar!')
    }
    
    accept(operation) {
        operation.visitLion(this)
    }
}

class Dolphin {
    speak() {
        console.log('Tuut tuttu tuutt!')
    }
    
    accept(operation) {
        operation.visitDolphin(this)
    }
}

Теперь мы хотим послушать, какие звуки они издают. Для этого создадим Посетителя:

const speak = {
    visitMonkey(monkey){
        monkey.shout()
    },
    visitLion(lion){
        lion.roar()
    },
    visitDolphin(dolphin){
        dolphin.speak()
    }
}

Он просто обращается к каждому классу и вызывает нужный метод:

const monkey = new Monkey()
const lion = new Lion()
const dolphin = new Dolphin()

monkey.accept(speak)    // Ooh oo aa aa!    
lion.accept(speak)      // Roaaar!
dolphin.accept(speak)   // Tuut tutt tuutt!

Посетитель позволяет не изменять существующие объекты. С его помощью можно, например, добавить всем этим животным возможность прыгать без создания дополнительных методов.

const jump = {
    visitMonkey(monkey) {
        console.log('Jumped 20 feet high! on to the tree!')
    },
    visitLion(lion) {
        console.log('Jumped 7 feet! Back on the ground!')
    },
    visitDolphin(dolphin) {
        console.log('Walked on water a little and disappeared')
    }
}

Вуаля:

monkey.accept(speak)   // Ooh oo aa aa!
monkey.accept(jump)    // Jumped 20 feet high! on to the tree!

lion.accept(speak)     // Roaaar!
lion.accept(jump)      // Jumped 7 feet! Back on the ground! 

dolphin.accept(speak)  // Tuut tutt tuutt! 
dolphin.accept(jump)   // Walked on water a little and disappeared

Стратегия

Для упорядочивания некоторого набора данных вы используете алгоритм пузырьковой сортировки. Она отлично справляется с небольшими объемами, но тормозит с крупными. У быстрой сортировки противоположная проблема. Тогда вы решаете изменять алгоритм в зависимости от размера набора. Это ваша Стратегия.

Шаблон Стратегия позволяет переключать используемый алгоритм в зависимости от ситуации.

Пример реализации

Воплотить Стратегию в JavaScript помогут функции первого класса.

const bubbleSort = dataset => {
    console.log('Sorting with bubble sort')
    // ...
    // ...
    return dataset
}

const quickSort = dataset => {
    console.log('Sorting with quick sort')
    // ...
    // ...
    return dataset
}

А это клиент, который может использовать любую стратегию:

const sorter = dataset => {
    if(dataset.length > 5){
        return quickSort
    } else {
        return bubbleSort
    }
}

Теперь можно сортировать массивы:

const longDataSet = [1, 5, 4, 3, 2, 8]
const shortDataSet = [1, 5, 4]

const sorter1 = sorter(longDataSet)
const sorter2 = sorter(shortDataSet)

sorter1(longDataSet) // Sorting with quick sort
sorter2(shortDataSet) // Sorting with bubble sort

Состояние

Вы рисуете в Paint. В зависимости от вашего выбора кисть меняет свое состояние: рисует красным, синим или любым другим цветом.

Паттерн Состояние позволяет изменять поведение класса при изменении состояния.

Пример реализации

Создадим текстовый редактор, в котором можно менять состояние текста – жирный, курсив и т. д.

Это функции преобразования:

const upperCase = inputString => inputString.toUpperCase()
const lowerCase = inputString => inputString.toLowerCase()
const defaultTransform = inputString => inputString

А вот и сам редактор:

class TextEditor {
    constructor(transform) {
        this._transform = transform
    }
    
    setTransform(transform) {
        this._transform = transform
    }
    
    type(words) {
        console.log(this._transform(words))
    }
}

Можно работать:

const editor = new TextEditor(defaultTransform)

editor.type('First line')

editor.setTransform(upperCase)

editor.type('Second line')
editor.type('Third line')

editor.setTransform(lowerCase)

editor.type('Fourth line')
editor.type('Fifth line')

// First line
// SECOND LINE
// THIRD LINE
// fourth line
// fifth line

Шаблонный метод

Вы строите дом по определенному плану: сначала фундамент, затем стены и только потом крыша. Порядок этих шагов изменить нельзя, но их реализация может быть разной.

Шаблонный метод определяет "скелет" алгоритма, но делегирует реализацию шагов дочерним классам.

Пример реализации

Создадим инструмент для тестирования, сборки и разворачивания приложения.

Базовый класс определяет скелет алгоритма сборки:

class Builder {
    // Template method 
    build() {
        this.test()
        this.lint()
        this.assemble()
        this.deploy()
    }
}

А дочерние классы – конкретную реализацию каждого шага:

class AndroidBuilder extends Builder {
    test() {
        console.log('Running android tests')
    }
    
    lint() {
        console.log('Linting the android code')
    }
    
    assemble() {
        console.log('Assembling the android build')
    }
    
    deploy() {
        console.log('Deploying android build to server')
    }
}

class IosBuilder extends Builder {
    test() {
        console.log('Running ios tests')
    }
    
    lint() {
        console.log('Linting the ios code')
    }
    
    assemble() {
        console.log('Assembling the ios build')
    }
    
    deploy() {
        console.log('Deploying ios build to server')
    }
}

Соберем проект:

const androidBuilder = new AndroidBuilder()
androidBuilder.build()

// Running android tests
// Linting the android code
// Assembling the android build
// Deploying android build to server

const iosBuilder = new IosBuilder()
iosBuilder.build()

// Running ios tests
// Linting the ios code
// Assembling the ios build
// Deploying ios build to server

Перевод статьи Design Patterns for Humans! JavaScript Edition

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...