21.观察者模式(行为型)

观察者模式(行为型)

原书链接设计模式(刘伟)

一、相关概论

观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。

1). 定义

观察者模式(Observer Pattern): (对象间联动的观察者模式) 定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。

观察者模式结构中通常包括观察目标和观察者两个继承层次结构,其结构如图所示


观察者模式.png

2). 相关角色

  1. Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。
  2. ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。
  3. Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。
  4. ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。

观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。

3). 典型代码

  1. 目标类
/**
 * 抽象目标类:
 */
public abstract class Subject {

    // 定义一个观察者集合用于存储所有观察者对象
    protected ArrayList<Observer> observers = new ArrayList<>();

    // 注册方法,用于向观察者集合中增加一个观察者
    public void attach(Observer observer) {
        observers.add(observer);
    }

    // 注销方法,用于在观察者集合中删除一个观察者
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    // 声明抽象通知方法
    public abstract void notifying();
}

/**
 * 具体目标类
 */
public class ConcreteSubject extends Subject {

    // 实现通知方法
    public void notifying() {
        // 遍历观察者集合,调用每一个观察者的响应方法
        for(Observer obs:observers) {
            obs.update();
        }
    }
}
  1. 观察者类
/**
 * 抽象观察者:
 */
public interface Observer {
    
    //声明响应方法
    public void update();
}
/**
 * 在具体观察者
 */
public class ConcreteObserver implements Observer {
    //实现响应方法
    public void update() {
        //具体响应代码
    }
}

在有些更加复杂的情况下,具体观察者类ConcreteObserver的update()方法在执行时需要使用到具体目标类ConcreteSubject中的状态(属性),因此在ConcreteObserver与ConcreteSubject之间有时候还存在关联或依赖关系,在ConcreteObserver中定义一个ConcreteSubject实例,通过该实例获取存储在ConcreteSubject中的状态。如果ConcreteObserver的update()方法不需要使用到ConcreteSubject中的状态属性,则可以对观察者模式的标准结构进行简化,在具体观察者ConcreteObserver和具体目标ConcreteSubject之间无须维持对象引用。如果在具体层具有关联关系,系统的扩展性将受到一定的影响,增加新的具体目标类有时候需要修改原有观察者的代码,在一定程度上违反了“开闭原则”,但是如果原有观察者类无须关联新增的具体目标,则系统扩展性不受影响。

二、案例需求举例

1). 需求描述

Sunny软件公司欲开发一款多人联机对战游戏(类似魔兽世界、星际争霸等游戏),在该游戏中,多个玩家可以加入同一战队组成联盟,当战队中某一成员受到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将作出响应。

2). 类图设计


观察者模式案例.png

3). 完整代码

  1. 抽象观察者
/**
 * 抽象观察者
 */
public interface Observer {

    public String getName();

    public void setName(String name);

    // 声明支援盟友的方法
    public void help();

    // 声明遭受到攻击的方法
    public void beAttacked(AllyControlCenter acc);
}
  1. 具体观察者
/**
 * 具体观察者:陆战队成员
 */
@AllArgsConstructor // lombok插件+依赖
public class Player implements Observer {

    private String name;

    @Override
    public String getName() {
        return this.name;
    }

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

    // 支援盟友的方法实现
    @Override
    public void help() {
        System.out.println("坚持住," + this.name + "来救你!");
    }

    // 遭受到攻击,此时执行特定的陆战队控制中心求救!
    // 通过运行时注入的方式,可以最大程度降低耦合
    @Override
    public void beAttacked(AllyControlCenter acc) {
        System.out.println(this.name + "遭受攻击,请求支援!");
        acc.notifyObserver(this);
    }
}
  1. 抽象目标类
/**
 * 目标抽象类:陆战队控制中心
 */
public abstract class AllyControlCenter {

    // 战队名称
    protected String allyName;

    // 战队成员
    protected List<Observer> players = new ArrayList<>();

    public void setAllyName(String name) {
        this.allyName = name;
    }

    public String getAllyName() {
        return this.allyName;
    }

    // 注册成员
    public void join(Observer obs) {
        System.out.println(obs.getName() + "加入" + this.allyName + "战队");
        players.add(obs);
    }

    public void join(Observer[] ...players) {

    }

    // 声明抽象通知方法
    public abstract void notifyObserver(Observer observer);
}
  1. 具体目标类
/**
 * 具体目标类:具体战队控制中心
 */
public class ConcreteAllyControlCenter extends AllyControlCenter {

    public ConcreteAllyControlCenter(String allyName) {
        System.out.println(allyName + "战队组建成功!\n~~~~~~~~~~~~~~~~~~~~~~~~");
        this.allyName = allyName;
    }

    @Override
    public void notifyObserver(Observer observer) {
        System.out.println(this.allyName + "战队紧急通知,盟友" + observer.getName() + "收到攻击,需要紧急支援");

        // 通知其他观察者,进行帮助
        for (Observer player : players) {
            if (player != observer) {
                player.help();
            }
        }
    }
}
  1. 测试程序
/**
 * 客户端测试代码
 * @author Liucheng
 * @since 2019-09-11
 */
public class Client {

    public static void main(String[] args) {

        // 定义观察目标对象:战队
        AllyControlCenter acc = new ConcreteAllyControlCenter("师徒四人");

        // 定义观察者对象:并将观察者对象添加进战队
        Observer tangseng = new Player("唐僧");
        Observer wukong = new Player("悟空");
        Observer bajie = new Player("八戒");
        Observer shaseng = new Player("沙僧");

        // 唐僧从来不参战!!
        // acc.join(tangseng);
        acc.join(wukong);
        acc.join(bajie);
        acc.join(shaseng);

        // 八戒遭受到攻击
        bajie.beAttacked(acc);
    }
}

三、JDK对观察者模式的支持

观察者模式在Java语言中的地位非常重要。在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持


JDK观察者模式.png

1). 角色介绍

  1. Observer接口: 在java.util.Observer接口中只声明一个方法,它充当抽象观察者,其方法声明代码如下所示:void update(Observable o, Object arg);当观察目标的状态发生变化时,该方法将会被调用,在Observer的子类中将实现update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable的notifyObservers()方法时,将执行观察者类中的update()方法。
  2. Observable类: java.util.Observable类充当观察目标类,在Observable中定义了一个向量Vector来存储观察者对象,它所包含的方法及说明见下表:
方法名 方法描述
Observable() 构造方法,实例化Vector向量。
addObserver(Observer o) 用于注册新的观察者对象到向量中。
deleteObserver(Observer o) 用于删除向量中的某一个观察者对象。
notifyObservers()和notifyObservers(Object arg) 通知方法,用于在方法内部循环调用向量中每一个观察者的update()方法。
deleteObservers() 用于清空向量,即删除向量中所有观察者对象。
setChanged() 该方法被调用后会设置一个boolean类型的内部标记变量changed的值为true,表示观察目标对象的状态发生了变化。
clearChanged() 用于将changed变量的值设为false,表示对象状态不再发生改变或者已经通知了所有的观察者对象,调用了它们的update()方法。
hasChanged() 用于测试对象状态是否改变。
countObservers() 用于返回向量中观察者的数量。

我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,通过使用JDK中的Observer接口和Observable类,可以更加方便地在Java语言中应用观察者模式。

2). 观察者模式与Java事件处理

JDK 1.0及更早版本的事件模型基于职责链模式,但是这种模型不适用于复杂的系统,因此在JDK 1.1及以后的各个版本中,事件处理模型采用基于观察者模式的委派事件模型(DelegationEvent Model, DEM),即一个Java组件所引发的事件并不由引发事件的对象自己来负责处理,而是委派给独立的事件处理对象负责。

在DEM模型中,目标角色(如界面组件)负责发布事件,而观察者角色(事件处理者)可以向目标订阅它所感兴趣的事件。当一个具体目标产生一个事件时,它将通知所有订阅者。事件的发布者称为事件源(Event Source),而订阅者称为事件监听器(Event Listener),在这个过程中还可以通过事件对象(Event Object)来传递与事件相关的信息,可以在事件监听者的实现类中实现事件处理,因此事件监听对象又可以称为事件处理对象。事件源对象、事件监听对象(事件处理对象)和事件对象构成了Java事件处理模型的三要素。事件源对象充当观察目标,而事件监听对象充当观察者。

3). 观察者模式与MVC

在当前流行的MVC(Model-View-Controller)架构中也应用了观察者模式,MVC是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。其中模型可对应于观察者模式中的观察目标,而视图对应于观察者,控制器可充当两者之间的中介者。当模型层的数据发生改变时,视图层将自动改变其显示内容。

四、观察者模式总结

1). 优点

  1. 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。
  2. 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
  3. 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。
  4. 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。

2). 缺点

  1. 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。
  2. 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

3). 适用场景

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

推荐阅读更多精彩内容