2020-11-10

观察者模式

 观察者模式,在程序开发中最通俗的解释就是,观察一个对象的行为,当达到预期的某个状态时需要做什么动作。它还有个这名称叫做发布订阅模式,当一个对象改变了状态,会通知与它相关角色做相应的业务。

​ 这里通过一个需求来演示观察者模式的具体设计逻辑,如有一个线上商城项目当商品成交后会立即已短信的形式将商品的订单信息发送至用户手机,用户通过短信得知订单的详细信息。

​ 首先我们通过类图,设计该需求需要哪些类。

未命名文件 (13)

以上类图,我们定义了IMall商城接口它可以执行商品下单及付款的操作,并使用Mall类来实现它。还定义了一个短信接口它可以发送消息,并使用Sms类来实现它。另外Mall还依赖了Sms类,需要使用Sms类来发送消息。

代码示例

  • IMall商城接口代码

    public interface IMall {
        void order();
        void buy();
    }
    
  • Mall商城实现类

    public class Mall implements IMall {
        private ISms sms;
        public Mall(ISms sms) {
            this.sms = sms;
        }
        @Override
        public void order() {
            System.out.println("【商城服务】用户下单");
            sms.sendMsg("用户下单成功");
        }
        @Override
        public void buy() {
            System.out.println("【商城服务】用户支付");
            sms.sendMsg("用户支付成功");
        }
    }
    
  • ISms短信发送接口

    public interface ISms {
        void sendMsg(String msg);
    }
    
  • Sms短信发送具体实现类

    public class Sms implements ISms {
        @Override
        public void sendMsg(String msg) {
            System.out.println("【短信服务】发送订单消息:" + msg);
        }
    }
    
  • 测试类

    public class TestAction {
        public static void main(String[] args) {
            // 定义消息发送器
            ISms sms = new Sms();
            // 定义商城,并依赖短信发送服务
            IMall mall = new Mall(sms);
            // 开始执行下单和支付
            mall.order();
            mall.buy();
        }
    }
    
  • 打印结果

    【商城服务】用户下单
    【短信服务】发送订单消息:用户下单成功
    【商城服务】用户支付
    【短信服务】发送订单消息:用户支付成功
    

​ 通过上述例子我们实现了一个最简单的观察者模式,不难看出我们的程序分为了两个部分,一部分是执行业务的部分,一部分是在业务达到某个状态的时候执行某个动作的。商城类中下单和支付就是我们的业务部分,短信发送就是执行部分。带入到观察者模式中来,短信发送就属于是观察者,他时刻观察着业务执行的动向,当下单成功后就开始执行发送短信的工作了。

多观察者模式

​ 通过上述的代码示例我们简单的了解了观察者模式大概的概念,但在实际的开发过程中,业务往往不会这么单一。比如现在需求又变了。当用户下单后不仅仅需要短信通知还需要同时发送微信消息及邮件消息。

​ 这时我们可能想到的是,在Mall类中有Sms对象的基础上再次增加Email,Wechat两个对象然后通过调用发送消息的方式来进行通知。这样做时可以实现业务需求的,但是在程序开发上严重违反了里氏替换原则、和开闭原则。

​ 实际上我们可以对现有程序做如下修改,先修改类图。

未命名文件 (14)

​ 我们做了如下修改,将原有的短信发送接口再次抽象成了消息发送的接口,增加了邮箱(Email)微信(Wechat)实现类。

​ 以下是具体代码的示例

  • 修改商城类,它拥有多个观察者

    public class Mall implements IMall {
        private List<IMsg> msgs = new ArrayList<>();
        // 增加一个消息发送观察者
        @Override
        public void addMsg(IMsg msg){
            msgs.add(msg);
        }
        // 删除一个消息发送观察者
        @Override
        public void delMsg(IMsg msg){
            msgs.remove(msg);
        }
        // 通知所有的消息观察者,开始发送消息
        @Override
        public void notifyMsgs (String msg){
            for (IMsg iMsg : msgs) {
                iMsg.sendMsg(msg);
            }
        }
        @Override
        public void order() {
            System.out.println("【商城服务】用户下单");
            this.notifyMsgs("用户支付成功");
        }
        @Override
        public void buy() {
            System.out.println("【商城服务】用户支付");
            this.notifyMsgs("用户支付成功");
        }
    }
    
  • 增加Email邮箱类,用来发送邮箱。微信类与之类似

    public class Email implements IMsg {
        @Override
        public void sendMsg(String msg) {
            System.out.println("【邮箱服务】发送订单消息:" + msg);
        }
    }
    
  • 测试类

    public class TestAction {
        public static void main(String[] args) {
            // 定义消息发送器
            IMsg sms = new Sms();
            IMsg email = new Email();
            IMsg wechat = new Wechat();
            // 定义商城,并依赖短信发送服务
            IMall mall = new Mall();
            mall.addMsg(sms);
            mall.addMsg(email);
            mall.addMsg(wechat);
    
            // 开始执行下单和支付
            mall.order();
            mall.buy();
        }
    }
    
  • 打印日志

    【商城服务】用户下单
    【短信服务】发送订单消息:用户支付成功
    【邮箱服务】发送订单消息:用户支付成功
    【微信服务】发送订单消息:用户支付成功
    【商城服务】用户支付
    【短信服务】发送订单消息:用户支付成功
    【邮箱服务】发送订单消息:用户支付成功
    【微信服务】发送订单消息:用户支付成功
    

以上代码我们通过商城类(Mall)中msgs集合的方式来存放多个观察者的容器,并提供了观察者的注册与删除的功能。当要通知观察者只需动作的时候调用notifyMsgs方法,将通知到所有消息观察者中的每一个成员。这与我们所使用的消息中间件(如Rabbit Mq)的设计基本原理是一致的。所以前文所说的观察者模式还有一个更加形象的名称叫做发布订阅模式。例子中商城属于发布者,各个消息服务属于订阅者。

Java的最佳实现

​ 观察者模式使用场景也是非常的多。只要在开发过程中碰到一个业务的某个动作之后会触发很多关联的服务做相应的业务流程的都可以使用观察者模式的结构。实现起来也非常简单。另外介于观察者模式使用场景的固定性,Java从一开始就提供了这种模式可供拓展的父类。即java.util.Observable类。我们可以查看JDK文档看看这个类。

image-20201018233516591

​ 实际上通过以上我们已经写好的例子,再将其改造为使用Java提供的类来实现也是很方便的。

​ 如下面代码

  • 修改商城类,增加实现java的Observable类。

    public class Mall extends Observable implements IMall{
    
        @Override
        public void order() {
            System.out.println("【商城服务】用户下单");
            super.setChanged();
            this.notifyObservers("用户支付成功");
        }
    
        @Override
        public void buy() {
            System.out.println("【商城服务】用户支付");
            super.setChanged();
            this.notifyObservers("用户支付成功");
        }
    }
    
  • 修改各个消息服务类,增加一个Observer实现。

    public class Sms implements Observer, IMsg {
        @Override
        public void sendMsg(String msg) {
            System.out.println("【短信服务】发送订单消息:" + msg);
        }
    
        @Override
        public void update(Observable o, Object arg) {
            this.sendMsg(arg.toString());
        }
    }
    
  • 测试类

    public class TestAction {
        public static void main(String[] args) {
            // 定义消息发送器
            Observer sms = new Sms();
            Observer email = new Email();
            Observer wechat = new Wechat();
            // 定义商城,并依赖短信发送服务
            Mall mall = new Mall();
            mall.addObserver(sms);
            mall.addObserver(email);
            mall.addObserver(wechat);
    
            // 开始执行下单和支付
            mall.order();
            mall.buy();
        }
    }
    
  • 打印日志

    【商城服务】用户下单
    【微信服务】发送订单消息:用户支付成功
    【邮箱服务】发送订单消息:用户支付成功
    【短信服务】发送订单消息:用户支付成功
    【商城服务】用户支付
    【微信服务】发送订单消息:用户支付成功
    【邮箱服务】发送订单消息:用户支付成功
    【短信服务】发送订单消息:用户支付成功
    

以上代码使用了java提供的api也同样实现了观察者模式的功能。我们可以看看Observerable类中的源码,这里只截取了部分重要的代码。其中的实现原理与我们的示例二是一样的。

  • Observerable部分源码
public class Observable {
    private boolean changed = false;
    // 定义观察者集合,可以供多个观察者订阅
    private Vector<Observer> obs;
    
    // 增加一个消息发送观察者
    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    /**
     * Deletes an observer from the set of observers of this object.
     * Passing <CODE>null</CODE> to this method will have no effect.
     * @param   o   the observer to be deleted.
     */
    // 删除一个消息发送观察者
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }
    
    // 通知所有的消息观察者,开始发送消息
    public void notifyObservers(Object arg) {
        /*
         * a temporary array buffer, used as a snapshot of the state of
         * current Observers.
         */
        Object[] arrLocal;

        synchronized (this) {
            /* We don't want the Observer doing callbacks into
             * arbitrary code while holding its own Monitor.
             * The code where we extract each Observable from
             * the Vector and store the state of the Observer
             * needs synchronization, but notifying observers
             * does not (should not).  The worst result of any
             * potential race-condition here is that:
             * 1) a newly-added Observer will miss a
             *   notification in progress
             * 2) a recently unregistered Observer will be
             *   wrongly notified when it doesn't care
             */
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }
        // 遍历所有观察者,一一通知
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    /**
     * Marks this <tt>Observable</tt> object as having been changed; the
     * <tt>hasChanged</tt> method will now return <tt>true</tt>.
     */
    // 开关操作,要发送消息时先调用这里将开关打开
    protected synchronized void setChanged() {
        changed = true;
    }
}
  • 新的类图如下

    未命名文件 (15)

优点

  • 观察者与被观察者(发布者与订阅者)是抽象的耦合

    耦合仅控制在抽象的接口或者顶级父类(ObserverAble)中,不会下沉到具体的实现类。避免了实际的耦合。对拓展开发有很好的支持

  • 简单的触发机制

    在被观察者这边来说可以随时随地的触发观察者机制。不用过多的关心观察者的具体实现。

缺点

  • 虽然观察者模式比较容易实现,但不利于嵌套使用。如A观察者监听业务服务1的动作,同时又有一个B观察者要监听A观察者的业务动作后面可能还有C观察者等等。这个是程序无法控制的,且对于开发和维护上来说是很不方便的。所以在开发设计时需要考虑具体的业务场景,最好不要出现多层嵌套监听的情况。
  • 在以上所述例子中所有的被观察者都是使用的同步的方式通知观察者的。但是在实际项目中观察者的数量可能会比较多,也有可能观察者处理触发事件效率比较低,这个时候如果使用同步的方式通知观察者,势必会导致被观察者(具体业务部分)的效率降低。当然这也是有解决方法的。如果业务允许观察者异步执行触发动作的话。我们可以使用异步的形式来通知观察者来执行动作,那么观察者这边可以使用缓存、消息队列等形式慢慢消化掉这些通知。

使用场景

​ 文中就是一个比较典型的使用场景,属于跨系统的消息交换场景。还有就是观察者和被观察者两者的业务是需要可拆分联系不紧密的情况下。

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

推荐阅读更多精彩内容