设计模式——2_6 观察者(Observer)

这世界没有一件事情是虚空而生的,站在光里,背后就会有阴影,这深夜里一片寂静,是因为你还没有听见声音

——马良《坦白书》

定义

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

二十年前如果你想知道明天的天气怎么样,你用不着隔几分钟就到气象台问问有没有最新的情报,而是可以在气象台登记一下你的电话号码。每当有最新的天气预报发布的时候,气象台会自动给所有进行登记过的电话号码发短信(这里面就包括你的)

这种模式其实就是观察者模式,而你就是被记录在册的观察者




图纸

在这里插入图片描述




一个例子:在RPG游戏里应对善变的天气

假定现在我们有一个RPG对战游戏,有这样的设定:

  • 天气分为:晴天、大雾和下雨
  • 玩家可以选择火元素、水元素或者风元素的骑士
  • 骑士一定是在某个区域内活动,而区域有对应的天气。每种元素的骑士在不同的天气下会变化自己的属性



定义元素

很显然,天气是区域的一种属性,就像这样:

在这里插入图片描述

Area & Weather
/**
 * 骑士可以活动的区域
 */
public class Area {

    /**
     * 当前区域的天气
     */
    private Weather weather;

    public Area(Weather weather) {
        this.weather = weather;
    }

    public Weather getWeather() {
        return weather;
    }

    public void setWeather(Weather weather) {
        this.weather = weather;
    }
}

public enum Weather {

	sunny,fog,rain
}

骑士也应当有自己的类簇,就像这样:

在这里插入图片描述

/**
 * 骑士
 */
public class Knight {

    /**
     * 攻击力
     */
    private int attack;

    /**
     * 生命值
     */
    private int healthPoint;

    /**
     * 骑士名称
     */
    private String name;

    /**
     * 骑士所在区域
     */
    private Area area;

    public Knight(String name, Area area) {
        this.name = name;

        setAttack(10);//默认10点攻击力
        setHealthPoint(100);//默认100点生命值
      	setArea(area);
    }

    public int getAttack() {
        return attack;
    }

    public void setAttack(int attack) {
        this.attack = attack;
    }

    public int getHealthPoint() {
        return healthPoint;
    }

    public void setHealthPoint(int healthPoint) {
        this.healthPoint = healthPoint;
    }

    public String getName() {
        return name;
    }

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

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }
    
    @Override
    public String toString() {
        return String.format("%s:攻击力=%s", name, attack);
    }
}

/**
 * 火属性骑士
 */
public class FireKnight extends Knight {

    public FireKnight(Area area) {
        super("火属性骑士", area);
    }
}

/**
 * 风属性骑士
 */
public class WindKnight extends Knight {

    public WindKnight(Area area) {
        super("风属性骑士", area);
    }
}

/**
 * 水属性骑士
 */
public class WaterKnight extends Knight {

    public WaterKnight(Area area) {
        super("水属性骑士", area);
    }
}

我们创建了天气的枚举 Weather 用来表示所有当前可能出现的天气,并把 Weather 作为 Area 的内部属性

对于骑士,我们创建 Knight 根类用于存放所有的骑士都有的一些属性,再根据不同的元素分出三个子类


现在我们实现了前两步,至于最后一步,想必我们需要在 AreaKnight 之间建立一些联系



给 Area 和 Knight 建立联系

那你会说了,不对啊,Knight 里面有自己当前所处的 Area 的引用,这不就是很好的联系吗?

Knight 里面的引用,实现出来的效果是这样的:

在这里插入图片描述


/**
 * 骑士
 */
public class Knight {

    ……

    public void setArea(Area area) {
        this.area = area;
        updateByWeather();
    }

    protected void updateByWeather(){
        //不实现,也不强制子类实现她,所以留空
    }
    
    /**
     * 把属性复原
     */
    protected void reset(){
        setAttack(10);
    }
}

/**
 * 火属性骑士
 */
public class FireKnight extends Knight {

    public FireKnight(Area area) {
        super("火属性骑士", area);
    }

    @Override
    protected void updateByWeather() {
        reset();

        Weather weather = getArea().getWeather();
        if (weather.equals(Weather.sunny)) {
            //如果是晴天,攻击力+10
            setAttack(getAttack() + 10);
        } else if (weather.equals(Weather.rain)) {
            //如果是雨天,攻击力减半
            setAttack(getAttack() / 2);
        }
    }
}

/**
 * 风属性骑士
 */
public class WindKnight extends Knight {

    public WindKnight(Area area) {
        super("风属性骑士", area);
    }

    //什么天气都跟他没关系 所以不需要重写
}

/**
 * 水属性骑士
 */
public class WaterKnight extends Knight {

    public WaterKnight(Area area) {
        super("水属性骑士", area);
    }

    @Override
    protected void updateByWeather() {
        reset();

        Weather weather = getArea().getWeather();
        if (weather.equals(Weather.rain) || weather.equals(Weather.fog)) {
            //如果是雾天或者下雨,攻击力翻倍
            setAttack(getAttack() * 2);
        } else if (weather.equals(Weather.sunny)) {
            //如果是晴天,攻击力降为1
            setAttack(1);
        }
    }
}

采用这种方案,我们在 Knight 为自己设定 Area 的时候就读取了天气信息,同时更新自己的属性,使用 updateByWeather 方法



善变的天气

可是问题很快出现了,我们玩这个游戏的时候发现,所有的玩家都会根据即将进入的区域选择合适的骑士,没有人蠢到故意在晴天选水骑士,或者在下雨时选火骑士

所以为了增加可玩性,我们设定了第四点需求:

  • 一个区域内的天气不是一成不变的,他会进行随机的变化

想法很好,实践起来却有点麻烦了

根据前面的设计,我们在set Area 的时候变化了自己的属性,之后 Area 里面的 Weather 会如何变化,Knight 是不知道的


怎么让他知道呢?我们有两种方案:

  1. Knight 里面添加一个定时器,固定时间去查 Area 里面的 Weather 属性,如果出现了变化,更新自己
  2. 想个办法让 Area 在更新 Weather 的时候去通知 Knight,让 Knight 及时更新

一看就知道后者明显优于前者,那能做到吗?

可以的,就像我们之前注册迭代器一样。我们只需要在 Area 里面维护一个 Knight 列表,然后在 set Weather 的时候通知 Knight 就完事了,就像这样:

在这里插入图片描述

/**
 * 骑士
 */
public class Knight {

    ……
        
    public void setArea(Area area) {
        //注销
        if (this.area != null) {
            this.area.removeKnight(this);
        }

        this.area = area;

        //注册
        area.addKnight(this);

        //第一次执行
        updateByWeather();
    }

    public void update(){
        updateByWeather();
    }
}

/**
 * 骑士可以活动的区域
 */
public class Area {
    
    ……

    public void setWeather(Weather weather) {
        this.weather = weather;
        notifyKnight();
    }

    private final List<Knight> knightList = new ArrayList<>();

    public void addKnight(Knight knight){
        knightList.add(knight);
    }

    public void removeKnight(Knight knight){
        knightList.remove(knight);
    }

    public void notifyKnight(){
        for (Knight knight : knightList) {
            knight.update();
        }
    }
}

我们让 Area 去维护一个 knightList ,并在 Knight 设定 Area 的时候把自己写到 knightList 里面去。这就实现了一个可以从 Area 发指令给 Knight 的通道。接着,我们需要发送指令的时候,可以通过 notifyKnight 方法通知 knightList 里面所有的 Knight

这样一来,第四点需求得以实现,就像这样:

public static void main(String[] args) {
	Area area = new Area(Weather.sunny);

	Knight fire = new FireKnight(area);
	Knight wind = new WindKnight(area);
	Knight water = new WaterKnight(area);

	System.out.println("晴天");
	System.out.printf("%s \n%s \n%s \n", fire, wind, water);

	System.out.println("*************************************************************");

	area.setWeather(Weather.rain);
	System.out.println("雨天");
	System.out.printf("%s \n%s \n%s \n", fire, wind, water);
}

在这里插入图片描述

而这正是一个标准的观察者实现


观察者的结构和原理简单到一眼就能望到头,但是这个简单的结构解决了无数个问题。这有点像多线程里面的 生产-消费者模型,也是结构简单但极其实用。也许这就是大道至简吧




碎碎念

定时器的方案一无是处吗?

其实并不是的,上例的情况是因为 KnightArea 可以双向主动向对方发起请求,所以可以用观察者,但是很多时候连接是单向的

比如说 http,这就是个无状态协议,除非用一些比较特殊的手法(比如 WebSocket),否则服务器是没办法主动向客户机发送请求的

这时候如果你有时效性不那么高的类似请求(游戏的时效性要求肯定不允许你用定时器),那么定时器和长连接就是你需要考虑的解决方案了



观察者和中介者

到了行为型模式这一篇,其实有很多模式关注的内容是类似的

比如前面讲过的 职责链(Chain of Responsibility)命令(Command)

职责链和命令都是通过参数化请求,以求实现请求者和处理器之间的解耦

之后还会提到的 状态模式(State)策略模式(Strategy)

状态模式简直就是策略模式水里的倒影


以及现在要讲的 观察者(Observer)中介者(Mediator)

观察者和中介者都是为对象和对象之间通讯而存在的

这种通讯相当于,对象A执行了某个动作(在面向对象中其实就是某个函数被调用),对象B就要针对这个行为执行自己的动作

这就像自行车的主动轮和从动轮之间的关系


假定我们现在有A和B两个对象,A发出通知,B接收A的通知并执行操作

在这种情况下,如果让A直接调用B,那就意味着他们之间建立紧耦合;如果想要解耦,那么对象之间的通信方式基本上有两种

  1. 在A和B之间建立一个平台,让A和B都去跟平台打交道而不知道对方的存在,这个平台就是 中介者
  2. 在A里面,维护一个监听者列表,形成一个 1→N 的关系,这时候我会把B写入A的监听者列表里(这时候建立的是抽象耦合)。当发生某个事件的时候,A会通知所有监听者进行更新(其中就包含B),这时候的B就是 观察者

先说两种方式的共同点,两种做法都可以解除A和B之间的紧耦合。A可以不知道这个通知会被传递到哪里去,可以不知道B的数量,甚至可以不知道B的具体类型


但两种设计模式又各有千秋:

  • 中介者 内部的对象没有明确的主次,任何对象都可以通过平台发出信息或对某个信息进行响应
  • 观察者 不需要这个平台,subject和observer之间存在明确的主次关系,信息传递的方向也永远是 S u b j e c t → O b s e r v e r Subject → Observer SubjectObserver

这是他们好的一面,而他们的缺点和优点一样明显:

  • 中介者 的平台随着所要维护的对象数量增加,需要处理的关联也越来越多,这最终会让中介者平台变成一个庞然大物,所有的关联都集中到一种,最终形成一个错综复杂的线团,把他理清是很痛苦的事情

  • 观察者 不需要第三方平台,这是便利,也是缺陷。因为subject和observer都对对方太不了解了,所以在后期维护的时候,如果不了解程序结构的人调用了subject里某个会在observer里产生副作用的方法,程序可能出现一些诡异的行为,而很难发现是哪个观察者的问题。这些诡异的行为包括但不限于:

    • 调用一个更新数据的方法,但另一个看似风马牛不相及的视图也被更新了

    • subject通知observer进行操作,但是observer又会调用subject里的行为,到最后形成死循环

      那你会说,我又不是傻,为什么要这样写?

      现实是这种情况时有发生,因为整个项目的结构里不会只有 subject 和 observer 这两者。也许你通过subject 通知 observer 后,observer 又会去调用其他对象,其他对象又调用其他对象,以此往复,最终跑回 subject 来

在四人组的设计模式“圣经”里面,对观察者和中介者有这样一段描写:

我们发现生成可复用的 Observer 和 Subject 比生成可复用的 Mediator 容易一些。观察者模式有利于 Observer 和 Subject 之间的切割和松耦合,同时这将产生颗粒度更细从而更易于复用的类

——《设计模式·可复用面向对象软件的基础》 华章版 P258

这段话怎么理解呢?观察者比中介者更好用?

当然不是,作为相互竞争的两种模式,如果存在单方面的碾压,那另一个设计模式根本就没有存在的必要性

出现上文中的问题的关键,是内聚和解耦

中介者是高内聚的,他内部的对象的每个行为的信息接收方都是明确的,那就意味着对一个 Mediator 来说,他关联的对象都是明确的,从外部看,中介者+内部的对象 就是一个整体。所以我们说中介者适合用来描述表单内的元素之间的关联,因为对表单来说一个表单就应该是一个整体

观察者不是这样的,observer和subject之间可以说是无关的。这种无关性让观察者在项目中出现时理解起来更复杂,但实现了两者之间的解耦



可以抽象出来的Subject和Observer

你可能发现了,其实所有在观察者模式中发出信息的 变化主体,或者说 subject,在维护观察者列表的时候,都需要三个方法:

  1. addObserver

    增加观察者

  2. removeObserver

    删除观察者

  3. notifyObserver

    通知观察者进行更新

在观察者,或者说Observer里,则需要用于更新的 update 方法


既然有通用的部分,那我们其实就可以把他们抽象出来,就像这样:

/**
* 被观察者 主体
**/
public class Subject {

    private final List<Observer> observerList = new ArrayList<>();

    public void notifyObserver() {
        for (Observer observer : observerList) {
            observer.update(this);
        }
    }

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

    public void removeObserver(Observer observer) {
        observerList.remove(observer);
    }
}

/**
* 观察者
**/
public interface Observer {

    void update(Subject subject);
}

在Java里面,Subject和Observer甚至不需要你自己写,因为在 java.util 里面就有对应的工具类,可以直接继承

虽然说从Java9 开始这玩意就废弃了,我也是写这个的时候才发现

在这里插入图片描述


不过值得一提的是,Observer 通常可以作为接口存在,但如果你需要 Subject 帮你维护观察者列表,那么 Subject 至少得是一个抽象类,那就只能用继承

在Java这个单继承的语言里,使用继承要慎重,就比如上例的 Area,就算我有 Subject 工具类,我也不会让 Area 去继承她,我宁可自己写

因为这会破坏整体的语法,区域怎么可能是主体的子类呢?这会让继承我的代码的后辈产生误解,这虽然只是编程风格的问题,但是我坚信细节决定成败,所以该抠的地方还是严谨一点的好




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
观察者模式是一种常见的设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。在这个模式中,被观察者对象通常称为主题(Subject),而观察者对象通常称为观察者Observer)。 下面我们就以一个简单的天气预报系统为例来介绍观察者模式的使用。 首先,我们需要定义一个主题接口(Subject),它包含了添加、删除和通知观察者的方法: ```java public interface Subject { public void registerObserver(Observer o); public void removeObserver(Observer o); public void notifyObservers(); } ``` 然后,我们需要定义一个观察者接口(Observer),它包含了更新数据的方法: ```java public interface Observer { public void update(float temp, float humidity, float pressure); } ``` 接下来,我们需要定义一个具体的主题类(WeatherData),它实现了主题接口,并包含了一个列表来存储观察者对象,以及当前的温度、湿度和气压等数据: ```java import java.util.ArrayList; public class WeatherData implements Subject { private ArrayList<Observer> observers; private float temperature; private float humidity; private float pressure; public WeatherData() { observers = new ArrayList<Observer>(); } public void registerObserver(Observer o) { observers.add(o); } public void removeObserver(Observer o) { int i = observers.indexOf(o); if (i >= 0) { observers.remove(i); } } public void notifyObservers() { for (int i = 0; i < observers.size(); i++) { Observer observer = (Observer)observers.get(i); observer.update(temperature, humidity, pressure); } } public void measurementsChanged() { notifyObservers(); } public void setMeasurements(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; measurementsChanged(); } // other WeatherData methods here } ``` 最后,我们需要定义一个具体的观察者类(CurrentConditionsDisplay),它实现了观察者接口,并在更新数据时打印出当前的温度、湿度和气压等信息: ```java public class CurrentConditionsDisplay implements Observer { private float temperature; private float humidity; private Subject weatherData; public CurrentConditionsDisplay(Subject weatherData) { this.weatherData = weatherData; weatherData.registerObserver(this); } public void update(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; display(); } public void display() { System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity"); } } ``` 现在,我们可以创建一个天气预报系统,它包含了一个主题对象和一个观察者对象,并通过调用主题对象的方法来更新数据和通知观察者: ```java public class WeatherStation { public static void main(String[] args) { WeatherData weatherData = new WeatherData(); CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); weatherData.setMeasurements(80, 65, 30.4f); weatherData.setMeasurements(82, 70, 29.2f); weatherData.setMeasurements(78, 90, 29.2f); } } ``` 以上就是一个简单的观察者模式的例子,它可以让我们更好地理解和应用这个常见的设计模式

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乡亲们啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值