学习、探究Java设计模式——观察者模式

前言

观察者模式是面向对象编程中较为常用的一种设计模式,观察者模式又称为订阅-发布模式,特别是适用于GUI图形界面中,比如Android的View中就大量使用了此模式。那么观察者模式到底是什么以及我们应该怎么使用它呢?相信通过本文的学习,你们会有一个更为清晰的答案。

定义

观察者模式:定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

由以上的定义,我们可以知道,观察者模式有两个重要要素:观察者、被观察者。用生活中一个常见的例子来描述:我们通常会订阅天气预报信息,假如天气出现了变化,气象App就会通知我们天气变了要做出相应的准备,那么在这个场景中,我们就充当了“观察者”角色,而气象App则充当了“被观察者”角色,当天气发生了变化,“被观察者”就会通知我们,让我们做出相应改变。

那么,我们已经知道了观察者和被观察者的概念,但我们还不知道它们两个是怎样联系起来的,接下来我们正要解决这个问题,为了更好地理解,我们先来看看观察者模式的UML类图。

UML类图

UML类图

我们来分析下上图各个类或者接口的含义:
Observerable:被观察者接口,规定了几个方法,分别是registerObserver():表示将观察者注册到被观察者中,即“订阅”;removeObserver():表示将观察者从被观察者中移除,即“取消订阅”;notifyObservers():当被观察者状态改变的时候,这个方法被调用,通知所有已经注册的观察者。

ConcreteObserverable:被观察者,实现了Observerable接口,对以上的三个方法进行了具体实现,同时有一个List集合,用以保存注册的观察者,等需要通知观察者时,遍历该集合即可。注意,该集合的泛型参数应该是Observer,接口类型,而不应该是具体的Observer实现类,这样做的原因是一个被观察者可能会有多个不同实现类的观察者(但都实现了Observer接口),如果限定了某一个具体的类型,那么会导致以后要加入新类型的时候而不得不修改当前类,耦合度过高,这是一个非常不好的行为。(设计原则:面向接口编程而不是面向实现编程

Observer:观察者接口,规定了update()方法,当被观察者调用notifyObservers()方法时,观察者的update()方法会被回调。

ConcreteObserver:观察者,实现了update()方法。

那么了解了以上各部分的含义后,应该不难得出观察者模式的实现过程了,那么我们接下来看看实现观察者模式的一般过程。

实现步骤

我们需要准备Observerable接口,声明注册、移除、通知更新这三个方法,接着需要Observer接口,声明update()方法,然后我们要实现以上两个接口,分别是ConcreteObserverable和ConcreteObserver这两个实现类。其中,ConcreteObserverable应该有一个成员变量用户保存注册的观察者,当需要通知更新的时候,调用ConcreteObserverable#notifyObservers()方法,该方法内部遍历所有的观察者,并调用他们的update()方法,这样便实现了订阅——发布流程。

A Sample

接下来,我们实现一个简单的例子,让大家更熟悉观察者模式。
我们来模拟一个场景:A、B、C三人在书店订阅了同一本杂志,当该杂志上架的时候,会通知他们杂志上架的信息(包括杂志的期数、最新价格),让他们过来购买。

Step 1.建立接口

新建Observerable.java文件:

public interface Observerable {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObservers();
}

新建Observer.java文件:

public interface Observer {
    public void update(int edition,float cost);
}

Step 2.实现被观察者接口

新建MagazineData.java文件:

public class MagazineData implements Observerable {
    
    private List<Observer> mObservers;
    private int edition;
    private float cost;
    
    public MagazineData() {
        mObservers = new ArrayList<>();
    }
    
    @Override
    public void registerObserver(Observer o) {
        mObservers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int i = mObservers.indexOf(o);
        if(i >= 0)
            mObservers.remove(i);
    }

    @Override
    public void notifyObservers() {
        for(int i = 0; i < mObservers.size(); i++){
            Observer observer = mObservers.get(i);
            observer.update(edition, cost);
        }
    }
    
    public void setInfomation(int edition,float cost){
        this.edition = edition;
        this.cost = cost;
        //信息更新完毕,通知所有观察者
        notifyObservers();
    }
    
}

Step 3.实现观察者接口

新建Customer.java文件:

public class Customer implements Observer {
    
    private String name;
    private int edition;
    private float cost;
    
    public Customer(String name){
        this.name = name;
    }
    
    @Override
    public void update(int edition, float cost) {
        this.edition = edition;
        this.cost = cost;
        buy();
    }
    
    public void buy(){
        System.out.println(name+"购买了第"+edition+"期的杂志,花费了"+cost+"元。");
    }

}

经过以上三个步骤,已经实现了观察者模式了,那么我们最后再编写一个测试类,来进行测试:

public class Test {

    public static void main(String[] args) {
        //创建被观察者
        MagazineData magazine = new MagazineData();
        //创建三个不同的观察者
        Observer customerA = new Customer("A");
        Observer customerB = new Customer("B");
        Observer customerC = new Customer("C");
        
        //将观察者注册到被观察者中
        magazine.registerObserver(customerA);
        magazine.registerObserver(customerB);
        magazine.registerObserver(customerC);
        
        //更新被观察者中的数据,当数据更新后,会自动通知所有已注册的观察者
        magazine.setInfomation(5, 12);
    }

}

运行,结果如下:

运行结果.jpg

通过上面的例子,我们可以看出,MagazineData和Customer两者处于松耦合状态,甚至是零耦合状态,如果某一者需求改变了,需要进行代码的修改,这并不会涉及到另一者,这是非常良好的松耦合设计,只需要实现接口即可。

认识推模型和拉模型

通过上面的一个例子,我们简单实现了观察值模式,我们注意到在被观察者通知观察者的过程中,即在Observerable内调用Observer的update(int edition, float cost)方法时,我们把MagazineData的几个数据传递了进去,使得观察者能够从参数中得到数据,这就像从MagazineData中把数据“推”到了观察者中。这也就是推模型。与推模型相对的,假如MagazineData并不是把具体的数据传递过去,而是把自身的引用作为参数传递过去,那么观察者得到的是MagazineData实例的引用,那么我们可以直接通过一系列的get方法来获取数据,这也就是拉模型。下面,我们给推模型和拉模型下定义:

推模型:被观察者主动向观察者推送自身的信息,可以是全部信息或者是部分信息。

拉模型:被观察者通过把自身的引用传递给观察者,需要观察者自行通过该引用来获取相关的信息。

其实,我们上面实现的例子,就是使用了推模型,把MagazineData的两个成员变量推送给了观察者,如果要把以上例子修改为拉模型也很简单,在update()方法中,把参数修改为:update(Observerable observerable)即可,然后在观察者的实现类中,把得到的Observerable引用强制转换为具体的实现类型即可。

比较:推模型适用于提前知道观察者所需要的数据的情况,而拉模型由于把自身传递了过去,因此适用于大多数场景。

认识Java内置的观察者模式

上面我们自行实现了观察者模式,其实在Java中,它已经为我们准备了Observerable类以及Observer接口,只要继承或实现,就能快速实现我们的被观察者和观察者了,那么,我们学习下该内置的观察者模式。

首先,我们来看看java.util.Observerable类,这与我们上面自行实现的不同,它是一个类而不是一个接口,因此我们的被观察者要继承该类,另外,Observerable类已经帮我们实现了addObserver()、deleteObserver()、notifyObservers()等方法,因此我们在子类不需要再重写这些方法了,另外,该父类还提供了一个setChanged()方法,该方法用来标记状态已经改变的事实,因此要先调用该方法再调用notifyObservers()方法。其实,该类提供了两个notifyObservers()方法,一个有参数,一个无参数,而有参数的适用于推模型,无参数的适用于拉模型。
java.util.Observer则是一个接口,与我们上面定义的基本一样。

那么,我们来利用Java内置的观察者模式结合拉模型来实现我们上面的小例子。
新建JournalData.java:

public class JournalData extends Observable {
    private int edition;
    private float cost;
    
    public void setInfomation(int edition,float cost){
        this.edition = edition;
        this.cost = cost;
        setChanged();
        //调用无参数的方法,使用拉模型
        notifyObservers();
    }
    
    //提供get方法
    public int getEdition(){
        return edition;
    }
    
    public float getCost(){
        return cost;
    }
}

新建Consumer.java:

public class Consumer implements Observer {
    
    private String name;
    private int edition;
    private float cost;
    
    public Consumer(String name){
        this.name = name;
    }
    
    @Override
    public void update(Observable o, Object arg) {
        //判断o是否是JournalData的一个实例
        if(o instanceof JournalData){
            //强制转化为JournalData类型
            JournalData journalData = (JournalData) o;
            //拉取数据
            this.edition = journalData.getEdition();
            this.cost = journalData.getCost();
            buy();
        }

    }
    
    public void buy(){
        System.out.println(name+"购买了第"+edition+"期的杂志,花费了"+cost+"元。");
    }

}

测试类Test.java:

public static void main(String[] args) {
        
        //创建被观察者
        JournalData journal = new JournalData();
        //创建三个不同的观察者
        Consumer consumerA = new Consumer("A");
        Consumer consumerB = new Consumer("B");
        Consumer consumerC = new Consumer("C");
        //将观察者注册到被观察者中
        journal.addObserver(consumerA);
        journal.addObserver(consumerB);
        journal.addObserver(consumerC);
        //更新被观察者中的数据
        journal.setInfomation(6, 11);
    }

}

运行结果如下所示:

运行结果

经过上面的几个例子,相信大家对观察者模式有了一个比较深刻的认识了,最后我们再说说观察者模式可以用在什么地方。比如我们有两个对象,一个对象依赖于另一个对象的变化而变化,此时我们可以将这两个对象抽象出来,做成接口,利用观察者模式来进行解耦,又或者,当一个对象发生变化的时候,需要通知别的对象来做出改变,但又不知道这样的对象有多少个,此时利用观察者模式非常合适。

最后,谢谢大家的阅读~

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

推荐阅读更多精彩内容