观察者模式是一种非常流行的设计模式,也常被叫作订阅-发布模式。观察者模式在现代的软件开发中应用非常广泛,比如,商品系统、物流系统、监控系统、运营数据分析系统等。
现在我们常说的基于事件驱动的架构,其实也是观察者模式的一种最佳实践。当我们观察某一个对象时,对象传递出的每一个行为都被看成是一个事件,观察者通过处理每一个事件来完成自身的操作处理。
一、模式原理分析
观察者模式的原始定义是:定义对象之间的一对多依赖关系,这样当一个对象改变状态时,它的所有依赖项都会自动得到通知和更新。
这个定义中包含了两个前提条件:一是被依赖的对象叫作被观察者,依赖的对象叫作观察者;二是观察者观察被观察者的状态变化。不过,这听上去还是有点太抽象,实际上我们更容易理解下面这些不同的对于观察者模式的叫法:
-
发布者-订阅者;
-
生产者-消费者;
-
事件发布-事件监听。
不管怎么叫,这些模式在本质上都是观察者模式。我们通过 UML 图来看看观察者模式中对象之间的关系:
从该 UML 图中,我们能看出观察者模式包含的四个关键角色。
-
发布者(Publisher):也被叫作主题、被订阅者、被观察者等,通常是指观察者关心的相关对象集合,比如,将 GitLab 上的 Git 库作为发布者,我们关心 Git 库的代码发生了变更后做特定的操作。
-
具体发布者(PublisherImpl):实现了发布者定义的方法的具体实现类。
-
订阅者(Observer):也叫作观察者,它会存储一个注册列表,用于存放订阅者。当发布者发布消息或事件时,会通知到订阅者进行处理。
-
具体订阅者(ObserverImpl):实现具体定义的方法操作。
接下来,再来看看该 UML 对应的代码实现,这是一种比较经典的实现方式。具体代码如下所示:
public interface Publisher {
void addObserver(Observer o);
void removeObserver(Observer o);
void notify(double amt);
}
public class PublisherImpl implements Publisher {
private String acct;
private double balance;
private List<Observer> myObservers;
public PublisherImpl(String anAcct, double aBalance) {
acct = anAcct;
balance = aBalance;
myObservers = new ArrayList();
}
public void addObserver(Observer o){
myObservers.add(o);
}
public void removeObserver(Observer o) {
myObservers.remove(o);
}
public void notify(double amt) {
balance -= amt;
if(balance < 0) {
overdrawn();
}
}
private void overdrawn() {
for (Observer o: myObservers) {
o.notify(acct, balance);
}
}
}
public interface Observer {
void notify(String acct, double amt);
}
public class ObserverImpl implements Observer {
@Override
public void notify(String acct, double amt) {
System.out.println("=== 接收到通知:账户:"+acct + " 账单:"+amt);
}
}
public class Demo {
public static void main(String[] args) {
Publisher account = new PublisherImpl("TEST123", 10.00);
Observer bill = new ObserverImpl();
account.addObserver(bill);
account.notify(11.00);
}
}
//输出结果
=== 接收到通知:账户:TEST123 账单:-1.0
在上面的代码实现中,具体发布者会动态维护一个订阅者的列表:可在运行时根据程序需要开始或停止发布给对应订阅者的事件通知。实际上,发布者本身并不维护订阅列表,它会将工作委派给具体发布者;订阅者在接收到发布者的消息后,会委派具体的订阅者来进行相关的处理。
二、使用场景分析
观察者模式常见的使用场景有以下几种。
-
当一个对象状态的改变需要改变其他对象时。比如,商品库存数量发生变化时,需要通知商品详情页、购物车等系统改变数量。
-
一个对象发生改变时只想要发送通知,而不需要知道接收者是谁。比如,订阅微信公众号的文章,发送者通过公众号发送,订阅者并不知道哪些用户订阅了公众号。
-
需要创建一种链式触发机制时。比如,在系统中创建一个触发链,A 对象的行为将影响 B 对象,B 对象的行为将影响 C 对象……这样通过观察者模式能够很好地实现。
-
微博或微信朋友圈发送的场景。这是观察者模式的典型应用场景,一个人发微博或朋友圈,只要是关联的朋友都会收到通知;一旦取消关注,此人以后将不会收到相关通知。
-
需要建立基于事件触发的场景。比如,基于 Java UI 的编程,所有键盘和鼠标事件都由它的侦听器对象和指定函数处理。当用户单击鼠标时,订阅鼠标单击事件的函数将被调用,并将所有上下文数据作为方法参数传递给它。
为了帮助你更好地理解观察者模式的使用,下面我们还是通过一个简单的例子来帮助你理解——实现一个简易的消息订阅通知功能。
我们首先定义观察者 MessageObserver。它只有一个 update 方法,当有消息 Message 发送过来时就会调用该方法。
public interface MessageObserver {
void update(Message m);
}
接着再定义消息的具体数据结构,这里使用一个 content 消息内容。
public class Message {
final String content;
public Message (String m) {
this.content = m;
}
public String getContent() {
return content;
}
}
再接下来,实现 Subject 的具体发布者类 MessagePublisher,它持有一组观察者 MessageObserver 实例,可以通过 attach 和 detach 接口方法新增和删除观察者。
public class MessagePublisher implements Subject {
private List<MessageObserver> observers = new ArrayList<>();
@Override
public void attach(MessageObserver o) {
observers.add(o);
}
@Override
public void detach(MessageObserver o) {
observers.remove(o);
}
@Override
public void notifyUpdate(Message m) {
observers.forEach(x->x.update(m));
}
}
最后,我们实现三个具体订阅者类,它们都实现了 MessageObserver 接口,分别代表不同的消息接收后的对应处理操作。
public class MessageSubscriber1 implements MessageObserver {
@Override
public void update(Message m) {
System.out.println("MessageSubscriber1 :: " + m.getContent());
}
}
public class MessageSubscriber2 implements MessageObserver {
@Override
public void update(Message m) {
System.out.println("MessageSubscriber2 :: " + m.getContent());
}
}
public class MessageSubscriber3 implements MessageObserver {
@Override
public void update(Message m) {
System.out.println("MessageSubscriber3 :: " + m.getContent());
}
}
设计完成后,我们来运行一段单元测试,看看具体运行结果。
public class Client {
public static void main(String[] args) {
MessageObserver s1 = new MessageSubscriber1();
MessageObserver s2 = new MessageSubscriber2();
MessageObserver s3 = new MessageSubscriber3();
Subject p = new MessagePublisher();
p.attach(s1);//
p.attach(s2);
p.notifyUpdate(new Message("First Message")); //s1和s2会收到消息通知
p.detach(s1);
p.attach(s3);
p.notifyUpdate(new Message("Second Message")); //s2和s3会收到消息通知
}
}
//输出结果
MessageSubscriber1 :: First Message
MessageSubscriber2 :: First Message
MessageSubscriber2 :: Second Message
MessageSubscriber3 :: Second Message
上面的代码实现非常简单,但是充分体现了观察者模式的使用场景。观察者模式使用场景的特点在于找到合适的被观察者,定义一个通知列表,将需要通知的对象放到这个通知列表中,当被观察者需要发起通知时,就会通知这个列表中的所有“人”。这和现实中我们打开收音机收听电台很类似。
三、为什么使用观察者模式?
分析完观察者模式的原理和使用场景后,我们再来说说使用观察者模式的原因,主要有以下两个。
第一个,为了方便捕获观察对象的变化并及时做出相应的操作。 观察者模式对于对象自身属性或行为发生变化后,需要快速应对的应用场景尤其有效,比如,购物包裹的运送。因为商品变成包裹的形式后会出现各种各样的状态变化,比如,配送中、接收中、退货中等,这些状态变化非常快,而每一个状态都会对应一系列连锁的操作,这时使用观察者模式就非常方便,能够通知需要知道这些状态变化的系统,并做相应的处理,比如,物流监控、性能监控、运营系统等。
第二个,为了提升代码扩展性。 如果不使用观察者模式来捕获一个被观察对象的属性变化,那么就需要在被观察对象执行代码逻辑中加入调用通知某个对象进行变更的逻辑,这样不仅增加了代码的耦合性,也让代码扩展变得非常困难,因为要想新增一个新的观察对象,就需要修改被观察对象的代码,这样的扩展性非常低。相反,使用观察者模式则只需要将观察者对象注册到被观察者存储的观察者列表中就能完成代码扩展。
四、观察者模式的优缺点是什么?
通过上述分析,我们也可以得出使用观察者模式主要有以下优点。
-
能够降低系统与系统之间的耦合性。 比如,建立以事件驱动的系统,我们能创建更多观察者与被观察者,对象之间相互的关系比较清晰,可以随时独立添加或删除观察者。
-
提升代码扩展性。 由于观察者和被观察之间是抽象耦合,所以增删具体类并不影响抽象类之间的关系,这样不仅满足开闭原则,也满足里氏替换原则,能够很好地提升代码扩展性。
-
可以建立一套基于目标对象特定操作或数据的触发机制。 比如,基于消息的通知系统、性能监控系统、用户数据跟踪系统等,观察者通过特定的事件触发或捕获特定的操作来完成一系列的操作。
当然,观察者模式也有一些缺点。
-
增加代码的理解难度。 由于使用组合关系,被观察者和观察者之间属于松散关系,所以在代码逻辑的理解上,就需要提前搞清楚上下文中有哪些中间类或调用逻辑关系,这样才能正确理解逻辑。
-
降低了系统性能。 观察者模式通常需要事件触发,当观察者对象越多时,被观察者需要通知观察者所花费的时间也会越长,这样会在某种程度上影响程序的效率。
虽然观察者模式在原理上是一个比较抽象的模式,但是业界根据不同的应用场景和需求总结出了很多不同的实现方式,这让观察者模式变得易于使用。
在观察者模式中,被观察者通常会维护一个观察者列表。当被观察者的状态发生改变时,就会通知观察者。观察者模式现在被大量应用在分布式系统的开发中。
从本质上来讲,观察者模式描述了如何建立对象与对象之间一种简单的依赖关系。在现实中,最为常见的观察者模式的例子就是订阅微信公众号,当公众号发布一篇新文章时,我们能在微信上收到消息,然后选择合适的时间打开阅读。
因此,当我们在使用观察者模式时,要重点关注哪些变化是我们需要从被观察对象那里捕获(观察到)并进行下一步处理的,一方面不能盲目捕获所有的变化,另一方面也不能遗漏重要的变化。换句话说,找到合适的变化并进行正确的处理才是使用观察者模式的正确打开方式。
文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发。
个人小工具程序上线啦,通过公众号(服务端技术精选)菜单【个人工具】即可体验,欢迎大家体验后提出优化意见!