关于设计模式:一文彻底搞懂观察者模式Observer-Pattern

2次阅读

共计 4749 个字符,预计需要花费 12 分钟才能阅读完成。

文章已收录我的仓库:Java 学习笔记与收费书籍分享

设计用意

定义对象间的一种一对多的依赖关系,当一个对象的状态产生扭转时,所有依赖于它的对象都失去告诉并被自动更新。

在理论设计开发中,咱们通常会升高类与类之间的耦合度,这样可能会产生一个副作用:因为类与类被宰割,咱们难以保护类之间的一致性。

举一个常见的例子,咱们对用户显示数学饼状图是须要数据撑持的,例如上面这张东京奥运会金牌榜:

在开发中,这张图表分为两个局部,一个是视图局部,也就是以饼状图呈现出的样子,一个是数据局部,即各国的金牌数量,因为咱们将数据与视图抽离,因而一旦数据局部更新,视图局部得不到最新的数据,难以维持一致性,这个时候咱们须要一个时刻关注数据变动的观察者,一旦观察者感知到数据变动则立刻更新视图,咱们能够让视图自身作为一个观察者,但这样设计是不好的,视图类该当做好设计视图的事而无需插手其余工作,更好的方法是独自拆散出一个观察者类以保护两个类之间的一致性,这就是观察者模式的设计用意。

在理论例子中,这种模式利用十分宽泛,例如一旦小说更新将会主动订阅,一旦会员过期将会主动续费,MVC 三层模式中的控制器就会察看视图并实时更新模型局部 …… 观察者模式是利用最宽泛的模式之一。

设计

实现观察者模式时要留神具体指标对象和具体观察者对象之间不能间接调用,否则将使两者之间严密耦合起来,这违反了面向对象的设计准则。

观察者模式的次要角色如下。

  1. 形象主题(Subject)角色:也叫形象指标类或指标接口类,它提供了一个用于保留观察者对象的汇集类和减少、删除观察者对象的办法,以及告诉所有观察者的形象办法。
  2. 具体主题(Concrete Subject)(被察看指标)角色:也叫具体指标类,它是被察看的指标,它实现形象指标中的告诉办法,当具体主题的外部状态产生扭转时,告诉所有注册过的观察者对象。
  3. 观察者接口(Observer)角色:它是一个抽象类或接口,它蕴含了一个更新本人的形象办法,当接到具体主题的更改告诉时被调用。
  4. 具体观察者(Concrete Observer)角色:实现形象观察者中定义的形象办法,以便在失去指标的更改告诉时更新本身的状态。

设计接口 (形象) 是惯例的设计思维:定义一个接口由子类实现。这样做利于后续扩大,但如果确定只有一个被察看对象,则没有必要设计接口 (形象) 类。

常见的设计是:观察者到被察看指标中注册注销,通知它有一个观察者正在察看他,如有变动请告诉,随后察看指标发生变化,则告诉所有注册登录过的观察者并通知本人的身份(观察者可能察看多个指标,某些时候它必须晓得具体是那个指标产生了变动),随后观察者更新相应数据。

代码示例

咱们思考上述数据与视图之间的例子,这里假如咱们的视图接管谷歌数据源与百度数据源:

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

// 视图类
class View {
    // 通过简单转换将数据可视化,这里简略的打印
    public void show(Object data) {System.out.println(data);
    }
}

// 定义抽象类 数据源类
abstract class DataSource {
    // 相干的源数据
    protected String data = "";

    // 存储曾经注册过的观察者
    protected List<Observer> observers = new ArrayList<>();

    // 获取该数据
    public String getData() {return data;}

    // 观察者到这里注册,被观察者保留观察者信息
    public void addObserver(Observer observer) {observers.add(observer);
    }
    // 移除更改观察者就不写了

    // 接口办法,更新数据,由指标类告诉观察者
    abstract protected void updateData(String newData);

    // 接口办法,告诉观察者,由子类采纳不同的办法实现
    abstract public void notifyObserver();}

// 数据源类的具体实现之一,百度数据源类
class BaiduDataSource extends DataSource {
    @Override
    protected void updateData(String newData) {
        // 如果数据发生变化,则更新数据并告诉观察者
        if (!newData.equals(data)) {
            // 这一步是必须的,在告诉观察者前肯定要实现变动
            // 这就好比你今天才登程可你却通知你的好敌人明天走,你的好敌人来接你没看到你,情谊破碎
            // 必须要放弃状态的一致性
            data = newData;
            notifyObserver();}
    }

    @Override
    public void notifyObserver() {
        // 播送音讯,并告知观察者本人是谁
        for (var observer : observers) {observer.update(this, data);
        }
    }
}

// 数据源类的具体实现之一,谷歌数据源类
class GoogleDataSource extends DataSource {
    @Override
    protected void updateData(String newData) {
        // 如果数据发生变化,则更新数据并告诉观察者
        if (!newData.equals(data)) {
            // 必须要放弃状态的一致性
            data = newData;
            notifyObserver();}
    }

    @Override
    public void notifyObserver() {
        // 播送音讯,并告知观察者本人是谁
        for (var observer : observers) {observer.update(this, data);
        }
    }
}

// 观察者接口
interface Observer {
    /**
     * 更新操作
     * @param ds    察看的具体数据源
     * @param data  更新的数据
     */
    void update(DataSource ds, String data);
}

// 观察者 A
class ObserverA implements Observer {
    // 由 view 示例委托察看数据源
    private View view;

    public ObserverA(View view) {this.view = view;}

    @Override
    public void update(DataSource ds, String data) {System.out.println("察看到" + ds.getClass().getSimpleName() + "发生变化,更新视图");
        // 更新视图 View
        view.show(data);
    }
}


// 测试类
public class Test {public static void main(String[] args) {
        // 定义视图类
        View view = new View();
        view.show("初始状态");

        System.out.println();

        // 定义与 view 相干数据源
        DataSource bds = new BaiduDataSource();// 百度数据源
        DataSource gds = new GoogleDataSource();// 谷歌数据源

        // 为 view 增加察看数据源的观察者
        Observer observer = new ObserverA(view);

        // 观察者须要到到数据源类中注册
        bds.addObserver(observer);
        gds.addObserver(observer);

        // 手动更新数据
        bds.updateData("这是百度新数据 --" + new Date());
        System.out.println();
        gds.updateData("这是谷歌新数据 --" + new Date());
    }
}
// 输入
/*
初始状态

察看到 BaiduDataSource 发生变化,更新视图
这是百度新数据 --Fri Jul 30 10:43:55 CST 2021

察看到 GoogleDataSource 发生变化,更新视图
这是谷歌新数据 --Fri Jul 30 10:43:55 CST 2021
*/

探讨与优化

咱们围绕下面的代码示例来探讨。

  • 在发送给告诉给观察者前,保护本身状态一致性是很重要的,在下面的代码中咱们必须要先更新数据在发送告诉,就像例子说的,你明明要等到今天才登程,可你却告诉你的好敌人马上就走走,这样总会引起一些不好的后果。
  • 上述代码只设置了一个观察者,理论中可能有多个观察者,可是观察者之间却又相互不晓得彼此的存在,这就可能会造成反复更新的甚至更重大的问题,咱们必须要好好设置观察者,以保障它们在性能上不具备重复性。事实上,当观察者越来越多时,代码会变得更加难以扩大保护。
  • 上述代码中咱们让观察者保留了 View 的实例,理论的更新还是由该实例本人来实现,这是合乎观察者模式的定义的。但实际上,经常会由观察者本身来更新相干数据。
  • 观察者可能察看多个指标,因而当指标告诉观察者时应该告知观察者它本人是谁,以便观察者做出相应操作,实现的方法就是指标将本身传入观察者办法的参数中。这样是合乎常理的——观察者正在察看 5 岁、6 岁、7 岁的人较量跑步,一旦呈现达到起点则观察者颁发奖状,不同年龄的人评奖准则也是不同的,所以观察者必须晓得到底是谁实现较量。
  • 上述代码中一旦有变动则告诉所有的观察者——只管有些观察者对这些音讯并不感兴趣,当观察者较多时,效率是很低的,咱们应该只告诉那些对该变动感兴趣的观察者们,咱们能够定义一个 Aspect 类示意该变动的特点,能够采纳哈希表保留观察者:

    Map<Aspect, List<Observer>> map = new HashMap<>();

    观察者注册时,必须外表本人对那些方面的变动感兴趣:

    public void addObserver(Aspect aspect, Observer observer) {map.put(aspect, observer);
    }

其余

在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只有实现它们的子类就能够编写观察者模式实例。咱们来剖析次要的类与它们的性能:

1. Observable 类

Observable 类是形象指标类,它有一个 Vector 向量,用于保留所有要告诉的观察者对象,上面来介绍它最重要的 3 个办法。

  1. void addObserver(Observer o) 办法:用于将新的观察者对象增加到向量中。
  2. void notifyObservers(Object arg) 办法:调用向量中的所有观察者对象的 update() 办法,告诉它们数据产生扭转。通常越晚退出向量的观察者越先失去告诉。
  3. void setChange() 办法:用来设置一个 boolean 类型的外部标记位,注明指标对象产生了变动。当它为真时,notifyObservers() 才会告诉观察者。

2. Observer 接口

Observer 接口是形象观察者,它监督指标对象的变动,当指标对象发生变化时,观察者失去告诉,并调用 void update(Observable o,Object arg) 办法,进行相应的工作。

事实上这一套类曾经太老了,效率比拟低,不倡议应用。

总结

观察者模式是一种对象行为型模式,其次要长处如下。

  1. 升高了指标与观察者之间的耦合关系,两者之间是形象耦合关系。合乎依赖倒置准则。
  2. 指标与观察者之间建设了一套触发机制。

它的次要毛病如下。

  1. 指标与观察者之间的依赖关系并没有齐全解除,而且有可能呈现循环援用。
  2. 当观察者对象很多时,告诉的发布会破费很多工夫,影响程序的效率,并且可能会导致意外的更新。
正文完
 0