查看原文
其他

百度工程师教你玩转设计模式(观察者模式)

北极星小组 百度Geek说 2022-09-06


要写好代码,设计模式(Design Pattern)是必不可少的基本功,设计模式是对面向对象设计(Object Oriented Design)中反复出现的问题的一种有效解决方案,本次从比较常见的观察者模式入手(Observer Pattern)。在观察者模式中,存在多个观察者对象依赖(Observer)都依赖同一个目标对象(Subject),当被依赖的目标对象发生变化的时候,会通知所有依赖它的观察者对象,然后各个观察者对象根据自己的需要做出对应的响应。

其主要优点如下:

  • 降低了目标与观察者之间的耦合关系

  • 建立了目标与观察者之间的变化触发机制

其主要缺点如下:

  • 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用

  • 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率


比较抽象不好理解?我们来参考日常功能设计中几个常见的场景。


01

观察者模式在天气预报场景的应用

关注天气预报是我们日常生活中一个比较重要的习惯,不同的角色对于天气的变化由有着不同的反应。例如明天特大暴雨,气象部门考虑的是评估并发布合理的政策指导,教育部门需要评估是否需要停课,应急部门考虑的是如何提前准备应急救援,环卫部门需要准备暴雨后的大量环卫工作,打工人需要考虑如何上下班通勤,也存在一些不受此次暴雨影响地区的人什么也不需要考虑。结合上述观察者模式的介绍,在此场景中,天气属于被各个角色依赖的目标对象(Subject),气象/教育/应急/环卫等部门,打工人,其他地区的人都属于观察者对象(Observer)。目标对象发生变化后,各个观察者对象收到消息后都会作出对应的响应措施。基于以上场景实现的简化版观察者模式类图如下:

  • 目标对象(WeatherSubject):即被观察的目标对象,即本例中的天气,持有一个天气状态的属性state,以及绑定观察者的方法attach(),观察者的方法notifyAllObservers()。当状态发生变更后,调用notifyAllObservers()方法通知所有观察者。

  • 抽象观察者对象(Observer):用于定义各个观察者的行为规范,也可以采用接口的方式实现

  • 实际观察者对象:MeteorologicalDepartment、RescueDepartment、OfficeWorker、Other等,实际需要关注目标对象并作出响应的角色。继承自抽象观察者对象,并基于各自的关注点实现了对于的响应方法update()。

基于以上示例的Java版代码demo试下:

package com;import java.util.ArrayList;import java.util.List;// 基于天气预报场景的观察者模式实现Demopublic class ObserverPatternDemo { // 天气对象 static class WeatherSubject { private int state; List<Observer> observers = new ArrayList<>(); void attach(Observer observer) { observers.add(observer); } // 通知观察者 void notifyAllObservers() { for (Observer o : observers ) { o.update(getState()); } } public int getState() { return state; } // 状态变更 public void setState(int state) { this.state = state; if (state == 1) { System.out.println("=====明天要下大暴雨====="); } else { System.out.println("=====明天天气很好呀====="); } // 变更后通知观察者 notifyAllObservers(); } } // 抽象观察者对象 static abstract class Observer { abstract void update(int state); } // 政府气象部门 static class MeteorologicalDepartment extends Observer { @Override void update(int state) { if (state == 1) { System.out.println("【气象部门】发出预警"); } } } // 政府应急救援部门 static class ResumeDepartment extends Observer { @Override void update(int state) { if (state == 1) { System.out.println("【救援部门】准备应急预案"); } } } // 打工人 static class OfficeWorker extends Observer { @Override void update(int state) { if (state == 1) { System.out.println("【打工人】思考明天怎么上下班通勤"); } else { System.out.println("【打工人】努力搬砖"); } } } // 其他无影响的人 static class Other extends Observer { @Override void update(int state) { if (state == 1) { System.out.println("【其他人】下雨啊,对我影响不大"); } else { System.out.println("【其他人】明天天气不错,出去玩玩"); } } } public static void main(String[] args) { // 初始化目标对象 WeatherSubject subject = new WeatherSubject(); // 初始化观察者对象,并关注目标对象 Observer ob1 = new MeteorologicalDepartment(); Observer ob2 = new ResumeDepartment(); Observer ob3 = new OfficeWorker(); Observer ob4 = new Other(); subject.attach(ob1); subject.attach(ob2); subject.attach(ob3); subject.attach(ob4); // 状态变化: 大暴雨 subject.setState(1); // 状态变化: 好天气 subject.setState(2); //执行结果如下 =====明天要下大暴雨===== 【气象部门】发出预警 【救援部门】准备应急预案 【打工人】思考明天怎么上下班通勤 【其他人】下雨啊,对我影响不大 =====明天天气很好呀===== 【打工人】努力搬砖 【其他人】明天天气不错,出去玩玩 }}

02

观察者模式在支付场景中的应用

在支付业务场景下,用户购买一件商品,当支付成功之后三方会回调自身,在这个时候系统可能会有很多需要执行的逻辑(如:更新订单状态,发送短信通知,通知物流系统开始备货,赠送礼品…)。

通常最直观的处理方式,会创建对应支付系统需要依赖的类(Order、SMS、Express...)以及支付类Pay,在支付主逻辑中实例化各依赖类,当用户支付成功后,逐个调用各依赖类处理逻辑。但这样支付类要了解需要通知哪些类,且支付主逻辑臃肿耦合也较重,不便于扩展和维护。

观察者模式则可以更好的处理这种支付场景,这些支付系统所依赖的类逻辑之间并没有强耦合,因此适合使用观察者模式去实现这些功能,对与支付类来说不关心需要通知哪些类,只需要提供通知列表,当有更多的操作时,只需要向通知列表中添加新的观察者即可,用户支付成功通知所有注册的观察者。实现了对修改关闭,对扩展开放的开闭原则。


具体实现上通常包括以下几部分:

  • 抽象出被观察者Observable类,抽象出共有的属性和方法;

  • 创建具体被观察者Pay类,只要用户支付成功,它就要去通知所有注册的观察者;

  • 抽象出观察者接口Observer,它包含了一个更新自己的抽象方法update,当接到具体主题Pay的更改通知时被调用;

  • 创建订单、短信、物流等具体观察者类,它们要观察支付状态,得到Pay的更改通知时更新自身的状态;

// 抽象主题(Subject)角色public abstract class Observable { // 观察者列表 private List<Observer> observers = new ArrayList<>(); // 添加观察者 public void add(Observer observer){ observers.add(observer); } // 移除观察者 public void remove(Observer observer){ observers.remove(observer); } // 通知观察者 protected void notifyObservers(){ for (Observer observer : observers) { observer.update(); } }}// 具体主题(Concrete Subject)角色public class Pay extends Observable{ public void pay(){ System.out.println("支付完成."); // 通知观察者 super.notifyObservers(); }}// 抽象观察者(Observer)角色public interface Observer { // 通知 void update();}// 具体观察者(Concrete Observer)角色// 订单:修改订单状态public class Order implements Observer{ @Override public void update() { System.out.println("修改订单状态..."); }}// 短信:发送扣款短信到用户public class SMS implements Observer{ @Override public void update() { System.out.println("账户扣款短信通知..."); }}//物流:通知物流系统开始备货public class Express implements Observer{ @Override public void update() { System.out.println("物流开始备货..."); }}// 客户端调用public class Client { public static void main(String[] args) { Pay pay = new Pay(); pay.add(new Order()); pay.add(new SMS()); pay.add(new Express()); pay.pay(); }}

03

观察者模式在数据订阅场景的应用

在实际应用中,数据推送的场景下,经常会用到观察者模式。以小说资源为例,新的章节生产完成后,需要该数据的业务很多,如搜索、网盘、贴吧、小度等,各方接收数据的方式各异。

对于每个订阅方,会实现截然不同的订阅方法,可能是推送http接口、kafka队列、afs文件系统等。可以分别实现这些订阅方法,并进行注册,当出现消息发布时,依次触发订阅者方法。

以下是PHP语言的实现demo:

interface Observer { public function push($data);}class Publish { private $observers = array(); public function register(Observer $observer) { $this->observers[] = $observer; } public function delete(Observer $observer) { $index = array_search($observer, $this->observers); if ($index !== FALSE && array_key_exists($index, $this->observers)) { unset($this->_observers[$index]); } } public function push($data) { foreach ($this->observers as $observer) { $observer->push($data); } }}class Search implements Observer { public function push($data) { //推送afs }}class Cloud implements Observer { public function push($data) { //推送kafaka }}$publish = new Publish();$publish->register(new Search());$publish->register(new Cloud());$publish->push($data);


04

       总     结       

通过对以上三个实际案例的讲解和具体的代码实现阅读,大家对观察者模式的应用场景和具体实现方案应该有了更加深入的了解了。结合以上三个案例的的分析,适合观察者模式的场景都有以下典型特征:

  • 存在多对一的依赖关系:即多个观察者依赖同一个目标对象

  • 存在目标变更触发机制:目标变更后,需要触发一系列的其他任务

通过观察者机制来实现以上场景,可以实现目标类和观察者类的解耦,即目标对象无需知道需要通知哪些观察者,方便后续的扩展与维护。

----------  END  ----------


推荐阅读【技术加油站】系列:

百度工程师教你玩转设计模式(单例模式)

百度工程师教你快速提升研发效率小技巧


一键三连,好运连连,bug不见👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存