Меню

Паттерн Наблюдатель (Observer) в JavaScript

25.06.2018 - java script, NodeJS, ЯП

Observer Pattern

Что делает этот паттерн?
Наблюдатель подписывается на объект (литерал, функция, экземпляр класса). У объекта может быть одно или несколько полей. Некоторые наблюдатели, которые могут быть классами или просто функциями хотят знать когда происходят изменения в этих полях. Объект уведомляет все наблюдатели, когда изменение происходит.

Посмотрим на примере.

Давайте добавим классу Car принимать наблюдателей.
Добавим в конструктор массив, предназначенный для наблюдателей.
Дальше добавим метод subscribeSpeedObserver который будет принимать наблюдателей. У наблюдателя должен быть специальный метод notify на наличие которого мы будем проверять при добавлении наблюдателей к списку.

Еще должна быть возможность отписаться от наблюдения с помощью метода unsubscribeSpeedObserver.
Последний метод — тот, который уведомляет все наблюдатели. Это цикл, который проходит по всем наблюдателям и вызывает их метод notify. Также этот метод должен принимать и новое значение и старое значение. Это хорошая практика для наблюдателей. Так как нам нужно вызывать этот метод каждый раз когда скорость меняется, именно поэтому мы устанавливаем скорость через сеттер.

При установке скорости мы проверяем, отличается ли текущая скорость от предыдущей. Если да, то вызываем уведомитель подписчиков.

Теперь дело в шляпе. У нас есть классы, которые имеют подходящие методы для наблюдения. Мы создаем их экземпляр и просто подписываем их к нашему car экземпляру.

//app.js
class Car {
    constructor() {
        this._currentSpeed = 0;
        this.speedObservers = [];
    }

    subscribeSpeedObserver(observer) {
        if (observer.notify) {
            this.speedObservers.push(observer);
        } else {
            throw new Error("Invalid observer. notify implementation missing");
        }
    }

    unsubscribeSpeedObserver(observer) {
        let index = this.speedObservers.indexOf(observer);
        if (~index) {
            this.speedObservers.splice(index, 0);
        }
    }

    notifySpeedObserver(newVal, oldVal) {
        for (let observer of this.speedObservers) {
            observer.notify(newVal, oldVal);
        }
    }

    get currentSpeed() {
        return this._currentSpeed;
    }

    set currentSpeed(value) {
        let oldVal = this._currentSpeed;
        this._currentSpeed = value;
        if (this._currentSpeed != oldVal) {
            this.notifySpeedObserver(this._currentSpeed, oldVal);
        }
    }
}

class CurrentSpeedConsoleObserver {
    notify(newVal, oldVal) {
        console.log(`Current Speed changed from ${oldVal} to ${newVal}`);
    }
}

class DOMCarSpeedObserver {
    constructor(selector) {
        this.textField = document.querySelector(selector);
    }

    notify(newVal, oldVal) {
        this.textField.textContent = newVal;
    }
}

let car = new Car();
let consoleObserver = new CurrentSpeedConsoleObserver();
let domObserver = new DOMCarSpeedObserver('#speedometer');
car.subscribeSpeedObserver(domObserver);
car.subscribeSpeedObserver(consoleObserver);
car.currentSpeed += 10;
car.currentSpeed += 10;
car.currentSpeed += 10;
car.currentSpeed += 10;
let interval = setInterval(() => {
    car.currentSpeed += 10
}, 2000)

setTimeout(() => {
    clearInterval(interval);
}, 10000);
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <span>Car running at <span id="speedometer"></span> mph</span>
    <script src="./bundle.min.js"></script>
</body>
</html>
Метки: , , ,

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *