观察者模式(发布订阅模式)
定义
-
观察者模式
一个或多个观察者对目标的状态感兴趣,通过将自己依附在目标对象上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,他们可以简单将自己从中分离。
-
发布订阅模式
定义相同,就是发布订阅模式有个事件调度中心。
区别
a.png从图中可以看出,观察者模式中观察者和目标直接进行交互,而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。
简单示例
-
观察者模式
// 观察者 class Observer { constructor() { } update(val) { } } // 观察者列表 class ObserverList { constructor() { this.observerList = [] } add(observer) { return this.observerList.push(observer); } remove(observer) { this.observerList = this.observerList.filter(ob => ob !== observer); } count() { return this.observerList.length; } get(index) { return this.observerList[index]; } } // 目标 class Subject { constructor() { this.observers = new ObserverList(); } addObserver(observer) { this.observers.add(observer); } removeObserver(observer) { this.observers.remove(observer); } notify(...args) { let obCount = this.observers.count(); for (let index = 0; index < obCount; index++) { this.observers.get(i).update(...args); } } }
-
发布订阅模式
class PubSub { constructor() { this.subscribers = {} } subscribe(type, fn) { if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) { this.subscribers[type] = []; } this.subscribers[type].push(fn); } unsubscribe(type, fn) { let listeners = this.subscribers[type]; if (!listeners || !listeners.length) return; this.subscribers[type] = listeners.filter(v => v !== fn); } publish(type, ...args) { let listeners = this.subscribers[type]; if (!listeners || !listeners.length) return; listeners.forEach(fn => fn(...args)); } } let ob = new PubSub(); ob.subscribe('add', (val) => console.log(val)); ob.publish('add', 1);
在观察者模式中,观察者是知道Subject的,Subject也一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
WEB开发应用应用场景
-
购票流程开发中,当购票完成后,需要记录文本日志,发送短信,赠送积分等等活动,传统是冗余在一个模块中。
存在的问题就是一旦某个业务逻辑发生改变,如购票业务中增加其他业务逻辑,需要修改购票核心文件、甚至购票流程。日积月累后,文件冗长,导致后续维护困难。
为了解决这种机密耦合的编程方式,使用观察模式将目前的业务逻辑优化成"松耦合",达到易维护、易修改的目的
#===================定义观察者、被观察者接口============ /** * 观察者接口(通知接口) */ interface ITicketObserver //观察者接口 { function onBuyTicketOver($sender, $args); //得到通知后调用的方法 } /** * 主题接口 */ interface ITicketObservable //被观察对象接口 { function addObserver($observer); //提供注册观察者方法 } #====================主题类实现======================== /** * 主题类(购票) */ class HipiaoBuy implements ITicketObservable { //实现主题接口(被观察者) private $_observers = array (); //通知数组(观察者) public function buyTicket($ticket) //购票核心类,处理购票流程 { // TODO购票逻辑 //循环通知,调用其onBuyTicketOver实现不同业务逻辑 foreach ( $this->_observersas $obs ) $obs->onBuyTicketOver ( $this, $ticket ); //$this 可用来获取主题类句柄,在通知中使用 } //添加通知 public function addObserver($observer) //添加N个通知 { $this->_observers [] = $observer; } } #=========================定义多个通知==================== //短信日志通知 class HipiaoMSM implements ITicketObserver { public function onBuyTicketOver($sender, $ticket) { echo (date ( 'Y-m-d H:i:s' ) . " 短信日志记录:购票成功:$ticket<br>"); } } //文本日志通知 class HipiaoTxt implements ITicketObserver { public function onBuyTicketOver($sender, $ticket) { echo (date ( 'Y-m-d H:i:s' ) . " 文本日志记录:购票成功:$ticket<br>"); } } //抵扣卷赠送通知 class HipiaoDiKou implements ITicketObserver { public function onBuyTicketOver($sender, $ticket) { echo (date ( 'Y-m-d H:i:s' ) . " 赠送抵扣卷:购票成功:$ticket赠送10元抵扣卷1张。<br>"); } } #============================用户购票==================== $buy = new HipiaoBuy (); $buy->addObserver ( new HipiaoMSM () ); //根据不同业务逻辑加入各种通知 $buy->addObserver ( new HipiaoTxt () ); $buy->addObserver ( new HipiaoDiKou () ); //购票 $buy->buyTicket ( "一排一号" );
总结
-
优点
1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
-
缺点
1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、如果某些观察者的响应方法被阻塞,整个通知过程即被阻塞,其它观察者不能及时被通知(异步?)
命令模式
定义
在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事物”等处理,这种无法抵御变化的紧耦合是不合适的。将一组行为抽象为对象,实现二者之间的松耦合,这就是命令模式。
在命令的发布者和接收者之间,定义一个命令对象,命令对象暴露出一个统一的接口给命令的发布者,而命令的发布者不用去管接收者是如何执行命令的,做到命令发布者和接收者的解耦。
简单示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>cmd-demo</title>
</head>
<body>
<div>
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
<button id="btn3">按钮3</button>
</div>
<script>
var btn1 = document.getElementById('btn1')
var btn2 = document.getElementById('btn2')
var btn3 = document.getElementById('btn3')
// 定义一个命令发布者(执行者)的类
class Executor {
setCommand(btn, command) {
btn.onclick = function() {
command.execute()
}
}
}
// 定义一个命令接收者
class Menu {
refresh() {
console.log('刷新菜单')
}
addSubMenu() {
console.log('增加子菜单')
}
}
// 定义一个刷新菜单的命令对象的类
class RefreshMenu {
constructor(receiver) {
// 命令对象与接收者关联
this.receiver = receiver
}
// 暴露出统一的接口给命令发布者Executor
execute() {
this.receiver.refresh()
}
}
// 定义一个增加子菜单的命令对象的类
class AddSubMenu {
constructor(receiver) {
// 命令对象与接收者关联
this.receiver = receiver
}
// 暴露出统一的接口给命令发布者Executor
execute() {
this.receiver.addSubMenu()
}
}
var menu = new Menu()
var executor = new Executor()
var refreshMenu = new RefreshMenu(menu)
// 给按钮1添加刷新功能
executor.setCommand(btn1, refreshMenu)
var addSubMenu = new AddSubMenu(menu)
// 给按钮2添加增加子菜单功能
executor.setCommand(btn2, addSubMenu)
// 如果想给按钮3增加删除菜单的功能,就继续增加删除菜单的命令对象和接收者的具体删除方法,而不必修改命令对象
</script>
</body>
</html>
WEB开发应用示例
总结
命令模式的主要优点如下。
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
其缺点是:可能产生大量具体命令类。因为计对每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。