听说你还不会写观察者模式?一文帮你搞定!

问题背景:

书接上回,话说程序员小强成功进入一家公司,并且老板也信守承诺给他分配了一个女朋友小美,老板这样做除了能让小强每天安心写代码之外,还有另外两个意图,第一就是小美是安插在小强身边的眼线,负责监督小强的工作,第二个也是最重要的目的是通过小美可以把公司重要的通知传递给小强。如下是过程示意图

在这里插入图片描述

以前我们是怎么用程序演示上面过程呢?(这里还没有使用观察者模式)为了简单,我不使用接口,直接使用老板,程序员,小美和客户端四个类。

老板类如下:

public class Boss{
    //用来定义今晚是否加班
    private boolean isnotifyProgrammer = false;
    public boolean getIsIsnotifyProgrammer() {
        return isnotifyProgrammer;
    }

    //通知所有程序员加班
    public void notifyProgrammer() {
        System.out.println("所有程序员今晚加班");
        this.isnotifyProgrammer = true;
    }
}

程序员类:

public class Programmer {
    private String name;
    //省去构造函数和set/get
    //接到加班通知,然后加班
    public void work_overtime() {
        System.out.println(name+"要加班");
    }
}

小美类,使用线程进行监控老板类,查看老板是否发出加班通知,如果发出就通知所有程序员加班

public class GrilFriend extends Thread{
    private Boss boss;
    private Programmer programmer;
    public GrilFriend(Boss _boss,Programmer _programmer){
        this.boss = _boss;
        this.programmer = _programmer;
    }

    @Override
    public void run(){
        while (true){
            if(this.boss.getIsIsnotifyProgrammer()){
                this.programmer.work_overtime();
            }
        }
    }
}

继承Thread类,重写run()方法,然后start()就可以启动一个线程了。

客户端

package Observer;

public class Client {
    public static void main(String[] args) throws InterruptedException {
        Boss boss = new Boss();
        Programmer programmer = new Programmer("小强");
        GrilFriend grilFriend = new GrilFriend(boss,programmer);
        grilFriend.start();
        boss.notifyProgrammer();
        Thread.sleep(1000);
    }
}

将老板和程序员小强的实例传进小美类中,然后开启线程进行监听老板是否发出通知,只要这时老板发出通知,程序员就可以接收到并执行加班的命令。输出如下:

小强要加班
小强要加班
.....

有何问题:

上面这个程序,小美使用死循环来监听,导致CPU飙升,严重浪费了公司的资源,公司面临亏损,老板决定裁员来开源节流,就这样处在程序员和老板直接的小美就被裁掉了,没了小美之后,程序员就要上班时去老板办公室签到,下班时再去询问今晚是否加班。久而久之,程序员心生怨念,老板每天被问来问去也很烦,于是他们想到了一种新的解决方案。

解决方法:

于是老板买来几个小喇叭,放在程序员的办公室里,而按钮放在老板的办公室里,如果有新的通知,就按喇叭,程序员们听到之后就前往老板的办公室领取具体的通知内容,这样就不需要程序员每天去老板办公室询问是否有新的通知了,而且还节约了成本,所谓一举两得。

怎么用代码实现上面的过程呢

首先定义一个老板的接口类,里面主要是公共方法如添加程序员对象和删除程序员对象以及通知所有程序员的方法。以后还有其他的小老板,都继承自这个接口:

import java.util.ArrayList;
import java.util.List;

public class Boss{

    //定义一个列表,用来保存程序员对象
    private List<Programmer> programmers = new ArrayList<Programmer>();

    //添加一个程序员到列表中
    public void attach(Programmer programmer){
        programmers.add(programmer);
    }

    //从列表中删除某个程序员对象
    public void detach(Programmer programmer){
        programmers.remove(programmer);
    }
    //通知所有程序员加班
     public void notifyProgrammer(boolean isWorkOvertime) {
        //遍历程序员列表
        for(Programmer programmer:programmers)
            programmer.work_overtime(isWorkOvertime);
    }
}

当前老板的类,当前老板设置一个状态,就是今晚程序员是否需要加班,当这个状态发生改变时,就调用上面的notifyProgrammer()方法通知所有程序员:

//具体的老板-李老板
public class Li_Boss extends Boss {
    //是否加班的通知
    private boolean isWorkOvertime;
    public boolean getIsWorkOvertime(){
        return isWorkOvertime;
    }
    //当加班状态发生变化时,要通知程序员最新的加班状态
    public void setIsWorkOvertime(boolean isWorkOvertime){
        this.isWorkOvertime = isWorkOvertime;
        this.notifyProgrammer(isWorkOvertime);
    }
}

程序员的接口,只有一个方法,被通知的是否加班方法:

public interface IProgrammer {
    //被通知的方法
    public void work_overtime(boolean isWorkOvertime);
}

具体的程序员类,实现上面加班的方法,执行加班行为:

public class Programmer implements IProgrammer{
    private String name;

    public Programmer(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    //自己的加班状态
    private boolean WorkOvertimeState;
    //接到加班通知,然后更新自己的加班状态
    public void work_overtime(boolean isWorkOvertime) {
        WorkOvertimeState = isWorkOvertime;
        System.out.println(name+"是否要加班:"+WorkOvertimeState);
    }
}

客户端,使用上面的类,先创建一个老板对象和两个程序员对象,并把两个程序员对象添加到老板对象中的程序员列表中(相当于上班前来打卡,老板知道今天有哪些程序员来上班了),然后老板发布今晚加班的通知,所有程序员就会自动接收到通知,并执行加班的行为:

public class Client {
    public static void main(String[] args) throws InterruptedException {
        Li_Boss boss = new Li_Boss();
        Programmer programmer1 = new Programmer("小强");
        Programmer programmer2 = new Programmer("小华");
        //将上面两个程序员添加到老板类里的程序员列表中
        boss.attach(programmer1);
        boss.attach(programmer2);
        //老板发布通知,今晚加班
        boss.setIsWorkOvertime(true);
    }
}

打印结果如下:

小强是否要加班:true
小华是否要加班:true

模式讲解:

以上的实现方式就是通过观察者模式实现的,观察者模式定义:

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

更多内容请关注我的weixin公号【程序员修炼】
上面老板和程序员的关系就是典型的 一对多关系,并且程序员是否加班的状态也是由老板决定的,程序员会根据老板的通知执行相对应的行为,而老板不需要知道具体有哪些程序员,只需要维护一个程序员的列表就可以了,通知时遍历这个列表。

老板相当于被观察者,也被称为目标:Subject,程序员是观察者,也就是Observer,一个目标可以有任意多个观察者对象,一旦目标的状态发生变化,所有注册的观察者都会得到通知,然后各个观察者会对通知做出相应的反应。

但是需要注意,观察者始终是处于一种被动的地位,也就是注册和删除都是由目标来决定的,观察者没有权限决定是否观察哪个目标。这叫做单向依赖,观察者依赖于目标,目标是不会依赖于观察者的。这一点也很好了解,就像是程序员没有权限决定听不听老板的通知一样,老板通知所有程序员加班,你不能决定你能不能接收到通知吧。

观察者也可以观察多个目标,但是应该为不同的观察者目标定义不同的回调函数用来区分,也就是每个通知对应一个响应函数。这一点也很容易理解,一家公司会有老板,还会有部门经理,主管等职位,程序员都要听他们的。

观察者模型结构示意图:

在这里插入图片描述
  • Subject目标对象,相当于上面的Boss类
  • Observer:观察者接口,提供目标通知时对应的更新方法,可以在这个方法中回调目标对象,以获取目标对象的数据,相当于上面的IProgrammer接口
  • ConcreteSubject:具体的目标实现类,相当于上面的Li_boss
  • ConcreteObserver:具体的观察者对象,用来接收通知并做出响应,相当于上面的Programmer类。

新的问题:

公司这样执行了一段时间,新的问题又出现了,因为老板并不只是通知加班这么简单,还会通知其他的事情,可能每个程序员想得到的通知不一样,比如小强去出差,小华去休假。这个时候该怎么通知他们呢。

这就涉及观察者的两种模型,拉模型和推模型。上面的代码就是推模型实现的。

  • 推模型:目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据。如上面就是推送的是否加班的状态。
  • 拉模型“目标对象在通知观察者的时候,只会传递少量信息,如果不知道观察者具体需要什么数据,就把目标对象自身传递给观察者,让观察者自己按需去取信息。

为了方便对比,接下来用拉模型实现通知加班的代码,首先看一下Boss类,只需要更改通知的方法:

import java.util.ArrayList;
import java.util.List;

public class Boss{

    //定义一个列表,用来保存程序员对象
    private List<Programmer> programmers = new ArrayList<Programmer>();

    //添加一个程序员到列表中
    public void attach(Programmer programmer){
        programmers.add(programmer);
    }

    //从列表中删除某个程序员对象
    public void detach(Programmer programmer){
        programmers.remove(programmer);
    }
    //通知所有程序员加班
    public void notifyProgrammer() {
        //遍历程序员列表
        for(Programmer programmer:programmers)
            programmer.work_overtime(this);


    }
}

notifyProgrammer()方法去掉参数,调用 programmer.work_overtime()时将自身this传递进去。具体的老板类Li_Boss只需要实现这个方法就可以了。

IProgrammer接口中被通知的方法接收参数boss对象:

public interface IProgrammer {
    //被通知的方法
    public void work_overtime(Boss boss);
}

具体的programmer实现类,也只需要该接收通知的方法:

public class Programmer implements IProgrammer{
   ......
    //自己的加班状态
    private boolean WorkOvertimeState;
    //接到加班通知,然后更新自己的加班状态
    public void work_overtime(Boss boss) {
        WorkOvertimeState = ((Li_Boss)boss).getIsWorkOvertime();
        System.out.println(name+"是否要加班:"+WorkOvertimeState);
    }
}

客户端不需要更改,输出结果和上面一样,没有问题。当有多个具体的观察者(程序员)类时,不同的类需要不同的信息,在目标类中定义多种状态,当具体的观察者接到通知后通过目标对象实例按需去状态就可以了。

推拉模型的比较:

  • 推模型是假定目标对象知道观察者需要什么数据,相当于精准推送。拉模型目标对象不知道观察者需要什么数据,把自身对象给观察者,让观察者自己去取。
  • 推模型使观察者模型难以复用,拉模型可以复用。

相关扩展:

更多内容请关注我的weixin公号【程序员修炼】
其实在java中已经有了观察者模式的实现,不需要自己从头写。在java.util包里面有一个类Observable,它实现了大部分我们需要的目标功能。接口Observer中定义了update方法。相比于上面自己的实现区别如下:

  • 不需要定义观察者和目标的接口,JDK已经定义好了
  • 在目标实现类中不需要维护观察者的注册信息,这个Observable类中帮忙实现好了。
  • 触发方式需要先调用setChanged方法
  • 具体的观察者实现中,update方法同时支持推模型和拉模型。

使用java中的Observable实现上面的程序:

目标对象 :

import java.util.Observable;

//具体的老板-李老板
public class Li_Boss extends Observable {
    //是否加班的通知
    private String isWorkOvertime;
    public String getIsWorkOvertime(){
        return isWorkOvertime;
    }
    //当加班状态发生变化时,要通知程序员来领取新通知
    public void setIsWorkOvertime(String isWorkOvertime){
        this.isWorkOvertime = isWorkOvertime;
        //使用java中的Observable,这句话是必须的。
        this.setChanged();
        //拉模式
        this.notifyObservers();
        //推模式
        //this.notifyObservers(isWorkOvertime);
    }
}

观察者对象:

package Observer;

import java.util.Observable;
import java.util.Observer;

public class Programmer implements Observer {
    private String name;

    //构造函数和set方法省略
    //自己的加班状态
    private String WorkOvertimeState;

    //接到加班通知,然后更新自己的加班状态

    @Override
    public void update(Observable observable, Object o) {
        //拉模式对应的更新方法
        WorkOvertimeState = ((Li_Boss)observable).getIsWorkOvertime();
        System.out.println(name+WorkOvertimeState);

       //推模式对应的更新方法
//        WorkOvertimeState = o.toString() ;
//        System.out.println(name+WorkOvertimeState);
    }
}

客户端:

public class Client {
    public static void main(String[] args){
        Li_Boss boss = new Li_Boss();
        Programmer programmer1 = new Programmer("小强");
        Programmer programmer2 = new Programmer("小华");
        //将上面两个程序员添加到老板类里的程序员列表中
        boss.addObserver(programmer1);
        boss.addObserver(programmer2);
        boss.setIsWorkOvertime("今晚加班");

    }
}

输出结果:

小华今晚加班
小强今晚加班

这样看观察者模式是不是非常简单了。

观察者模式的优点:

  • 实现观察者和目标之间的抽象耦合:只是在抽象层面耦合了。
  • 观察者模式实现了动态联动:一个操作会引起其他相关操作。
  • 观察者支持广播通信:需要注意避免死循环。

观察者模式的缺点:

  • 广播通信会引起不必要的操作。

何时使用观察者模式:

  • 当一个抽象模型有两个方面,一个方面的操作依赖于另一个方面的状态变化时。
  • 更改一个对象时,需要同时联动更改其他对象,但却不知道应该有多少对象需要被改变时。
  • 当一个对象必须通知其他对象,但是又希望他们之间是松散耦合的。

模式变形:

不久老板又陷入了苦恼,因为上面这种方式每次都会同时通知小华和小强,但是老板想只通知小强而不通知小华,想要区别对待观察者,例如当有用户反应公司的软件产品出现bug时,需要小华和小强去找bug并修复。但当出现不是特别紧急的bug时只需要小华一个人修改就可以了,如果出现了紧急的bug,需要马上修复的,就需要小华和小强同时去修复。该如何使用观察者实现上面的场景呢?

因为经过上面的学习,对于观察者已经了解了,代码不做太多的解释。
更多内容请关注我的weixin公号【程序员修炼】
在这里可以把Bug当做目标,小强和小华都是观察者,首先定义观察者接口:

//观察者接口
public interface BugObserver {
    //传入被观察的目标对象
    public void update(BugSubject subject);

    //观察人员职务
    public void setName(String name);

    public String getName();
}

目标对象抽象接口:

import java.util.ArrayList;
import java.util.List;

//目标对象
public abstract class BugSubject {
    //保存注册的观察者
    protected List<BugObserver> observers = new ArrayList<BugObserver>();

    public void attach(BugObserver observer){
        observers.add(observer);
    }
    public void detach(BugObserver observer){
        observers.remove(observer);
    }
    //通知具体的观察者程序员
    public abstract void notifyProgrammer();
    //获取当前bug级别
    public abstract int getBugLevel();

}

具体的观察者实现

//具体的观察者实现
public class Programmer implements BugObserver{

    private String name;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }
    @Override
    public void update(BugSubject subject) {
        System.out.println(name+"获取到通知,当前bug级别为"+subject.getBugLevel());
    }

}

具体的目标实现:

//具体的Bug对象
public class Bug extends BugSubject {
    private int bugLevel = 0;
    public int getBugLevel(){
        return bugLevel;
    }
    public void setBugLevel(int bugLevel){
        this.bugLevel = bugLevel;
        this.notifyProgrammer();
    }
    public void notifyProgrammer(){
        for(BugObserver programmer:observers){
            if(this.bugLevel>=1){
                if("小华".equals(programmer.getName())){
                    programmer.update(this);
                }
            }
            if(this.bugLevel>=2){
                if("小强".equals(programmer.getName())){
                    programmer.update(this);
                }
            }
        }
    }
}

客户端测试一下:

public class Client {
    public static void main(String[] args){
        //创建目标对象
        Bug bug = new Bug();
        //创建观察者
        BugObserver bugObserver1 = new Programmer();
        bugObserver1.setName("小华");
        BugObserver bugObserver2 = new Programmer();
        bugObserver2.setName("小强");
        //注册观察者
        bug.attach(bugObserver1);
        bug.attach(bugObserver2);
        bug.setBugLevel(1);
        System.out.println("---------------------");
        bug.setBugLevel(2);
    }
}

输出结果如下:

小华获取到通知,当前bug级别为1
---------------------
小华获取到通知,当前bug级别为2
小强获取到通知,当前bug级别为2

可以看出,当Bug的级别不同的时候通知的人是不一样的,一级时只通知小华,二级的时候会通知小华和小强。

好了,这就是观察者模式的全部内容了。

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