一天一种JAVA设计模式之十五:观察者模式

540 阅读12分钟

写在前面的话

复习、总结23种设计模式

获取详细源码请点击我

上一篇

# # 一天一种JAVA设计模式之十四:模板方法模式

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

记重点

java自带的观察者接口(java.util.Observer)和被观察者接口(java.util.Observable)

定义

定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

DEMO-1 从李斯监视韩非子开始说起,一步一步走到最高

定义被观察者接口:韩非子是被观察者接口(李斯 观察 韩非子)

package com.design.pattern.template.method.test02;  
  
// 
public interface IHanFeiZI {  
    // 吃早饭  
    void haveBreakfast();  

    // 开始娱乐  
    void haveFun();  
}

// 对接口进行扩充,增加了两个状态isHavingBreakfast(是否在吃早饭)和isHavingFun(是否在娱// // 乐),以方便间谍进行监控
class HanFeiZI implements IHanFeiZI {  
  
    private volatile boolean isHavingBreakfast = false;  
    private volatile boolean isHavingFun = false;  

    @Override  
    public void haveBreakfast() {  
    System.out.println("韩非子:开始吃早饭了...");  
        isHavingBreakfast = true;  
    }  

    @Override  
    public void haveFun() {  
    System.out.println("韩非子:开始娱乐了...");  
        isHavingFun = true;  
    }  

    public boolean isHavingBreakfast() {  
        return isHavingBreakfast;  
    }  

    public void setHavingBreakfast(boolean havingBreakfast) {  
        isHavingBreakfast = havingBreakfast;  
    }  

    public boolean isHavingFun() {  
        return isHavingFun;  
    }  

    public void setHavingFun(boolean havingFun) {  
        isHavingFun = havingFun;  
    }  
}

定义观察者接口:李斯是观察者(李斯 观察 韩非子)

package com.design.pattern.template.method.test02;  
  
// 
public interface ILiSI {  
    // 一发现别人有动静,自己也要行动起来  
    void update(String context);  
}

class LiSi implements ILiSI {  
    //首先李斯是个观察者,一旦韩非子有活动,他就知道,他就要向老板汇报  
    public void update(String str) {  
        System.out.println("李斯:观察到韩非子活动,开始向老板汇报了...");  
        this.reportToOiShiHuang(str);  
        System.out.println("李斯:汇报完毕...\n");  
    }  

    //汇报给秦始皇  
    private void reportToOiShiHuang(String reportContext) {  
        System.out.println("李斯:报告,秦老板!韩非子有活动了--->" + reportContext);  
    }  
}

找两个间谍,分别去观察韩非子有没有吃饭或娱乐,一旦发现就通知李斯

仅有这两个对象还是不够的,我们要解决的是李斯是怎么监控韩非子的? 创建一个后台线程一直处于运行状态,一旦发现韩非子在吃饭或者娱乐就触发事件?这是真实世界的翻版,安排了一个间谍,观察韩非子的生活起居,并上报给李斯,然后李斯再触发update事件。

package com.design.pattern.template.method.test02;  
  
// 李斯找了一个间谍,一直监控着韩非子有没有吃早饭,一旦吃了就会通知李斯  
public class BreakfastSpy extends Thread {  
  
    private HanFeiZI hanFeiZI;  
    private LiSi liSi;  

    public BreakfastSpy(HanFeiZI hanFeiZI, LiSi liSi) {  
        this.hanFeiZI = hanFeiZI;  
        this.liSi = liSi;  
    }  

    @Override  
    public void run() {  
        while (true) {  
            if (hanFeiZI.isHavingBreakfast()) {  
                liSi.update("韩非子在吃饭");  
                hanFeiZI.setHavingBreakfast(false);  
            }  
        }  
    }  
}
package com.design.pattern.template.method.test02;  
  
// 李斯找了一个间谍,一直监控着韩非子有没有玩耍,一旦玩耍就会通知李斯  
public class FunSpy extends Thread {  

    private HanFeiZI hanFeiZI;  
    private LiSi liSi;  

    public FunSpy(HanFeiZI hanFeiZI, LiSi liSi) {  
        this.hanFeiZI = hanFeiZI;  
        this.liSi = liSi;  
    }  

    @Override  
    public void run() {  
        while (true) {  
            if (hanFeiZI.isHavingFun()) {  
                liSi.update("韩非子在娱乐");  
                hanFeiZI.setHavingFun(false);  
            }  
        }  
    }  
}

测试

package com.design.pattern.template.method.test02;  
  
public class Client {  
  
    public static void main(String[] args) throws InterruptedException {  
        // 定义李斯和韩非子  
        LiSi liSi = new LiSi();  
        HanFeiZI hanFeiZI = new HanFeiZI();  

        // 李斯找了一个间谍,一直监控着韩非子有没有吃早饭,一旦吃了就会通知李斯  
        Thread breakfastSpy = new BreakfastSpy(hanFeiZI, liSi);  
        breakfastSpy.setDaemon(true);  
        breakfastSpy.start();  

        // 李斯找了一个间谍,一直监控着韩非子有没有玩耍,一旦玩耍就会通知李斯  
        Thread funSpy = new FunSpy(hanFeiZI, liSi);  
        funSpy.setDaemon(true);  
        funSpy.start();  

        // 等间谍潜伏到韩非子身边  
        Thread.sleep(2000);  
        // 终于等到韩非子吃饭了  
        hanFeiZI.haveBreakfast();  

        // 过来一会,韩非子开始娱乐了  
        Thread.sleep(1000);  
        hanFeiZI.haveFun();  

        Thread.sleep(2000);  
    }  
}

输出结果

韩非子:开始吃早饭了...
李斯:观察到韩非子活动,开始向老板汇报了...
李斯:报告,秦老板!韩非子有活动了--->韩非子在吃饭
李斯:汇报完毕...

韩非子:开始娱乐了...
李斯:观察到韩非子活动,开始向老板汇报了...
李斯:报告,秦老板!韩非子有活动了--->韩非子在娱乐
李斯:汇报完毕...

存在的问题

结果出来,韩非子一吃早饭李斯就知道,韩非子一娱乐李斯也知道,非常正确!结果正确但并不表示你有成绩,我告诉你:你的成绩是0,甚至是负的!你有没有看到你的CPU飙升Eclipse不响应状态?看到了?看到了你还不想为什么?!看看上面的程序,别的就不多说了使用了一个死循环while(true)来做监听,要是用到项目中,你要多少硬件投入进来?你还让不让别人的程序运行了?!一台服务器就跑你这一个程序就完事!错误也看到了,我们必须要修改,这个没法应用到项目中,而且这个程序根本就不是面向对象的程序,这完全是面向过程的,不改不行,怎么修改呢?我们来想,既然韩非子一吃饭李斯就知道了,那我们为什么不把李斯这个类聚集到韩非子那个类上呢?说改就改,立马动手,

DEMO-2 不通过间谍了,直接李斯本人去监视韩非子,一有动静立马汇报秦王

image.png

把观察者嵌在被观察者里面:在HanFeiZi类中引用了LiSi实例

package com.design.pattern.template.method.test03;  
  
import com.design.pattern.template.method.test02.IHanFeiZI;  
import com.design.pattern.template.method.test02.ILiSI;  
import com.design.pattern.template.method.test02.LiSi;  
  
public class HanFeiZI implements IHanFeiZI {  
    // 直接把李斯声明出来  
    private ILiSI liSI = new LiSi();  

    @Override  
    public void haveBreakfast() {  
        System.out.println("韩非子:开始吃早饭了...");  
        liSI.update("韩非子在吃饭");  
    }  

    @Override  
    public void haveFun() {  
        System.out.println("韩非子:开始娱乐了...");  
        liSI.update("韩非子在娱乐");  
    }  
  
}

测试

package com.design.pattern.template.method.test03;  
  
public class Client {  
  
    public static void main(String[] args) {  
        HanFeiZI hanFeiZI = new HanFeiZI();  
        hanFeiZI.haveBreakfast();  
        hanFeiZI.haveFun();  
    }  
}

存在的问题

李斯就不用在场景类中定义了,非常简单,运行结果相同,不再赘述。

我们思考一下,修改后的程序运行结果正确,效率也比较高,是不是应该乐呵乐呵了?大功告成了?稍等等,你想在战国争雄的时候,韩非子这么有名望、有实力的人,就只有秦国关心他吗?想想也不可能呀,确实有一大帮的各国类似于李斯这样的人在看着他,监视着他的一举一动,但是看看我们的程序,你在HanFeiZi这个类中定义:

 private ILiSI liSI = new LiSi();  

这样一来只有李斯才能观察到韩非子,这是不对的,也就是说韩非子的活动只通知了李斯一个人,这不可能,再者说了,李斯只观察韩非子的吃饭、娱乐吗?政治倾向不关心吗?思维倾向不关心吗?杀人放火不关心吗?也就说韩非子的一系列活动都要通知李斯,这可怎么办?要按照上面的例子,我们如何修改?这和开闭原则严重违背呀,我们的程序有问题,修改如图

DEMO-3 很多类似李斯的观察者去监视韩非子一人

image.png

我们把原有类图做了两个修改:

增加Observable

实现该接口的都是被观察者,那韩非子是被观察者,他当然也要实现该接口了,同时他还有与其他庸人相异的事要做,因此他还是要实现IHanFeizi接口。

修改ILiSI接口名称为Observer

接口名称修改了一下,这样显得更抽象化,所有实现该接口的都是观察者(类似李斯这样的)。

Observable是被观察者,就是类似韩非子这样的人,在Observable接口中有三个比较重要的方法,分别是

  • addObserver() 增加观察者,
  • deleteObserver() 删除观察者,
  • notifyObservers() 通知所有的观察者,

这是什么意思呢?我这里有一个信息,一个对象,我可以允许有多个对象来察看,你观察也成,我观察也成,只要是观察者就成,也就是说我的改变或动作执行,会通知其他的对象,看程序会更明白一点,先看Observable接口,如代码清单22-9所示。

定义一个通用的被观察者接口,所有的被观察者都可以实现这个接口

package com.design.pattern.template.method.test04;  
  
// 被观察者接口  
public interface Observable {  
  
    public void addObserver(Observer observer);  
    public void deleteObserver(Observer observer);  
    public void notifyObservers(String context);  
  
}

被观察者实现类(韩非子)

package com.design.pattern.template.method.test04;  
  
import com.design.pattern.template.method.test02.IHanFeiZI;  
  
import java.util.ArrayList;  
import java.util.List;  
  
public class HanFeiZI implements IHanFeiZI, Observable {  
  
    private List<Observer> observerList = new ArrayList<>();  

    @Override  
    public void addObserver(Observer observer) {  
        observerList.add(observer);  
    }  

    @Override  
    public void deleteObserver(Observer observer) {  
        observerList.remove(observer);  
    }  

    @Override  
    public void notifyObservers(String context) {  
        for (Observer observer : observerList) {  
            observer.update(context);  
        }  
    }  

    @Override  
    public void haveBreakfast() {  
        System.out.println("韩非子:开始吃早饭了...");  
        notifyObservers("韩非子在吃饭");  
    }  

    @Override  
    public void haveFun() {  
        System.out.println("韩非子:开始娱乐了...");  
        notifyObservers("韩非子在娱乐");  
    }  
  
}

观察者接口

package com.design.pattern.template.method.test04;  
  
// 抽象观察者  
public interface Observer {  
    // 一发现别人有动静,自己也要行动起来  
    void update(String context);  
}

观察者1李斯

package com.design.pattern.template.method.test04;  
  
public class LiSi implements Observer {  
    @Override  
    public void update(String str) {  
        System.out.println("李斯:观察到韩非子活动,开始向老板汇报了...");  
        this.reportToOiShiHuang(str);  
        System.out.println("李斯:汇报完毕...\n");  
    }  

    //汇报给秦始皇  
    private void reportToOiShiHuang(String reportContext) {  
        System.out.println("李斯:报告,秦老板!韩非子有活动了--->" + reportContext);  
    }  
}

测试

package com.design.pattern.template.method.test04;  
  
public class Client {  
    public static void main(String[] args) {  
        Observer observer1 = new LiSi();  
        Observer observer2 = new LiSi();  
        Observer observer3 = new LiSi();  

        HanFeiZI hanFeiZI = new HanFeiZI();  
        hanFeiZI.addObserver(observer1);  
        hanFeiZI.addObserver(observer2);  
        hanFeiZI.addObserver(observer3);  

        hanFeiZI.haveBreakfast();  
        hanFeiZI.haveFun();  
    }  
}

好了,结果也正确了,也符合开闭原则了,同时也实现类间解耦,想再加观察者?继续实现Observer接口就成了,这时候必须修改Client程序,因为你的业务都发生了变化。这就是观察者模式。

观察者模式的通用类图和角色划分

1706578477444.png

观察者模式的几个角色:

Subject被观察者

定义被观察者必须实现的职责,它必须能够动态地增加、取消观察者。它一般是抽象类或者是实现类,仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。

package com.design.pattern.template.method.test05;  
  
import java.util.Vector;  
  
// 被观察者  
public abstract class Subject {  
    //定义一个观察者数组  
    private Vector<Observer> obsVector = new Vector<Observer>();  

    //增加一个观察者  
    public void addObserver(Observer o) {  
        this.obsVector.add(o);  
    }  

    //删除一个观察者  
    public void delObserver(Observer o) {  
        this.obsVector.remove(o);  
    }  

    //通知所有观察者  
    public void notifyObservers() {  
        for (Observer o : this.obsVector) {  
            o.update();  
        }  
    }  
  
}

Observer观察者

观察者接收到消息后,即进行update(更新方法)操作,对接收到的信息进行处理。

package com.design.pattern.template.method.test05;  
  
// 观察者  
public interface Observer {  
  
    void update();  
}

ConcreteSubject具体的被观察者

定义被观察者自己的业务逻辑,同时定义对哪些事件进行通知。

package com.design.pattern.template.method.test05;  
  
// 具体被观察者  
public class ConcreteSubject extends Subject {  
  
    public void doSth() {  
        super.notifyObservers();  
    }  
  
}

ConcreteObserver具体的观察者

每个观察在接收到消息后的处理反应是不同,各个观察者有自己的处理逻辑

package com.design.pattern.template.method.test05;  
  
// 具体观察者  
public class ConcreteObserver implements Observer{  
  
    @Override  
    public void update() {  
        System.out.println("接收到信息,开始处理");  
    }  
}

场景类

package com.design.pattern.template.method.test05;  
  
public class Client {  
    public static void main(String[] args) {  
        // 定义一个被观察者  
        ConcreteSubject concreteSubject = new ConcreteSubject();  
        // 定义一个观察者  
        ConcreteObserver concreteObserver = new ConcreteObserver();  
        // 观察者开始观察被观察者  
        concreteSubject.addObserver(concreteObserver);  
        // 被观察者开始活动了  
        concreteSubject.doSth();  
    }  
}

java自带的观察者和被观察者

java.util.Observable 被观察者接口

java.util.Observer 观察者接口

观察者模式的优点

观察者和被观察者之间是抽象耦合

如此设计,则不管是增加观察者还是被观察者都非常容易扩展,而且在Java中都已经实现的抽象层级的定义,在系统扩展方面更是得心应手。

建立一套触发机制

根据单一职责原则,每个类的职责是单一的,那么怎么把各个单一的职责串联成真实世界的复杂的逻辑关系呢?比如,我们去打猎,打死了一只母鹿,母鹿有三个幼崽,因失去了母鹿而饿死,尸体又被两只秃鹰争抢,因分配不均,秃鹰开始斗殴,然后弱的秃鹰死掉,生存下来的秃鹰,则因此扩大了地盘……这就是一个触发机制,形成了一个触发链。观察者模式可以完美地实现这里的链条形式,

观察者模式的缺点

观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试就会比较复杂,而且在Java中消息的通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率。在这种情况下,一般考虑采用异步的方式。

多级触发时的效率更是让人担忧,大家在设计时注意考虑。

观察者模式的使用场景

口关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。 口事件多级触发场景。 口跨系统的消息交换场景,如消息队列的处理机制。

观密者模式的注意事项

使用观察者模式也有以下两个重点问题要解决。

广播链的问题

如果你做过数据库的触发器,你就应该知道有一个触发器链的问题,比如表A上写了一个触发器,内容是一个字段更新后更新表B的一条数据,而表B上也有个触发器,要更新表C,表C也有触发器……完蛋了,这个数据库基本上就毁掉了!我们的观察者模式也是一样的问题一个观察者可以有双重身份,既是观察者,也是被观察者,这没什么问题呀,但是链一旦建立这个逻辑就比较复杂,可维护性非常差,根据经验建议,在一个观察者模式中最多出现一个对象既是观察者也是被观察者,也就是说消息最多转发一次(传递两次),这还是比较好控制的,

注意 它和责任链模式的最大区别就是观察者广播链在传播的过程中消息是随时更改的,它是由相邻的两个节点协商的消息结构;而责任链模式在消息传递过程中基本上保持消息不可变,如果要改变,也只是在原有的消息上进行修正

异步处理问题

这个EJB是一个非常好的例子,被观察者发生动作了,观察者要做出回应,如果观察者比较多,而且处理时间比较长怎么办?那就用异步呗,异步处理就要考虑线程安全和队列的问题这个大家有时间看看Message Queue,就会有更深的了解。