(This is a study note based on the Observer model of Head First Design Pattern.)

Example: Designing weather stations

We recently received an order to build an application on WeatherData objects. The requirements are as follows: the application needs to have three bulletin boards showing currentConditions, meteorological statistics and simple forecasts, and must be updated in real time.

Also, it had to be a scalable weather station, and the owner wanted to publish a set of apis so that other developers could write their own weather bulletin boards and plug them into the app. I hope our company can provide such API.

Template source file

Party A provided the template source file on the second day:

class WeatherData {
  // Get temperature, humidity, pressure. We don't need to know the exact acquisition steps
  // Just call it directly.
  getTemperature() {}
  getHumidity() {}
  getPressure() {}

  // Call this method once the meteorological measurement changes.
  measurementChanged() {}
  // ...
}
Copy the code

Our main task is to implement measurementChanged() so that it updates current conditions, weather statistics, and weather forecast display boards.

Task analysis

  1. WeatherDataClasses have a variety ofgetterMethod, three measurements can be obtained: temperature, humidity and pressure.
  2. When the data changes,measurementChanged()The method will be called. (this has to do withvueAlso, we don’t care how the method was called, we only care that it was called.
  3. We need to implement three bulletin boards using weather data: the “current status” bulletin board, the “Weather Statistics” bulletin board, and the “Weather Forecast” bulletin board. Once theWeatherDataThese bulletin boards are updated as data changes.
  4. The system must be scalable. Other developers can create custom boards, and users can add or remove any boards they want.

The wrong sample

Before we get started, let’s look at a bad example:

class WeatherData {
  // ...
  measurementChanged() {
    const temp = this.getTemperature()
    const humidity = this.getHumidity()
    const pressure = this.getPressure()

    // Update the bulletin board, which will be implemented later
    currentConditionsDisplay.update(temp, humidity, pressure)
    statisticsDisplay.update(temp, humidity, pressure)
    forecastDisplay.update(temp, humidity, pressure)
  }
}
Copy the code

Potential problems:

  1. Highly coupled.

    Obviously, we aremeasurementChanged()The use of thexxxDisplay. Programming for a specific implementation will cause us to have to modify the program later to add or remove bulletin boards.
  2. Lack of encapsulation.

    For the bulletin board, we call itupdateMethod, which, obviously, we can unify into an interface with the argumenttemp, humidity, pressure.

To solve this problem, we can use the observer pattern.

Observer model

Before we get to the observer model, let’s look at an example from life — newspaper and magazine subscriptions.

  1. We subscribe to a newspaper. As soon as they have a new paper, they send it to us. As long as we are their customers, they will keep sending us newspapers.
  2. When we don’t want to receive any more newspapers, we can cancel the subscription and the newspaper will not send any new ones.
  3. As long as the newspaper is still running, new users/units will subscribe/unsubscribe to them.

As you can see from the graph above, when users are inversed with a publisher’s newspaper, they are notifyObservers of new ones. Those who do not subscribe will not be notified by the publisher.

In fact, the observer is such a principle. If we change the publisher to Subject and the subscriber/user to Observer. This is our observer model.

As we can see from the figure above, a topic corresponds to multiple objects. That is, the observer pattern defines a one-to-many relationship.

define

Observer pattern: Determines one-to-many dependencies between objects. When an object’s state changes, all of its dependencies are notified and updated.

model

In observer mode, a Subject can add an addObserver, delete an observer, and notify all observers. For our Observer objects, we should provide an update function that updates the subject as it changes.

Analogies to life examples. A newspaper is a Subject, and its users are observers. The newspaper can add users, delete users and notify users, and ignore users who do not subscribe. On the user side, a user can subscribe to or unsubscribe from a newspaper.

Let’s look at the abstract code for observer mode:

interface Observer {
  update(/*some states*/) :any
}

interface Subject {
  observers: Observer[]

  addObserver(observer: Observer): void
  deleteObserver(observer: Observer): void

  notifyObservers(): void
}
Copy the code

The specific implementation

class ConcreteSubject implements Subject {
  private readonly observers: Observer[] = []
  private state: number = 1

  addObserver(observer: Observer): void {
    this.observers.push(observer)
  }

  deleteObserver(observer: Observer): void {
    const index = this.observers.indexOf(observer)
    if (index >= 0) {
      for (let i = index, len = this.observers.length - 1; i < len; i++) {
        this.observers[i] = this.observers[i + 1]}this.observers.pop()
    }
  }

  notifyObservers(): void {
    this.observers.forEach((observer) = > observer.update())
  }

  getState(): number {
    return this.state
  }
  setState(state: number) {
    if (this.state ! == state) {this.state = state
      this.notifyObservers()
    }
  }
}

class ConcreteObserver implements Observer {
  constructor(private subject: Subject, privatename? :string) {
    subject.addObserver(this)}update() {
    console.log(`The ${this.name} update data/state.`)}deregister() {
    this.subject.deleteObserver(this)}}Copy the code
  1. Topics implement concrete interfaces.
  2. The topic is updating its status (setState), notifies the observer (notifyObservers).
  3. At creation time, the observer subscribes to the topic (callsubject.addObserver).
  4. Observer implementationupdateFunction. As soon as the topic state changes, the observer can receive the change.

Simply test:

const subject = new ConcreteSubject()
const observer1 = new ConcreteObserver(subject, 'observer1')
const observer2 = new ConcreteObserver(subject, 'observer2')
const observer3 = new ConcreteObserver(subject, 'observer3')
subject.setState(100)

console.log('-- -- -- -- -- -- -- -- -- --')
observer2.deregister()
subject.setState(10)
// observer1 update data/state.
// observer2 update data/state.
// observer3 update data/state.
// ----------
// observer1 update data/state.
// observer3 update data/state.
Copy the code

In the example above, the Observer does not get the state of the Subject. In fact, we can get the state of the subject in the observer.

interface Observer {
  // ...

  // Get the topic state. All you need is for the topic to call the observer update method,
  // Pass in the state of the topic.
  update(state: any) :void
}
Copy the code

Loose coupling

The Observer pattern provides an object design that loosens the coupling between the subject and the observer.

A Subject only knows that an Observer implements an interface (the Observer interface above). The topic does not need to know what the observer’s specific class is or what it does.

Since the only dependency of a topic is a list of interface objects that implement an Observer, we can always add an Observer (addObserver).

In addition, we can replace the existing observer with a new observer at run time and the theme will not be affected. Similarly, we can delete observers anywhere.

More importantly, we can reuse topics or observers independently. If we need a topic/observer somewhere else, we can easily reuse it because there is very little coupling between the two.

The realization of weather stations

We have now fulfilled our previous mission to the weather station. You need to implement a theme (WeatherData), three observers (CurrentConditionsDisplay StatisticsDisplay, ForecastDisplay)

Theme implementation

class WeatherData implements Subject {
  private _temperature: number = 0
  private _humidity: number = 0
  private _pressure: number = 0
  private _observers: Observer[] = []

  getTemperature() {
    return this._temperature
  }

  getHumidity() {
    return this._humidity
  }

  getPressure() {
    return this._pressure
  }

  // To implement interface adaptation
  / / ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇
  measurementChanged() {
    this.notifyObservers()
  }

  setMeasurements(temperature: number, humidity: number, pressure: number) {
    this._temperature = temperature
    this._humidity = humidity
    this._pressure = pressure
    this.measurementChanged()
  }

  addObserver(observer: Observer): void {
    this._observers.push(observer)
  }

  deleteObserver(observer: Observer): void {
    const index = this._observers.indexOf(observer)
    if (index >= 0) {
      for (let i = index, len = this._observers.length - 1; i < len; i++) {
        this._observers[i] = this._observers[i + 1]}this._observers.pop()
    }
  }

  notifyObservers(): void {
    this._observers.forEach((observer) = >
      observer.update(this._temperature, this._humidity, this._pressure)
    )
  }
}
Copy the code

The point to note here is that our implementation of measurementChanged is implemented through notifyObservers delegation. This is mainly to be consistent with the requirements of Party A.

Implementation of observer

interface DisplayElement {
  display(): void
}
class CurrentConditionsDisplay implements Observer.DisplayElement {
  private_temperature! :number
  private_humidity! :number
  // private subject: Subject

  constructor(private subject: Subject) {
    subject.addObserver(this)
  }

  display(): void {
    if (
      typeof this._temperature ! = ='undefined' &&
      typeof this._humidity ! = ='undefined'
    ) {
      console.log(
        `Current conditions: The ${this._temperature}F degrees and The ${this._humidity}% humidity`)}}update(temperature: number, humidity: number, pressure: number) {
    this._temperature = temperature
    this._humidity = humidity
    this.display()
  }
}

class StatisticsDisplay implements Observer.DisplayElement {
  private _temperatures: number[] = []
  // private subject: Subject

  constructor(private subject: Subject) {
    subject.addObserver(this)
  }

  display(): void {
    let min: string | number = 0
    let max: string | number = 0
    let avg: string | number = 0
    if (this._temperatures.length === 0) {
      min = 'unknown'
      max = 'unknown'
      avg = 'unknown'
    }
    this._temperatures.sort((a, b) = > a - b)
    min = this._temperatures[0]
    max = this._temperatures[this._temperatures.length - 1]
    avg =
      this._temperatures.reduce((prev, cur) = > prev + cur, 0) /
      this._temperatures.length

    console.log(
      `Avg/Max/Min temperature = ${avg.toFixed(2)}/${min.toFixed(
        2
      )}/${max.toFixed(2)}`)}update(temperature: number, humidity: number, pressure: number) {
    this._temperatures.push(temperature)
    this.display()
  }
}

class ForecastDisplay implements Observer.DisplayElement {
  // private _temperatures! : number
  // private _humidity! : number
  // private _pressure! : number
  // private subject: Subject

  constructor(private subject: Subject) {
    subject.addObserver(this)
  }

  display(): void {
    console.log('Forecast: Improving weather on the way! ')}update(temperature: number, humidity: number, pressure: number) {
    this.display()
  }
}
Copy the code

In addition to implementing the Observer interface, we also implemented the DisplayElement interface for easy presentation.

Simply test

function weatherStation() {
  const weatherData = new WeatherData()
  const currentConditionsDisplay = new CurrentConditionsDisplay(weatherData)
  const statisticsDisplay = new StatisticsDisplay(weatherData)
  const forecastDisplay = new ForecastDisplay(weatherData)
  console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --')
  weatherData.setMeasurements(80.65.30.4)
  console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --')
  weatherData.setMeasurements(82.70.29.2)
  console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --')
  weatherData.setMeasurements(78.90.29.2)
}

weatherStation()
// -------------------------------------------
// Current conditions: 80F degrees and 65% humidity
// Avg/Max/Min temperature = 80.00/80.00/80.00
// Forecast: Improving weather on the way!
// -------------------------------------------
// Current conditions: 82F degrees and 70% humidity
// Avg/Max/Min temperature = 81.00/80.00/82.00
// Forecast: Improving weather on the way!
// -------------------------------------------
// Current conditions: 78F degrees and 90% humidity
// Avg/Max/Min temperature = 80.00/78.00/82.00
// Forecast: Improving weather on the way!
Copy the code

Look from the output results: when we execute. WeatherData setMeasurements, our bulletin board will perform the update operation, this is what we want.

Observer mode in VUE

Vue implements dependency collection through themes and page updates through observers.

Since themes need to collect dependencies, VUE uses a clever design: a single JS thread adds dependencies (i.e., our observer) to a static property of the theme. So we can see that in the theme code.

class Dep {
  static target
  // others...
}
Copy the code

The second is the timing of collecting dependencies. For each piece of data, VUE needs to do a dependency collection. Such as:

const data: {
  msg'hello vue'.person: {
    name: ’foo’,
    age18}},Object.keys(data).forEach((key) = > {
  defineReactive(data, key, data[key])
})

function defineReactive(data, key, value) {
  / / ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇
  // Set themes/collect dependencies
  const dep = new Dep()
  // Value can be an object
  observe(value)
  Object.defineProperty(data, key, {
    get() {
      / / ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇
      dep.addSubscriber(Dep.target)
      return value
    },
    set(newValue) {
      // console.log('observer: key = ', key)
      if (value === newValue) {
        return
      }
      value = newValue
      // newValue may be an object
      observe(newValue)
      / / ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇
      dep.notifySubscribers()
    }
  })
}
Copy the code

When iterating through data, each copy is a topic (or dependency). The time to add an observer is to use that data. So we can see in get(), dep.addsubscriber ().

Again, the time to notify the observer is when the data is updated. So we can see dep.NotifySubscribers () in set().

Take data.msg for example. When we set data.msg to reactive, we create a theme for it (const dep = new dep ()).

Second, we need to add an observer to the data.msg topic, and the time to add an observer is when we use data.msg. Using data.msg triggers its get method. So we do the dependency collection inside the GET method (dep.addsubscriber (dep.target)).

However, the question here is how do we determine which observers (dependencies) we need to add? In fact, we need to identify the observer before using the data.msg get method. Vue uses the single-threaded nature of JS to attach itself to dep.target when using dependencies in the observer. Then, when the data. MSG get method is executed, dep. target is the observer (dependency) we want. Hence the following code in the observer:

class Watcher {
  // ...

  get() {
    // Before using dependencies, attach yourself to dep.target.
    Dep.target = this
    / / ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇
    // Use dependencies
    const newValue = Watcher.compute(this.expr, this.scope)

    // Dependencies are collected and uninstalled.
    Dep.target = null
    return newValue
  }
}
Copy the code

The specific code is as follows:

defineReactive

/** * sets the object to reactive. *@param {Object} data
 * @param {*} key
 * @param {*} value* /
export function defineReactive(data, key, value) {
  const dep = new Dep()
  // Value can be an object
  observe(value)
  Object.defineProperty(data, key, {
    configurable: false.enumerable: true.get() {
      dep.addSubscriber(Dep.target)
      return value
    },
    set(newValue) {
      // console.log('observer: key = ', key)
      if (value === newValue) {
        return
      }
      value = newValue
      // newValue may be an object
      observe(newValue)
      dep.notifySubscribers()
    }
  })
}
Copy the code

The observer

let uuid = 0
export class Watcher {
  /** ** observer *@param {string} expr expression
   * @param {Object} scope
   * @param {Function | undefined} cb callback
   */
  constructor(expr, scope, cb) {
    this.expr = expr
    this.scope = scope
    this.cb = cb
    this.uuid = uuid++
    this.update()
  }

  /** * get the corresponding data according to the expression. * /
  get() {
    Dep.target = this
    const newValue = Watcher.compute(this.expr, this.scope)
    Dep.target = null
    return newValue
  }

  update() {
    let newValue = this.get()
    if (isFunction(this.cb)) {
      this.cb(newValue)
    }
    // console.log(newValue)
  }

  /** * evaluates the expression. *@param {string} expr expression
   * @param { Object } Scope scope. *@returns {*}* /
  static compute(expr, scope) {
    try {
      const func = new Function('scope'.'with(scope){return ' + expr + '} ')
      return func(scope)
    } catch (e) {
      console.error('watcher: ', e)
    }
  }
}
Copy the code

The theme

export class Dep {
  / * *@type{ Watcher } * /
  static target

  constructor() {
    / * *@type{Map<number, Watcher>} * /
    // Using Map is similar to using [].
    this.subscribers = new Map()}/** * Add observer. *@param { Watcher } subscriber* /
  addSubscriber(subscriber) {
    if(! subscriber) {return
    }
    this.subscribers.set(subscriber.uuid, subscriber)
  }

  /** * notify the observer. * /
  notifySubscribers() {
    this.subscribers.forEach((watcher) = > {
      watcher.update()
    })
  }
}
Copy the code

summary

  • The observer pattern defines a one-to-many relationship between objects.
  • Topics update observers with a common interface (Observer.update).
  • Loose-coupling is used between observers and multiobservers. The observed does not know the details of the observer, only that the observer implements the corresponding interface.

Design principles used in the Observer pattern;

  1. Change and unchange. In observer mode, what changes is the subject (Subject) (state), and the number of observers (observers). In this mode, we can change objects that depend on the state of the topic without modifying the topic.
  2. Interface oriented, not implementation oriented. Both topics and observers use interfaces to register with topics (addObserver), and the topic uses the observer interface to notify the observer (Observer.update), which allows the two to work properly while still being loosely coupled.
  3. Use composition more than inheritance. The observer model uses composition to combine many observers into a topic (observers), relationships between objects are generated not by inheritance but by composition at run time.