观察者模式(发布订阅模式)

定义

  • 观察者模式

    一个或多个观察者对目标的状态感兴趣,通过将自己依附在目标对象上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,他们可以简单将自己从中分离。

  • 发布订阅模式

    定义相同,就是发布订阅模式有个事件调度中心。

区别

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开发应用示例

总结

命令模式的主要优点如下。

  1. 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
  2. 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  3. 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  4. 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

其缺点是:可能产生大量具体命令类。因为计对每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容