设计模式-观察者模式-Observer Pattern
[TOC]
Overview
- 观察者模式(Observer Pattern)是一种行为型设计模式
- 它定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都会得到通知并自动更新
1.观察者模式(Observer Pattern)
观察者模式(Observer Pattern)是一种行为型设计模式,它定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。这种模式通常用于实现分布式事件处理系统。
在C++中实现观察者模式,通常需要定义观察者(Observer)和被观察者(Subject)的接口。以下是观察者模式的一个简单实现示例:
|
|
在这个例子中,Subject
类维护了一个观察者列表,并提供了 attach
和 detach
方法来添加或移除观察者。当被观察者的状态发生变化时,通过 notify
方法通知所有观察者。
ConcreteSubject
是具体的被观察者,它继承自 Subject
类,并实现了 setState
方法来改变状态,并通知观察者。
Observer
是观察者的基类,它定义了一个 update
方法,该方法将被所有具体观察者实现。
ConcreteObserver
是具体的观察者,它实现了 update
方法,以响应被观察者状态的变化。
在 main
函数中,我们创建了一个 ConcreteSubject
对象和两个 ConcreteObserver
对象,并将观察者附加到被观察者上。然后,我们改变了被观察者的状态,这导致所有观察者都得到了通知。之后,我们从被观察者中移除了一个观察者,并再次改变了状态,以演示观察者列表的更新。
观察者模式的优点包括实现了观察者和被观察者之间的解耦,增加了程序的可扩展性,并且可以动态地添加或移除观察者。缺点是如果观察者非常多,通知的开销可能会很大,而且如果被观察者的状态变化非常频繁,可能会导致性能问题。
2.观察者模式优缺点
- 优点
- 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
- 你可以在运行时建立对象之间的联系。
- 缺点
- 订阅者的通知顺序是随机的。
观察者模式(Observer Pattern)的优缺点如下:
2.1.优点
解耦:观察者模式能够将被观察者(Subject)和观察者(Observer)解耦,使得被观察者的改变不会直接影响到观察者,两者之间的依赖关系降低。
扩展性:新的观察者可以在不修改被观察者代码的情况下被添加到系统中,提高了系统的可扩展性。
灵活性:观察者模式允许观察者对象对被观察者的状态变化做出反应,这使得系统可以灵活地响应状态变化。
广播通信:观察者模式支持广播通信,即被观察者可以同时通知多个观察者对象。
可定制响应:不同的观察者可以对相同的事件做出不同的响应,增加了系统的灵活性。
松散关联:观察者和被观察者之间的松散关联关系使得在系统中添加新的观察者或被观察者变得容易,而不需要修改现有的代码。
2.2.缺点
循环依赖:在实现观察者模式时,如果不当心,可能会导致循环依赖的问题,特别是当观察者和被观察者相互持有对方的引用时。
内存泄漏:如果观察者和被观察者之间的引用关系没有正确管理,可能会导致内存泄漏。例如,如果被观察者持有观察者的强引用,而观察者又没有正确地从被观察者列表中移除自己,可能会导致无法释放内存。
性能开销:当有大量的观察者时,通知所有观察者可能会带来性能开销,尤其是在被观察者状态变化频繁的情况下。
顺序依赖:在某些情况下,观察者的更新顺序可能会影响系统的正确性,这需要在设计时仔细考虑。
过度使用:在一些简单的场景中,过度使用观察者模式可能会增加不必要的复杂性。
错误处理:在观察者模式中,如果一个观察者在更新时发生错误,可能会影响到其他观察者的更新,这需要在设计时考虑错误处理机制。
通知的一致性:确保所有观察者都能接收到通知,并且按照预期的顺序接收通知,可能是一个挑战。
在使用观察者模式时,应该权衡这些优缺点,并根据具体的应用场景和需求来决定是否采用这种模式。
3.观察者模式在实际开发中有哪些常见的应用场景?
观察者模式在实际开发中有许多应用场景,它主要用于实现事件处理和通知机制。以下是一些典型的应用实例:
事件处理系统:在图形用户界面(GUI)编程中,观察者模式用于处理用户的行为,如点击、滚动、按键等事件。当用户执行这些操作时,系统会通知所有注册的观察者(如按钮、文本框等)。
游戏开发:在游戏开发中,观察者模式可以用来实现游戏状态的监听和响应。例如,当玩家的分数发生变化时,游戏可以通知得分牌、排行榜等观察者对象。
模型-视图-控制器(MVC)框架:在MVC架构中,观察者模式用于实现视图(View)和控制器(Controller)对模型(Model)的依赖关系。当模型数据发生变化时,自动更新视图。
软件架构中的发布-订阅系统:在大型软件系统中,观察者模式可以实现跨组件的通信。组件可以注册为观察者,订阅感兴趣的事件,当事件发生时,系统会通知所有订阅者。
数据绑定:在现代编程框架中,观察者模式用于实现数据绑定。当数据模型发生变化时,自动更新界面元素,如输入框、列表等。
通知系统:在需要实现通知和消息传递的系统中,观察者模式可以用来通知用户或系统组件关于状态变化的信息,如电子邮件客户端中的新邮件通知。
股票价格监控:在金融应用中,观察者模式可以用来监控股票价格的变化。当股票价格达到特定阈值时,系统可以通知投资者或执行自动交易。
缓存系统:在缓存系统中,观察者模式可以用来监听数据的变更,当数据更新时,缓存可以同步更新或失效。
实时协作编辑:在多人协作编辑文档的应用程序中,观察者模式可以用来同步不同用户对文档的更改。
硬件驱动程序:在操作系统中,观察者模式可以用来监听硬件状态的变化,如电池电量、网络连接状态等,并在状态变化时通知应用程序。
观察者模式的优点在于它提供了一种松耦合的方式来实现组件之间的通信,使得系统更加灵活和可维护。然而,它也可能导致系统复杂度增加,特别是在有大量观察者和事件时,管理和维护这些关系可能会变得复杂。
3.在实现观察者模式时,如何确保被观察者和观察者之间的通信是线程安全的?
在实现观察者模式时,确保线程安全是至关重要的,特别是在多线程环境中。以下是一些确保线程安全的策略:
使用同步方法:在被观察者(Subject)中,添加、删除观察者(Observer)和通知观察者的方法可以使用
synchronized
关键字或在C++中使用互斥锁(如std::mutex
)来同步,确保同一时间只有一个线程可以执行这些操作。这种方法简单易行,但在高并发环境下可能会成为性能瓶颈。使用读写锁:如果读操作远多于写操作,可以使用读写锁(如C++中的
std::shared_mutex
)来允许多个线程同时读取观察者列表,但在修改观察者列表时需要独占访问。这样可以在读多写少的场景下提高性能。使用线程安全容器:一些编程语言提供了线程安全的容器,如Java的
ConcurrentHashMap
或C++的std::vector
(通过适当的同步机制)。使用这些容器来存储观察者列表可以减少自己管理同步的复杂性。避免在通知过程中修改观察者列表:在通知观察者的过程中,避免添加或删除观察者,因为这可能会导致死锁或不一致的状态。可以在通知之前创建观察者列表的副本,然后对副本进行迭代。
使用条件变量:在某些情况下,可以使用条件变量(如C++中的
std::condition_variable
)来协调观察者和被观察者之间的线程。例如,当被观察者状态发生变化时,可以使用条件变量来通知等待的观察者。使用不可变对象:如果可能,使观察者传递的状态不可变,这样即使在多线程环境中,也不需要额外的同步措施来保护状态。
使用锁分离技术:对于复杂的数据结构,可以使用锁分离技术,为数据结构的不同部分使用不同的锁,以减少锁竞争。
使用原子操作:对于简单的状态更新,可以使用原子操作来确保操作的原子性,而不需要使用重量级的锁。
每种方法都有其适用场景和潜在的性能影响。在实际应用中,应根据具体的并发需求和性能目标来选择合适的线程安全策略。
4.在实现观察者模式时,如何确保被观察者和观察者之间的通信是线程安全的?
在实现观察者模式时,确保线程安全是至关重要的,特别是在多线程环境中。以下是一些确保线程安全的策略:
使用同步方法:在被观察者(Subject)中,添加、删除观察者(Observer)和通知观察者的方法可以使用
synchronized
关键字或在C++中使用互斥锁(如std::mutex
)来同步,确保同一时间只有一个线程可以执行这些操作。这种方法简单易行,但在高并发环境下可能会成为性能瓶颈。使用读写锁:如果读操作远多于写操作,可以使用读写锁(如C++中的
std::shared_mutex
)来允许多个线程同时读取观察者列表,但在修改观察者列表时需要独占访问。这样可以在读多写少的场景下提高性能。使用线程安全容器:一些编程语言提供了线程安全的容器,如Java的
ConcurrentHashMap
或C++的std::vector
(通过适当的同步机制)。使用这些容器来存储观察者列表可以减少自己管理同步的复杂性。避免在通知过程中修改观察者列表:在通知观察者的过程中,避免添加或删除观察者,因为这可能会导致死锁或不一致的状态。可以在通知之前创建观察者列表的副本,然后对副本进行迭代。
使用条件变量:在某些情况下,可以使用条件变量(如C++中的
std::condition_variable
)来协调观察者和被观察者之间的线程。例如,当被观察者状态发生变化时,可以使用条件变量来通知等待的观察者。使用不可变对象:如果可能,使观察者传递的状态不可变,这样即使在多线程环境中,也不需要额外的同步措施来保护状态。
使用锁分离技术:对于复杂的数据结构,可以使用锁分离技术,为数据结构的不同部分使用不同的锁,以减少锁竞争。
使用原子操作:对于简单的状态更新,可以使用原子操作来确保操作的原子性,而不需要使用重量级的锁。
每种方法都有其适用场景和潜在的性能影响。在实际应用中,应根据具体的并发需求和性能目标来选择合适的线程安全策略。
5.在多线程环境下,如何避免观察者模式中的死锁问题?
在多线程环境下实现观察者模式时,要避免死锁问题,可以采取以下措施:
锁顺序:确保所有线程在获取多个锁时都按照相同的顺序进行,这样可以避免循环等待条件,这是死锁的四个必要条件之一 。
锁超时:在尝试获取锁时使用超时机制,如果在指定的时间内无法获取锁,则释放已持有的锁并重试,这有助于避免死锁 。
最小化锁持有时间:尽量减少持有锁的时间,例如,在持有锁的代码块中只执行必要的操作,这样可以减少锁竞争的机会 。
避免在持有锁时调用外部代码:因为外部代码可能会尝试获取其他锁,这可能导致死锁 。
使用不可变备忘录:如果备忘录对象是不可变的,那么它们不需要同步,因为它们的状态在创建后不会改变,这可以减少锁的需求 。
使用线程局部存储:对于备忘录对象,可以使用线程局部存储来确保每个线程都有其自己的副本,从而避免线程间的共享和竞争 。
使用条件变量:在某些情况下,可以使用条件变量(如C++中的
std::condition_variable
)来协调观察者和被观察者之间的线程 。避免死锁的策略:包括一次性分配所有资源、可剥夺资源、资源有序分配法等,这些策略可以破坏死锁产生的四个必要条件中的一个或多个 。
使用死锁检测和解除机制:在系统中实现死锁检测算法,如资源分配图或等待图,定期检查是否存在死锁,并采取恢复措施,如终止线程或资源抢占 。
避免嵌套锁:尽量避免在持有一个锁的情况下去请求另一个锁,因为这种情况很容易导致死锁 。
通过上述措施,可以在多线程环境中有效地使用观察者模式,同时避免死锁问题,从而提高系统的稳定性和可维护性。
关于作者
- 微信公众号:WeSiGJ
- GitHub:https://github.com/wesigj/cplusplusboys
- CSDN:https://blog.csdn.net/wesigj
- 微博:
- 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
