设计模式五(难逃法眼-观察者模式)

前言

提起观察者模式,LZ略微有点小小的激动,因为LZ在工作中接触的第一个项目就用到了观察者模式,虽然LZ当时处于一种懵逼状态,完全是复制粘贴代码去完成业务需求,但那个项目的接触,也让LZ对观察者模式有了一个初步的了解,此后一直对观察者模式的理解处于朦胧状态,似懂非懂,直到前些天看到左盟主的博客才恍然大悟,有种相见恨晚的感觉。

先来简单描述一下LZ当时项目中是什么样的业务用到观察者模式的。业务大致是这样的,项目当时牵扯到一些txt文件的上传,文件的组要内容是通信基站的一些状态信息和描述,文件大多是批量上传,上传后要自动解析并持久化到数据库,前台没有显示的解析按钮和请求,就意味着所有文件的上传只要请求触发就要通知解析功能对其进行自动解析。很明显这样的业务使用观察者模式更合适不过了,LZ完全后知后觉,先来看看观察者模式吧!

定义

观察者模式(有时又被称为发布-订阅模式、模型-视图模式、源-收听者模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实作事件处理系统。

上面的定义当中,主要有这样几个意思,首先是有一个目标的物件,通俗点讲就是一个类,它管理了所有依赖于它的观察者物件,或者通俗点说是观察者类,并在它自己状态发生变化时,主动发出通知。

简单点概括成通俗的话来说,就是一个类管理着所有依赖于它的观察者类,并且它状态变化时会主动给这些依赖它的类发出通知。

针对以上描述,我们先来看看百度给的类图


2017-11-19_142825.png

我们可以看到,被观察类Observable中只持有一个观察者Observer的列表,当被观察者自己状态发生改变时,调用notifyObservers方法通知自己持有的这些观察者列表中的对象,具体的观察者Observer都是什么,被观察者是不关心也不需要知道的。

上面就将观察者和被观察者二者的耦合度降到很低了,而我们具体的观察者是必须要知道自己观察的是谁,所以它依赖于被观察者。

我们可以看到观察者模式中有三个角色,一个是观察者的抽象接口,一个是观察者的具体实现,一个是被观察者,根据上边的类图我们来写一段测试案例

(1)一个抽象的观察者接口(用于定义观察者发现被观察者改变后所作出的相应动作)

/**
 * 观察者接口
 */
public interface IObserver {
    void update(Observable o);
}

(2)一个被观察者

/**
 * 被观察者类
 */
public class Observable {

    //观察者列表
    List<IObserver> observers = new ArrayList<>();
    
    //给被观察者添加观察者
    public void addObserver(IObserver o){
        observers.add(o);
    }
    
    public void change(){
        System.out.println("被观察者已经发送改变~~~~~");
        //通知观察者们
        notifyObservers();
    }

    private void notifyObservers() {
        for(IObserver o:observers){
            o.update(this);
        }   
    }
}

(3)若干个观察者的具体实现类

/**
 * 观察者A
 */
public class ObserverA implements IObserver {
    public void update(Observable o) {
        System.out.println("观察者A观察到" + o.getClass().getSimpleName() + "发生变化");
        System.out.println("观察者A做出相应");
    }
}


/**
 * 观察者B
 */
public class ObserverB implements IObserver {
    public void update(Observable o) {
        System.out.println("观察者B观察到" + o.getClass().getSimpleName() + "发生变化");
        System.out.println("观察者B做出相应");
    }
}

(4)测试类

public class TestDemo {

    @Test
    public void fun1(){
        //创建一个观察者
        Observable observable = new Observable();
        
        //给观察者添加被观察者
        observable.addObserver(new ObserverA());
        observable.addObserver(new ObserverB());
        
        //观察者执行改变方法
        observable.change();
    }
}
2017-11-19_145944.png

以上就是一个符合观察者模式思想的简单demo,要注意的是,这里的被观察者需要持有一个观察者的列表,列表中填装了观察他的观察者,它还需要有一个添加观察者的方法,来吧需要观察他的观察者添加到观察者列表中,最后还需要有一个通知方法notifyObservers,当自己改变时通知那些观察他的观察者。

我们可以想象生活中的一个这个例子,买房子,买房者就是观察者,房子就是被观察者,当房子价格变动时通知购房者,我们来试着写一写这个例子:
(1)首先我们定义一个购房者的抽象接口人(观察者接口)

/**
 * 定义一个观察者接口(人)
 */
public interface People {
    //人有一个买房的动作
    void buy(Home home);
}

(2)定义一个房子类(被观察者类)

/**
 * 一个房子类(被观察者)
 */
public class Home {

    //一个装载购买者的列表(观察者列表)
    List<People> list = new ArrayList<>();
    
    //一个添加购房者的方法
    public void addPeople(People people){
        list.add(people);
    }
    
    //一个自身的价格改变行为
    public void change(){
        System.out.println("房子价格有变动!");
        notifyObservers();//通知那些观察者(买房的人)
    }
    
    //一个通知购房者的方法
    public void notifyObservers(){
        for (People people : list) {
            people.buy(this);
        }
    }
}

(3)若干个想购买房子的人(观察者实例)

/**
 * 路人甲(观察者1)
 */
public class PeopleA implements People {
    public void buy(Home home) {
        System.out.println("购房者甲观察到" + home.getClass().getSimpleName() + "发生变化");
        System.out.println("购房者甲做出了相应决定");
    }
}



/**
 * 路人乙(观察者2)
 */
public class PeopleB implements People {
    public void buy(Home home) {
        System.out.println("购房者乙观察到" + home.getClass().getSimpleName() + "发生变化");
        System.out.println("购房者乙做出了相应决定");
    }
}

(4)测试类

public class App {
    public static void main(String[] args) {
        //创建一个房子
        Home home = new Home();
        //给房子添加观察者
        home.addPeople(new PeopleA());
        home.addPeople(new PeopleB());
        
        //房子价格有所变动
        home.change();
    }
}
2017-11-19_154754.png

好了,相比看了以上贴切生活的案例大家都对观察者模式都有一个更深的了解,针对上边的例子,大家有没有发现一个问题,我们的被观察者是房子,通知我们价格变动的通知者也是房子,问题就在这里,房子本身不会说话,如何通知我们,所以我们联想到了销售部或者房屋中介,所以这里还缺一个角色,被观察者的管理者。以此引入java中自带的观察者模式

JDK为了方便开发人员开发,已经写好了现成儿的观察者接口和被观察者接口,下面我们看看有关java中提供的观察者接口和被观察者接口的源码

(1)观察者接口

//观察者接口,每一个观察者都必须实现这个接口
public interface Observer {
    //这个方法是观察者在观察对象产生变化时所做的响应动作,从中传入了观察的对象和一个预留参数
    void update(Observable o, Object arg);

}

(2)被观察者接口

//被观察者类
public class Observable {
    //这是一个改变标识,来标记该被观察者有没有改变
    private boolean changed = false;
    //持有一个观察者列表
    private Vector obs;
    
    public Observable() {
    obs = new Vector();
    }
    //添加观察者,添加时会去重
    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
    if (!obs.contains(o)) {
        obs.addElement(o);
    }
    }
    //删除观察者
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }
    //notifyObservers(Object arg)的重载方法
    public void notifyObservers() {
    notifyObservers(null);
    }
    //通知所有观察者,被观察者改变了,你可以执行你的update方法了。
    public void notifyObservers(Object arg) {
        //一个临时的数组,用于并发访问被观察者时,留住观察者列表的当前状态,这种处理方式其实也算是一种设计模式,即备忘录模式。
        Object[] arrLocal;
    //注意这个同步块,它表示在获取观察者列表时,该对象是被锁定的
    //也就是说,在我获取到观察者列表之前,不允许其他线程改变观察者列表
    synchronized (this) {
        //如果没变化直接返回
        if (!changed)
                return;
            //这里将当前的观察者列表放入临时数组
            arrLocal = obs.toArray();
            //将改变标识重新置回未改变
            clearChanged();
        }
        //注意这个for循环没有在同步块,此时已经释放了被观察者的锁,其他线程可以改变观察者列表
        //但是这并不影响我们当前进行的操作,因为我们已经将观察者列表复制到临时数组
        //在通知时我们只通知数组中的观察者,当前删除和添加观察者,都不会影响我们通知的对象
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    //删除所有观察者
    public synchronized void deleteObservers() {
    obs.removeAllElements();
    }

    //标识被观察者被改变过了
    protected synchronized void setChanged() {
    changed = true;
    }
    //标识被观察者没改变
    protected synchronized void clearChanged() {
    changed = false;
    }
    //返回被观察者是否改变
    public synchronized boolean hasChanged() {
    return changed;
    }
    //返回观察者数量
    public synchronized int countObservers() {
    return obs.size();
    }
}

下面我们用jdk提供的观察者模式来修改一下上边的购房案例

(1)一个观察者people(java要求观察者实现Observer接口)

/**
 * 一个想购买房子的观察者
 */
public class People implements Observer {
    
    private String name;
    
    public People(String name) {
        super();
        this.name = name;
    }

    public String getName() {
        return name;
    }
    
    //关注
    public void subscribe(String homeName){
        HomeAgency.getInstance().getHome(homeName).addObserver(this);
    }
    
    //取消关注
    public void unsubscribe(String homeName){
        HomeAgency.getInstance().getHome(homeName).deleteObserver(this);
    }

    //当楼盘房子价格有所变动,就去通知购房者
    public void update(Observable o, Object arg) {
        if(o instanceof Home){
            Home home = (Home) o;
            System.out.println(name + "知道了" + home.getName() + "价格变为: " + home.getLastPrice() + ", 决定去买房子!");
        }   
    }
}

(2)一个被观察者Home(java要求被观察者需要继承Observable类)

/**
 * 一个被观察者(房子)
 */
public class Home extends Observable {

    //楼盘房子的名字
    private String name;

    //最后价格
    private Integer lastPrice;

    public Home(String name) {
        super();
        this.name = name;
        HomeAgency.getInstance().add(this);
    }
    
    public void changePrice(Integer price){
        System.out.println(name + "的价格有所改变,  改变后价格为: " + price + "平方米/元");
        lastPrice = price;
        //标志改变和提醒
        setChanged();
        notifyObservers();
    }   
    public String getName() {
        return name;
    }   
    public Integer getLastPrice() {
        return lastPrice;
    }
}

(3)一个管理被观察者的管理器(房屋中介)

/**
 * 被观察者的管理器(房屋中介)
 */
public class HomeAgency {

    private Map<String,Home> homeMap = new HashMap<>();
    
    public void add(Home home){
        homeMap.put(home.getName(), home);
    }
    
    public Home getHome(String name){
        return homeMap.get(name);
    }
    
    //管理器单例
    private HomeAgency(){}
    
    public static HomeAgency getInstance(){
        return HomeAgencyInstance.instance;
    }
    
    private static class HomeAgencyInstance{
        private static HomeAgency instance = new HomeAgency();
    }
}

(4)测试类

public class App {

    public static void main(String[] args) {
        //定义两个楼盘    五个购房者
        Home home1 = new Home("万科-楼盘");//毛坯
        Home home2 = new Home("恒大-楼盘");//精装修
        People people1 = new People("东邪");
        People people2 = new People("西毒");
        People people3 = new People("南帝");
        People people4 = new People("北丐");
        People people5 = new People("中神通");
        
        //东南西北关注了恒大的楼盘(没有钱,只能买毛坯房)
        people1.subscribe("万科-楼盘");
        people2.subscribe("万科-楼盘");
        people3.subscribe("万科-楼盘");
        people4.subscribe("万科-楼盘");
        
        //中神通比较有钱  关注了精装修的恒大楼盘
        people5.subscribe("恒大-楼盘");
        
        home1.changePrice(9000);
        home2.changePrice(13000);
        
        //后来,中神通没钱了,他就放弃了关注恒大楼盘,转去关注万科楼盘
        people5.unsubscribe("恒大-楼盘");
        people5.subscribe("万科-楼盘");
        
        //恒大楼盘再次涨价将不会通知中神通,反之万科楼盘价格变动会通知中神通
        home1.changePrice(9100);
        home2.changePrice(150000);
    }
}

运行结果


2017-11-19_165827.png

我们使用观察者模式的用意是为了让房子不在关系他价格变动都去通知谁,更重要的是他不需要关系他通知的购房者还是其他人,他只知道这个人实现了观察者接口,即我们的观察者依赖的只是一个抽象的观察者接口,而不关心观察者的具体实现。

另外让购房者来选择自己关注的楼盘,这相当于被观察者将维护通知对象的职能转化给了观察者,这样做的好处是由于一个被观察者可能有N多观察者,所以让被观察者自己维护这个列表会很艰难,这就像一个老师被许多学生认识,那么是所有的学生都记住老师的名字简单,还是让老师记住N多学生的名字简单?答案显而易见,让学生们都记住一个老师的名字是最简单的。

以上就是设计模式中的观察者模式

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

推荐阅读更多精彩内容

  • 1 场景问题# 1.1 订阅报纸的过程## 来考虑实际生活中订阅报纸的过程,这里简单总结了一下,订阅报纸的基本流程...
    七寸知架构阅读 4,516评论 5 57
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,875评论 1 15
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 本文的结构如下: 什么是观察者模式 为什么要用该模式 模式的结构 代码示例 推模型和拉模型 优点和缺点 适用环境 ...
    w1992wishes阅读 1,401评论 0 16
  • .题目1: dom对象的innerText和innerHTML有什么区别? innerText返回元素内的的文本内...
    大大的萝卜阅读 157评论 0 0