JavaScript 中观察者模式的威力

2025-05-25

JavaScript 中观察者模式的威力

在Medium上找到我

在这篇文章中,我们将介绍观察者模式并使用 JavaScript 实现它,以便您能够更好地理解它,特别是当您难以理解这个概念时。

观察者模式仍然是设计解耦系统的最佳实践之一,并且应该成为任何 JavaScript 开发人员使用的重要工具。

观察者模式是一种设计模式,其中主体(其实就是具有方法的对象)维护着一个“注册”的观察者列表,用于接收即将发送的消息通知。当它们从其所属的主体接收到关于某事的通知事件时,它们可以利用这些机会根据接收到的消息执行一些有用的操作。

这种模式最适用于需要多个对象同时收到最近状态变化通知的情况。因此,当您需要多个对象来维护整个应用的一致性,而不是使用紧密耦合的类时,这种模式的威力就凸显出来了。即便如此,甚至可以让多个彼此不直接相关的对象同时保持一致。

观察者在连接后可以自行移除,因此甚至可以灵活地选择加入或退出一个观察者或下一个观察者,反之亦然。将所有这些功能组合在一起,您就可以在主体和观察者之间建立动态关系,从而实现强大的功能。

这个概念是这样的:

当观察者关注某个主体的状态,并希望“观察”其即将发生的状态更新时,他们可以注册或附加到该主体以接收即将发生的信息。这样,当发生任何变化时,这些观察者将能够收到通知,包括之后的更新。这是通过主体使用某种广播方法向其附加的观察者发送通知消息来实现的。每条通知消息都可以包含对接收它们的一个或多个观察者有用的数据。发送通知消息的方式通常是调用某个通知方法循环遍历其观察者列表,并在每次循环中调用观察者的更新方法。当观察者不再希望与主体关联时,可以将其分离。

下面是一个简短而精确的表格,其中列出了构成此模式的所有常见参与者:

姓名 描述
主题 维护观察员。可以建议添加或删除观察员
观察者 为需要通知 Subject 状态变化的对象提供更新接口
具体主题 当状态发生变化时向观察者广播通知,并存储具体观察者的状态
具体观察者 存储对 ConcreteSubject 的引用,为观察者实现更新接口,以确保状态与 Subject 一致

现在让我们继续看看这在代码中是怎样的。

我们要做的第一件事就是创建一个包含接口的主体,用于管理它的观察者。为此,我们实际上要在一个单独的函数中定义构造函数,该函数名为ObserversList

function ObserversList() {
  this.observers = []
}

ObserversList.prototype.add = function(observer) {
  return this.observers.push(observer)
}

ObserversList.prototype.get = function(index) {
  if (typeof index !== number) {
    console.warn('the index passed in to getObserver is not a number')
    return
  }
  return this.observers[index]
}

ObserversList.prototype.removeAt = function(index) {
  this.observers.splice(index, 1)
}

ObserversList.prototype.count = function() {
  return this.observers.length
}

ObserversList.prototype.indexOf = function(observer, startIndex = 0) {
  let currentIndex = startIndex

  while (currentIndex < this.observers.length) {
    if (this.observers[currentIndex] === observer) {
      return currentIndex
    }
    currentIndex++
  }

  return -1
}

ObserversList.prototype.notifyAll = function(data) {
  const totalObservers = this.observers.length
  for (let index = 0; index < totalObservers; index++) {
    this.observers(index).update(data)
  }
}
Enter fullscreen mode Exit fullscreen mode

然后我们将这个接口直接附加到主题的属性上:

function Subject() {
  this.observers = new ObserversList()
}
Enter fullscreen mode Exit fullscreen mode

我们本来可以直接在主题上定义原型方法,但我们不这样做的原因是,主题通常是现实世界用例中某个事物的任意实例,只需要继承观察者接口,然后可能扩展其功能或围绕它们创建包装器。

现在我们继续定义观察者

function Observer() {
  this.update = function() {}
}
Enter fullscreen mode Exit fullscreen mode

当不同的对象继承Observer时,通常会发生的情况是,它们会覆盖update对它们正在寻找的某些数据感兴趣的函数(或某些更新程序)。

这是因为当主体调用其notifyAll方法时,观察者的更新函数在每个循环上都会使用。

您可以在上方看到这一效果:

ObserversList.prototype.notifyAll = function(data) {
  const totalObservers = this.observers.length
  for (let index = 0; index < totalObservers; index++) {
    // HERE
    this.observers(index).update(data)
  }
}
Enter fullscreen mode Exit fullscreen mode

现实世界的例子

现在让我们来看一个现实世界的例子。

假设我们正在某个地点运营一个车辆管理处Alhambra。我们将使用观察者模式来实现售票系统。

在 DMV 的典型罚单呼叫系统中,如果人们被列入等候名单,他们通常会获得一个罚单号码,然后他们会等到自己的号码被叫到。

在收到罚单号码之前,车管所会先检查是否有空位,然后再把罚单交给他们。如果没有空位,他们就会被放入等候名单,并附上指定的罚单号码。

当一个人在展位完成他们的服务后,我们假设他们当天的服务就结束了。这意味着他们的票号不再可用,以后可以再次使用。在我们的例子中,我们将把票号标记为立即可用,以便分配给其他人,而其他人将被列入候补名单。

我们需要做的第一件事是定义DMV构造函数:

function DMV(maxTicketsToProcess = 5) {
  this.ticketsFree = new Array(40).fill(null).map((_, index) => index + 1)
  this.ticketsProcessing = []
  this.maxTicketsToProcess = maxTicketsToProcess
  this.waitingList = new WaitingList()
}
Enter fullscreen mode Exit fullscreen mode

在我们的例子中,DMV主题,因为它将管理人员和票号的列表。

我们设置了一个maxTicketsToProcess参数,因为如果没有这个参数,候补名单将永远为空,因为我们无法知道何时将某人放入候补名单是合适的。当达到 时,如果 中仍有票,maxTicketsToProcess我们就会开始将票号为 的人员放入候补名单this.ticketsFree

现在,我们看一下DMV构造函数,它正在this.waitingList用一个WaitingList实例进行赋值。这WaitingList基本上是ObserversList因为它提供了一个几乎相同的接口来管理它的人员列表:

function WaitingList() {
  this.waitingList = []
}

WaitingList.prototype.add = function(person) {
  this.waitingList.push(person)
}

WaitingList.prototype.removeAt = function(index) {
  this.waitingList.splice(index, 1)
}

WaitingList.prototype.get = function(index) {
  return this.waitingList[index]
}

WaitingList.prototype.count = function() {
  return this.waitingList.length
}

WaitingList.prototype.indexOf = function(ticketNum, startIndex) {
  let currentIndex = startIndex

  while (currentIndex < this.waitingList.length) {
    const person = this.waitingList[currentIndex]
    if (person.ticketNum === ticketNum) {
      return currentIndex
    }
    currentIndex++
  }
  return -1
}

WaitingList.prototype.broadcastNext = function(ticketNum) {
  const self = this
  this.waitingList.forEach(function(person) {
    person.notifyTicket(ticketNum, function accept() {
      const index = self.waitingList.indexOf(person)
      self.waitingList.removeAt(index)
      delete person.processing
      delete person.ticketNum
      self.ticketsProcessing.push(ticketNum)
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

broadcastNext相当于示例notifyAll中的方法。不过,我们ObserversList不是直接调用,而是调用Person 实例上定义的那个方法(稍后会看到),并提供一个回调函数作为第二个参数,因为这将模拟真实场景:一个人查看自己的票号,意识到自己被分配的号码被叫到,然后走向自己的售票处。.update.notifyTicketaccept

让我们定义一个Person构造函数来为每个人实例化:

function Person(name) {
  this.name = name
}
Enter fullscreen mode Exit fullscreen mode

您可能已经意识到缺少该方法notifyTicket,因为我们在这里使用了它:

person.notifyTicket(ticketNum, function accept() {
Enter fullscreen mode Exit fullscreen mode

这很好,因为我们不想将等待列表的界面与通用People界面混合在一起。

因此,我们将创建一个构造函数,其中包含专门针对候补名单中WaitingListPerson人员的接口,因为我们知道,当人员被移出候补名单后,这些功能将不再有用。这样,我们就能保持代码的条理性和简洁性。

我们将Person通过一个名为的实用程序来扩展实例extend

function extend(target, extensions) {
  for (let ext in extensions) {
    target[ext] = extensions[ext]
  }
}
Enter fullscreen mode Exit fullscreen mode

这是的定义WaitingListPerson

function WaitingListPerson(ticketNum) {
  this.ticketNum = ticketNum

  this.notifyTicket = function(num, accept) {
    if (this.ticketNum === num) {
      accept()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

太棒了!我们要做的最后一件事就是最终实现这些方法,DMV以便它能够真正实现添加/删除人员、管理工单号等功能。

function DMV(maxTicketsToProcess = 5) {
  this.ticketsFree = new Array(40).fill(null).map((_, index) => index + 1)
  this.ticketsProcessing = []
  this.maxTicketsToProcess = maxTicketsToProcess

  this.waitingList = new WaitingList()
}

// Extracts ticket # from this.ticketsFree
// Adds extracted ticket # to this.ticketsProcessing
// Or add to this.waitingList
DMV.prototype.add = function(person) {
  if (this.ticketsProcessing.length < this.maxTicketsToProcess) {
    const ticketNum = this.ticketsFree.shift()
    console.log(`Taking next ticket #${ticketNum}`)
    this.processNext(person, ticketNum)
  } else {
    this.addToWaitingList(person)
  }
}

// Appends "processing" and "ticketNum" to person
// Inserts ticket # to this.ticketsProcessing if holding ticketNum
DMV.prototype.processNext = function(person, ticketNum) {
  person.processing = true
  if (ticketNum !== undefined) {
    person.ticketNum = ticketNum
    this.ticketsProcessing.push(ticketNum)
  }
}

// Extracts ticket # from this.ticketsFree
// Adds extracted ticket # to this.waitingList
DMV.prototype.addToWaitingList = function(person) {
  const ticketNum = this.ticketsFree.splice(0, 1)[0]
  extend(person, new WaitingListPerson(ticketNum))
  this.waitingList.add(person)
}

// Extracts ticket # from this.ticketsProcessing
// Adds extracted ticket to this.ticketsFree
DMV.prototype.complete = function(person) {
  const index = this.ticketsProcessing.indexOf(person.ticketNum)
  this.ticketsProcessing.splice(index, 1)[0]
  this.ticketsFree.push(person.ticketNum)
  delete person.ticketNum
  delete person.processing
  if (this.waitingList.count() > 0) {
    this.waitingList.broadcastNext(this.ticketsFree.shift())
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们有一个足够的 DMV 票务系统,由观察者模式支持!

让我们尝试看看它的用法:

const alhambraDmv = new DMV()

const michael = new Person('michael')
const ellis = new Person('ellis')
const joe = new Person('joe')
const jenny = new Person('jenny')
const clarissa = new Person('clarissa')
const bob = new Person('bob')
const lisa = new Person('lisa')
const crystal = new Person('crystal')

alhambraDmv.add(michael)
alhambraDmv.add(ellis)
alhambraDmv.add(joe)
alhambraDmv.add(jenny)
alhambraDmv.add(clarissa)
alhambraDmv.add(bob)
alhambraDmv.add(lisa)
alhambraDmv.add(crystal)

const ticketsFree = alhambraDmv.ticketsFree
const ticketsProcessing = alhambraDmv.ticketsProcessing

console.log(`waitingNum: ${alhambraDmv.waitingList.count()}`)
console.log(
  `ticketsFree: ${ticketsFree.length ? ticketsFree.map((s) => s) : 0}`,
)
console.log(`ticketsProcessing: ${ticketsProcessing.map((s) => s)}`)

console.log(michael)
console.log(ellis)
console.log(joe)
console.log(jenny)
console.log(clarissa)
console.log(bob)
console.log(lisa)
console.log(crystal)

alhambraDmv.complete(joe)

console.log(`waitingNum: ${alhambraDmv.waitingList.count()}`)
console.log(
  `ticketsFree: ${ticketsFree.length ? ticketsFree.map((s) => s) : 0}`,
)
console.log(`ticketsProcessing: ${ticketsProcessing.map((s) => s)}`)

alhambraDmv.complete(clarissa)

console.log(michael)
console.log(ellis)
console.log(joe)
console.log(jenny)
console.log(clarissa)
console.log(bob)
console.log(lisa)
console.log(crystal)
Enter fullscreen mode Exit fullscreen mode

JavaScript 中的观察者模式

现在我们已经见识了观察者模式能带你的应用走多远。我们已经利用它构建了一个功能齐全的DMV票务查询系统!给自己点个赞吧!

结论

这篇文章到此结束!希望你觉得这篇文章有价值,并期待未来更多精彩内容!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/the-power-of-the-observer-pattern-in-javascript-1ed0
PREV
我在 Amazon Web Services 上被扣了 14,000 美元
NEXT
JavaScript 中代理模式的威力