本书的原著为:《Design Patterns for Embedded Systems in C ——An Embedded Software Engineering Toolkit 》,讲解的是嵌入式系统设计模式,是一本不可多得的好书。
本系列描述我对书中内容的理解。本文章描述访问硬件的设计模式之四:观察者模式。
在面向对象编程中,观察者模式
(Observer Pattern)是一种常见的设计模式。它定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,其所有依赖者(观察者)都会收到通知并自动更新。
具体来说,观察者模式包含两个主要角色:
- 主题(Subject):主题是一个接口,该接口定义了一个注册观察者的方法、一个移除观察者的方法以及一个当主题状态改变时通知所有观察者的方法。主题通常维护一个观察者列表,用于存储注册的观察者对象。
- 观察者(Observer):观察者也是一个接口,该接口定义了一个更新方法,用于在主题状态改变时接收通知并更新自己。具体的观察者类需要实现这个接口,并在更新方法中定义自己的响应逻辑。
观察者模式的主要用途是实现对象之间的解耦。通过将对象之间的依赖关系从硬编码的方式转变为动态的方式,观察者模式使得系统的可扩展性和可维护性大大提高。当一个对象的状态发生改变时,它不再需要直接通知所有依赖它的对象,而是只需要通知它的观察者列表中的对象即可。
举个简单的例子来说明观察者模式的应用:假设有一个天气预报系统,其中包含一个天气数据类(主题)和多个显示天气信息的类(观察者)。当天气数据发生变化时,天气数据类会通知所有注册的观察者更新显示内容。这样一来,无论是添加新的显示类还是修改现有的显示类,都不会影响到天气数据类和其他显示类之间的依赖关系。这就是观察者模式的魅力所在。
观察者模式
是软件设计中最常用的模式之一。它提供了一种机制,使得对象能够“监听”或订阅其他对象的状态变化,而无需对数据服务器(主题)进行任何修改。在嵌入式领域中,这意味着传感器数据可以轻松共享给那些在编写传感器代理代码时甚至可能还不存在的元素。
具体来说,在嵌入式系统中,传感器负责收集和提供关于环境或系统状态的信息。传统上,每个需要使用这些信息的组件或模块都需要直接与传感器进行交互,这可能会导致代码之间的紧密耦合和难以维护的问题。
然而,通过使用观察者模式,我们可以将传感器数据与其使用者解耦。传感器可以作为 主题
(Subject),而需要使用传感器数据的组件或模块则可以注册为 观察者
(Observer)。当传感器数据发生变化时,所有注册的观察者都会收到 通知
,并可以根据需要使用这些数据。
这种方式的优点在于,它允许我们在不修改传感器代码的情况下动态地添加或删除观察者。这意味着我们可以轻松地添加新的组件或模块来使用传感器数据,而无需对现有代码进行大量修改。此外,由于观察者模式支持一对多的通信方式,因此多个组件或模块可以同时接收并使用同一份传感器数据。
摘要
观察者模式(也被称为“发布-订阅模式”)是一种设计模式,它让一组感兴趣的客户端(即观察者)能够订阅某个主题(或称为数据服务器)。当该主题的相关数据发生变化时,这些已订阅的客户端会收到通知。此模式的关键之处在于,主题无需事先了解任何关于其客户端的信息。相反,客户端通过主题提供的订阅功能,可以动态地将自己添加到通知列表中,并同样可以从列表中移除。
数据服务器可以根据具体需求执行各种通知策略。最常见的情况是,每当新数据到达时,都会立即通知已订阅的客户端。然而,通知也可以基于特定的策略进行,例如定期更新、按照设定的最小或最大频率进行更新等。这种方式显著降低了客户端的计算负担,因为它们无需持续检查数据是否已更新;相反,它们会在数据实际更新时收到通知,从而实现了更高效的数据处理和资源利用。
在实际应用中,观察者模式广泛应用于各种场景,如用户界面中的事件处理(如按钮点击、文本输入等),股票交易系统中的价格更新通知,或者是实时天气信息更新等。通过这种模式,系统可以更加灵活、可扩展,并且能够更好地处理变化。
问题
在简单情况中,客户端可能会定期从数据服务器请求数据,以检查数据是否发生了变化。然而,这种做法存在计算和通信资源的浪费问题,因为客户端通常无法准确知道何时有新数据可用,导致频繁的无效查询。另一方面,如果数据服务器主动推送数据给客户端,那么服务器就必须维护所有客户端的信息,这违背了客户端-服务器关系的基本原则,即服务器应该无需了解客户端的具体细节。每当添加新客户端时,都需要对服务器进行修改,这显然是不切实际的。
观察者模式通过引入订阅和解除订阅服务来有效地解决这个问题。在这种模式下,客户端可以动态地将自己添加到数据服务器的通知列表中,而无需服务器预先了解客户端的信息。这样一来,服务器就可以在数据发生变化时,根据其维护的通知列表向感兴趣的客户端发送更新通知。此外,观察者模式还允许动态修改订阅者列表,无论是添加新订阅者还是移除不再感兴趣的订阅者,都可以在不修改服务器代码的情况下轻松实现。这为软件设计带来了极大的灵活性和可扩展性。
模式结构
观察者模式的基本结构图如下所示。
抽象主题接口
(AbstractSubject) 扮演着主题(数据服务器)的角色,并且它内部维护了一个列表,用于记录所有对其数据感兴趣的订阅者。具体观察者
(客户端)通过调用 subscribe
函数,向数据服务器传递一个指向 accept(Datum)
函数的指针来将自己添加到通知列表中,同样地,客户端也可以通过传递相同的指针调用 unsubscribe
方法来移除自己。
当主题决定通知其客户端有新数据可用时,它会调用 notify()
函数。这个函数会遍历订阅者列表,对每个订阅者调用它们提供的 accept(Datum)
函数,并传递相关的数据。
另一方面,抽象观察者接口
提供了一个 accept(Datum)
函数的实现框架,用于接收和处理从主题 (数据服务器) 传入的数据。具体观察者
类将继承 抽象观察者接口
,并实现这个函数以处理特定类型的数据。
在这个结构中,抽象主题接口
和 抽象观察者接口
都是抽象类,它们定义了观察者模式所需的基本接口和行为。具体主题
和具体观察者
类将继承这些抽象类,并实现特定的数据处理和通知逻辑,以满足应用程序的具体需求。这种设计方式提高了代码的模块化和可重用性,使得观察者模式能够在各种不同的上下文中灵活应用。
模式详情
抽象观察者接口
抽象观察者接口
与 抽象主题接口
相 关联
,在 UML 中,关联表示类与类之间的连接,关联关系使一个类知道另外一个类的属性和方法。这里抽象观察者接口知道抽象主题接口的属性和方法,属于单向关联。这样,抽象观察者接口可以调用抽象主题接口提供的各种服务。
抽象观察者接口包含一个 accept(Datum)
函数声明,用于接收和处理从主题 (数据服务器) 传入的数据。抽象观察者调用 subscribe
函数时,会将 accept
函数作为参数注册到主题的订阅者列表中。当主题有新的数据可用时,会将数据作为参数调用 accept
函数。
每个 具体观察者
负责实现 accept
函数。
除了 accept
函数外,抽象观察者接口还与数据相关联。这通常是通过指针实现的,但也可能是通过变量的形式。“Datum”代表数据的类或结构体。
抽象主题接口
抽象主题接口
在此模式中充当数据服务器的角色,并提供三个核心服务来支持这一模式。
首先,subscribe(acceptPtr)
服务允许 观察者
将自己添加到订阅者列表中。观察者通过传递一个指向 accept
函数的指针来实现这一点。如果成功添加了指向函数的指针,则此服务返回零;如果添加失败(例如,由于指针无效或已存在于列表中),则返回非零值。
其次,unsubscribe(acceptPtr)
服务从订阅者列表中移除指定的观察者。这同样是通过传递指向 accept
函数的指针来完成的。如果成功移除了指针,服务返回零;如果移除失败(例如,由于指针不存在于列表中),则返回非零值。
最后,notify()
函数是抽象主题接口用来通知已订阅观察者数据变化的关键方法。它遍历订阅者列表,并对列表中的每个项调用相应的函数(即之前通过 subscribe
服务添加的指针所指向的函数)。这种机制确保了所有订阅了数据变化的观察者都能及时收到更新通知。
此外,抽象主题接口还持有要传递给观察者的数据(即 Datum)以及订阅者列表(即 NotificationHandle)。在图中,订阅者列表使用指针数组实现,但它也可以是链表或者其它数据结构。
具体观察者
具体观察者
是抽象观察者接口的具体实现。它实现了接口中定义的 acceptPtr(Datum)
函数,该函数用于处理从主题中接收到的数据。通过创建多个具体观察者,我们可以实现多个具有不同处理逻辑的观察者,从而满足不同的业务需求。
具体主题
具体主题
是抽象主题接口的具体实现。除了实现接口中定义的函数外,它还提供了获取和管理要分发给观察者的数据。这意味着具体主题可能需要与数据源进行交互,例如从硬件设备、传感器或其他系统中读取数据,并将其转化为适合观察者处理的形式。这样看来,具体主题还包括 硬件代理 的功能。
数据结构(Datum)
Datum
是观察者模式中传递的数据单位,它包含了具体主题的状态信息或其他相关数据。Datum 的具体形式取决于应用场景和需求,它可以是简单的数据类型(如整数、浮点数等),也可以是复杂的数据结构(如结构体、类对象等)。具体观察者通过订阅主题来接收 Datum,并根据需要对其进行处理。因此,Datum 的设计和定义对于观察者模式的实现和使用至关重要。
效果
观察者模式是一种高效且灵活的设计模式,它大大简化了将数据分发给一组在设计时可能未知的客户端的过程。通过引入订阅机制,观察者模式允许在运行时动态地管理对特定数据感兴趣的客户端列表。这种动态性不仅提供了极高的运行时灵活性,还维护了基本的客户端-服务器关系。
在这种模式中,客户端无需不断地检查数据是否已更新,而是可以在数据实际发生变化时收到通知。最常见的更新策略是在数据发生变化时立即通知客户端,但也可以根据实际需求实现任何适当的策略。这种按需更新的方式显著提高了计算效率,因为客户端只在真正需要的时候进行更新操作。
实现策略
这个模式中唯一复杂的方面是订阅者列表的管理(订阅和解除订阅)。订阅列表的元素几乎总是一个函数回调,即指向具有正确参数的函数的指针。当然,这个参数随着返回的数据而变化。
对于订阅者列表,最简单的方法是声明一个足够大的数组来容纳所有潜在的观察者。这在具有高度动态性和许多潜在观察者的系统中会浪费内存。另一种方法是将系统构建为链表,这将使用动态内存分配。
相关模式
该模式可以与前面介绍的模式自由混合使用,因为它们的关注点是正交的。例如,在嵌入式系统中,向 硬件代理 或 硬件适配器 添加观察者功能是非常常见的。
实例
见原书。