品设计模式 - (行为型) 观察者模式 Observer

251 阅读15分钟
这里写图片替代文字

介绍

定义对象之间的一种一对多的依赖关系,当一个对象发生改变的时候,所有依赖于它的对象都会得到通知并且被自动更新。

定义

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

观察者模式可以处理这种问题:

  • 多个订阅者: Observer
  • 多个观察者: Subject

解决了 常变对象不常变对象 之间的依赖关系。两者存在依赖关系的前提下,不常变对象 需要随着 常变对象经常改变逻辑的问题。

目标和观察者之间是典型的一对多关系也可以是一对一

观察者和目标是单向依赖,即: 只有观察者依赖于目标,而目标不依赖于观察者

需要符合单向依赖关系: 只有观察者依赖于目标,而目标是不会依赖于观察者的。

本质

触发联动


快速入门

问题描述

观察者观察一个会生成数值的对象,并且将它生成的数值结果显示出来。

但是不同的观察者的显示方式不一样。

DigitObserver 会以数字形式显示数值;

而 GraphObserver 则会以简单的图示形式来显示数值

UML图设计

画板

代码实现

Observer 接口

观察者接口,将观察一个会生成数值的对象,并将它生成的数值结果显示出来。

具体观察者会实现这个接口

public interface Observer {

    void update(NumberGenerator numberGenerator);
}

具体监听者 ConcreteObserver

public class DigitObserver implements Observer {
    @Override
    public void update(NumberGenerator numberGenerator) {
        System.out.println(this.getClass().getSimpleName() + ":" + numberGenerator.getNumber());

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
    }
}
public class GraphObserver implements Observer {
    @Override
    public void update(NumberGenerator numberGenerator) {
        System.out.println(this.getClass().getSimpleName() + ":" + "\n ===== ");
        int cnt = numberGenerator.getNumber();
        for (int i = 0; i < cnt; i++) {
            System.out.println("***");
        }
        System.out.println(" ===== ");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
    }
}

NumberGenerator 类

用于生成数值的抽象类。生成数值的方法和获取数值的方法都是抽象类

字段和方法解析:

  • observers 字段中保存有观察 NumberGeneratorObserver
  • addObserver(NumberGenerator generator) 方法用于注册 Observer,而 deleteObserver 方法用于删除 Observer
  • notifyObserver()会向所有的 Observer 发送通知,该方法会调用每个 Observer 的 update 方法
public abstract class NumberGenerator {

    //保存 Observer 实例
    private List<Observer> observers = new ArrayList<>();

    //注册 Observer 实例
    public void add(Observer observer) {
        observers.add(observer);
    }

    //删除 Observer 实例
    public void deleteObserver(Observer observer) {
        observers.remove(observer);
    }

    //向 Observer 实例发送通知
    public void notifyAllObservers() {
        for (Observer observer : observers) {
            observer.update(this);
        }
    }

    //获取数值
    public abstract int getNumber();

    //执行更新并通知
    public abstract void execute();
}

RandomNumberGenerator 类

生成随机数,getNumber 方法用于获取 number 字段的值

execute 方法会生成 20 个随机数字,并且通过 notifyObservers() 方法把每次生成结果通知给观察者。

public class RandomNumberGenerator extends NumberGenerator {

    private Random random = new Random();
    private int number;

    @Override
    public int getNumber() {
        return number;
    }

    @Override
    public void execute() {
        for (int i = 0; i < 5; i++) {
            number = random.nextInt(20);
            super.notifyAllObservers();     // 通知所有监听者
        }
    }
}

执行结果

public class Main {

    public static void main(String[] args) {

        NumberGenerator randomGenerator = new RandomNumberGenerator();

        // 创建监听者
        DigitObserver digitObserver = new DigitObserver();
        GraphObserver graphObserver = new GraphObserver();

        // 注册监听者
        randomGenerator.add(digitObserver);
        randomGenerator.add(graphObserver);

        randomGenerator.execute();

        /*
         输出:
         DigitObserver:1
         GraphObserver:
          =====
         ***
          =====
         DigitObserver:0
         GraphObserver:
          =====
          =====
         */
    }
}

介绍:观察模式 - 类图

被观察目标 Subject 有注册和移除观察者 Observer 及通知所有观察者的功能,通过维护观察者列表实现。观察者注册需调用 SubjectaddObserver() 或者类似作用的方法。

画板

组件简介功能
Subject (目标对象)观察对象。定义了注册观察者和删除观察者的方法,声明了获取现在的状态的方法1. 可以被多个观察者观察
2. 提供了对观察者注册和退订的维护
3. 当目标的状态变化的时候,负责通知所注册的、有效的观察者
ConcreteSubject (具体的目标实现对象)自身状态变化之后,会通知所有已经注册的 Observer 角色维护目标状态,当目标状态改变,通知所有注册有效的观察者,让观察者执行相应的处理
Observer (观察者的接口)负责接收来自 Subject 角色的状态变化的通知提供目标通知时对应的更新方法,可以在这个方法里面 ⌈回调目标对象(推荐,也有其他方案)⌋,以获取目标对象的数据
ConcreteObserver
(观察者的具体实现对象)
表示具体的 Observer。当它的 update 方法被调用之后,会去获取要观察的对象的最新状态。用来接收目标的通知,并且进行相应的后续处理

好处

观察对象无需关心观察自己的是哪一种具体观察类。

因为这些都是通过 addObserver() 注册的,确保了它们一定都实现了 Observer接口,因此它们肯定都实现了 update方法。可以通知并调用其 update实现

同时具体观察者类,比如 DigitObserver 类仅知道它们是观察对象的子类,并且持有 getNumber()方法

注意

  1. 注册的 Observer 角色顺序会涌向这些 Observer 具体实现类的 update 方法调用顺序;
  2. 由于 Observer 角色并非主动的去观察,而是被动地接受来自 Subject 角色的通知,因此 Observer 模式也被称为 Publish-Subscribe(发布 - 订阅)模式
  3. Java 内部提供了 Observer 模式的;基于 java.util.Observer 接口和 java.util.Observable。但是并不好用,并且已经于 JDK 9 中被标记为过时,因为 Java 是单一继承机制的,如果目标想要成为 Subject 的角色已经是某个类的子类了,它将无法继承 Observable

传递更新信息的方式

第一种: 上述实现中,update() 的实现形式为:仅传递了 Subject 角色作为参数。也被称为 拉模型

void update(NumberGenerator numberGenerator);

第二种形式:除了传递 Subject 角色之外,还传递了 Observer 所需的数据

void update(NumberGenerator numberGenerator, int number);

好处:省去了 Observer 自己获取数据的麻烦。

不足:Subject 角色就会知道 Observer 所要进行的处理的内容了; 会降低程序的灵活性,比如,假设现在我们需要传递上次数值和当前数值的差值,那么就必须要在 Subject 角色内先计算出这个差值

第三种形式: 仅传递数值

也被称为 推模型

void update(int number);

降低了 Observer 的复用性。不同的 Subject 角色的具体实现类传入的 number可能处理逻辑不同

[模板] - 目标对象,被观察者

Subject

/**
 * 目标对象,作为被观察者
 */
public class Subject {
    /**
     * 用来保存注册的观察者对象,也就是报纸的订阅者
     */
    private List<Observer> observers = new ArrayList<Observer>();

    /**
     * 注册
     * @param observer 观察者
     * @return 是否注册成功
     */
    public void add(Observer observer) {
       readers.add(observer);
    }
    /**
     * 取消订阅
     * @param observer 观察者
     * @return 是否取消成功
     */
    public void delete(Observer observer) {
       readers.remove(observer);
    }
    
    /**
     * 通知每个订阅的观察者
     */
    protected void notifyObservers() {
       for(Observer observer : observers){
           reader.update(this);
       }
    }
}

[模板] - 具体目标对象

public class ConcreteSubject extends Subject{
    /**
     * 具体内容
     */
    private String content;
    
    /**
     * 获取具体内容
     * @return 报纸的具体内容
     */
    public String getContent() {
       return content;
    }

    /**
     * 示意,设置报纸的具体内容,相当于要出版报纸了
     * @param content 报纸的具体内容
     */
    public void setContent(String content) {
       this.content = content;
       //内容有了,说明又出报纸了,那就通知所有的读者
       notifyObservers();
    }
}

[模板] - 观察者

/**
 * 观察者,比如报纸的读者
 */
public interface Observer {
    
    /**
     * 被通知的方法
     * @param subject 具体的目标对象,可以获取报纸的内容
     */
    public void update(Subject subject);

}

[模板] - 具体观察者

public class ConcreteObserver implements Observer {
    ....
    
    public void update(Subject subject) {
        .....
        //拉模式
    }

    .....
}

入门案例扩展

  1. 编写一个 NumberGenerator类,具有数值递增功能的子类 IncrementalNumberGenerator
    • 构造函数参数: 初始数值 + 结束数值 + 递增步长

代码实现

IncrementalNumberGenerator

public class IncrementalNumberGenerator extends NumberGenerator {

    //初始数值
    private int initNum;

    //结束数值
    private int boundNum;

    //递增步长
    private int step;

    private int num;

    public IncrementalNumberGenerator(int initNum, int boundNum, int step) {
        this.initNum = initNum;
        this.boundNum = boundNum;
        this.step = step;
        this.num = initNum;
        if(this.step <= 0 || this.boundNum <= 0) {
            throw new RuntimeException("Initial num cannot be lower than zero!");
        }
    }

    @Override
    public int getNumber() {
        return num;
    }

    @Override
    public void execute() {
        //不断更新
        while (this.num <= this.boundNum) {
            super.notifyAllObservers();
            if (this.num > this.boundNum) {
                this.num = this.boundNum;
            } else {
                //更新递增步长
                this.num += this.step;
            }
        }
    }
}

修改 GraphObserver, 改为一行输出 *方便观察

测试

    @Test
    public void testIncremental() {
        NumberGenerator incrementalGenerator = new IncrementalNumberGenerator(10, 100, 20);

        // 创建监听者
        DigitObserver digitObserver = new DigitObserver();
        GraphObserver graphObserver = new GraphObserver();

        //注册监听者
        incrementalGenerator.add(digitObserver);
        incrementalGenerator.add(graphObserver);

        incrementalGenerator.execute();
        
        /*
            DigitObserver:10
            GraphObserver:
             ==== 
            ****************************** ===== 
            DigitObserver:30
            GraphObserver:
             ==== 
            ***********************************************....
            DigitObserver:50
            GraphObserver:
             ==== 
            ***********************************....
            DigitObserver:70
            GraphObserver:
             ==== 
            ********************************....
            DigitObserver:90
            GraphObserver:
             ==== 
            ***********************....
         */
    }
  1. 增加一个 ConcreteObserver角色,修改 Main 使用该角色

新增 DotObserver,输出 .

public class DigitObserver implements Observer {
    @Override
    public void update(NumberGenerator numberGenerator) {
        System.out.println(this.getClass().getSimpleName() + ":" + numberGenerator.getNumber());

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
    }
}

修改 Main

    public static void main(String[] args) {

        NumberGenerator randomGenerator = new RandomNumberGenerator();

        // 创建监听者
        DigitObserver digitObserver = new DigitObserver();
        GraphObserver graphObserver = new GraphObserver();
        // 新增监听者
        DotObserver dotObserver = new DotObserver();

        // 注册监听者
        randomGenerator.add(digitObserver);
        randomGenerator.add(graphObserver);
        randomGenerator.add(dotObserver);

        randomGenerator.execute();

        /*
            DigitObserver:17
            GraphObserver:
             ==== 
            ***************************************************
            =======
            DotObserver:
            ====
            . 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	
            ========
            DigitObserver:15
            GraphObserver:
             ==== 
            *********************************************
            =======
            DotObserver:
            ====
            . 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	
            ========
            DigitObserver:18
            GraphObserver:
             ==== 
            ******************************************************
            =======
            DotObserver:
            ====
            . 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	
            ========
            DigitObserver:1
            GraphObserver:
             ==== 
            ***
            =======
            DotObserver:
            ====
            . 	
            ========
            DigitObserver:4
            GraphObserver:
             ==== 
            ************
            =======
            DotObserver:
            ====
            . 	. 	. 	. 	
            ========
         */
    }

巩固案例

题目来源: 卡码网KamaCoder

原题链接:设计模式专题之观察者模式

题目描述

小明所在的学校有一个时钟(主题),每到整点时,它就会通知所有的学生(观察者)当前的时间,请你使用观察者模式实现这个时钟通知系统。

注意点:时间从 0 开始,并每隔一个小时更新一次。

输入输出示例

UML 设计

image.png

由于输入限制,类统一不添加修饰符 public;在同一个文件内完成

代码实现

观察对象 AlertTrigger

abstract class AlertTrigger {
    
    //观察者当前数目
    private int size;   
    
    //观察者最大数目
    private int limit;
    
    //存储观察者集合
    private List<Student> students = new ArrayList<>();
    
    public void addStudent(Student student) {
        if(size == limit) {
            throw new RuntimeException("Exceed max observers!");
        }
        students.add(student);
        size++;
    }
    
    public void deleteStudent(Student student) {
        students.remove(student);
    }
    
    //通知所有监听者
    protected void notifyAllStudents() {
        for(Student s : students) {
            //传入观察目标
            s.alert(this);
        }
    }
    
    public void setSize(int size){
        this.size = size;
    }
    
    public void setLimit(int limit) {
        this.limit = limit;
    }
    
    // 获取当前数目
    public int getSize() {
        return this.size;
    }
    
    // 获取最大数目
    public int getLimit(){
        return this.limit;
    }
    
    // 获取时间方式
    protected abstract int getTime();
    
    // 更新
    protected abstract void update();
    
}

观察者 Observer

// 观察者
interface Observer {
    
    //回调观察对象,实现提醒
    void alert(AlertTrigger alertTrigger);
}

具体观察对象 TimeAlertTrigger

class TimeAlertTrigger extends AlertTrigger {
    
    //时间,对于子类可见
    private int time = 0;
    
    //初始化父类属性
    TimeAlertTrigger(int limit) {
        super.setLimit(limit);
        super.setSize(0);
    }
    
    @Override
    protected int getTime() {
        return this.time % 12;
    }
    
    @Override
    protected void update() {
        this.time++;       //每次更新 + 1
        super.notifyAllStudents();
    }
}

具体观察者

// 具体观察者
class Student implements Observer{
    
    private String name;
    
    public Student(String name) {
        this.name = name;
    }
    
    public String getName() {
        return this.name;
    }
    
    @Override
    public void alert(AlertTrigger alertTrigger) {
        //回调观察对象,动态绑定具体观察对象的方法
        System.out.println(getName() + " " + alertTrigger.getTime());	
    }
}

Client 端

public class Main {
    
    public static void main(String[] args) {
        
        Scanner scanner = new Scanner(System.in);
        int total = Integer.valueOf(scanner.nextLine());
        
        AlertTrigger alertTrigger = new TimeAlertTrigger(total);
        
        
        for(int i = 0; i < total; i++) {
            //创建具体观察者
            String name = scanner.nextLine();
            Student student = new Student(name);
            //注册    
            alertTrigger.addStudent(student);
        }
        
        int updateTimes = Integer.valueOf(scanner.nextLine());
        for(int i = 0; i < updateTimes; i++) {
            alertTrigger.update();
        }
    }
}

完整代码

import java.util.*;

public class Main {
    
    public static void main(String[] args) {
        
        Scanner scanner = new Scanner(System.in);
        int total = Integer.valueOf(scanner.nextLine());
        
        AlertTrigger alertTrigger = new TimeAlertTrigger(total);
        
        
        for(int i = 0; i < total; i++) {
            //创建具体观察者
            String name = scanner.nextLine();
            Student student = new Student(name);
            //注册    
            alertTrigger.addStudent(student);
        }
        
        int updateTimes = Integer.valueOf(scanner.nextLine());
        for(int i = 0; i < updateTimes; i++) {
            alertTrigger.update();
        }
    }
}


abstract class AlertTrigger {
    
    //观察者当前数目
    private int size;   
    
    //观察者最大数目
    private int limit;
    
    //时间,对于子类可见
    protected int time = 0;
    
    //存储观察者集合
    private List<Student> students = new ArrayList<>();
    
    public void addStudent(Student student) {
        if(size == limit) {
            throw new RuntimeException("Exceed max observers!");
        }
        students.add(student);
        size++;
    }
    
    public void deleteStudent(Student student) {
        students.remove(student);
    }
    
    //通知所有监听者
    public void notifyAllStudents() {
        for(Student s : students) {
            //传入观察目标
            s.alert(this);
        }
    }
    
    public void setSize(int size){
        this.size = size;
    }
    
    public void setLimit(int limit) {
        this.limit = limit;
    }
    
    // 获取当前数目
    public int getSize() {
        return this.size;
    }
    
    // 获取最大数目
    public int getLimit(){
        return this.limit;
    }
    
    // 获取时间方式
    protected abstract int getTime();
    
    // 更新
    protected abstract void update();
    
}

class TimeAlertTrigger extends AlertTrigger {
    
    //初始化父类属性
    TimeAlertTrigger(int limit) {
        super.setLimit(limit);
        super.setSize(0);
    }
    
    @Override
    protected int getTime() {
        return super.time % 12;
    }
    
    @Override
    protected void update() {
        super.time++;       //每次更新 + 1
        super.notifyAllStudents();
    }
}


// 观察者
interface Observer {
    
    //方案二: 回调观察对象实现提醒时间
    void alert(AlertTrigger alertTrigger);
}


// 具体观察者
class Student implements Observer{
    
    private String name;
    
    public Student(String name) {
        this.name = name;
    }
    
    public String getName() {
        return this.name;
    }
    
    @Override
    public void alert(AlertTrigger alertTrigger) {
        System.out.println(getName() + " " + alertTrigger.getTime());
    }
}

小结 - 观察者模式

实现要点

  1. 具体目标 ConcreteSubject要能够维护观察者的注册信息
  2. 具体目标 ConcreteSubject需要维护引起通知的状态
  3. 具体观察者 ConcreteObserver 需要能接收目标的通知
  4. 如果一个观察观察着多个目标,那么更新方法里面需要判断时哪一个目标的通知
  5. 命名规范,目标接口的定义,建议在名称后面跟 Subject;观察者接口的定义,建议在名称后面加 Observer

优缺点

优点

  1. 实现了目标类和具体的观察者类之间解耦
  2. 观察者模式对观察者实行注册管理,可以实现动态联动
  3. 支持目标类进行广播通信

缺点

  1. 可能会引起一些冗余操纵。每次都是广播通信,不管观察者是否需要,每个观察者都会被调用 update方法,如果观察者不需要执行相应处理,那么这次操作就浪费了。

适用场景

  1. 当抽象模型中的两个部分相互依赖,一方的状态变化影响另一方。当目标对象状态改变时,所有观察者自动更新。提高了代码的独立性和复用性。

  2. 对于需要同时修改一个对象及其数量不确定的相关联对象的情况。直接修改的对象作为目标对象,受间接影响的对象作为观察者。这样设计能灵活处理未知数量的对象变更。

  3. 如果一个对象需向多个其他对象发送通知且希望保持低耦合。发起通知的对象是目标对象,接收并响应通知的对象为观察者。

观察者模式推拉模型

推模型

目标对象主动向观察者推送目标的详细信息

推模型的观察者接口

需要把传递的数据直接推给观察者对象

public interface Observer {
    
    /**
     * 被通知的方式,直接把内容推送
     * @param content 内容
     */
    public void update(String content);

}

推模型的观察者的具体实现

直接接收传入的数据

public class ConcreteObserver implements Observer {
    ....

    //推模式
    public void update(String content) {
        .....
    }

    .....
}

推模型的目标对象

  • 注意 notifyObservers 方法,传入具体 content
/**
 * 目标对象,作为被观察者
 */
public class Subject {
    /**
     * 用来保存注册的观察者对象,也就是报纸的订阅者
     */
    private List<Observer> observers = new ArrayList<Observer>();

    /**
     * 注册
     * @param observer 观察者
     * @return 是否注册成功
     */
    public void add(Observer observer) {
       readers.add(observer);
    }
    /**
     * 取消订阅
     * @param observer 观察者
     * @return 是否取消成功
     */
    public void delete(Observer observer) {
       readers.remove(observer);
    }
    
    /**
     * 通知每个订阅的观察者
     */
    protected void notifyObservers(String content) {
       for(Observer observer : observers){
           reader.update(content);
       }
    }
}

推模型的具体目标实现

public class ConcreteSubject extends Subject{
    /**
     * 具体内容
     */
    private String content;
    
    /**
     * 获取具体内容
     * @return 报纸的具体内容
     */
    public String getContent() {
       return content;
    }

    /**
     * 示意,设置报纸的具体内容,相当于要出版报纸了
     * @param content 报纸的具体内容
     */
    public void setContent(String content) {
       this.content = content;
       //内容有了,说明又出报纸了,那就通知所有的读者
       notifyObservers(content);
    }
}

拉模型

目标对象在通知观察者的时候,回调观察目标。借助具体目标实现

拉模型的观察者接口

public interface Observer {
    
    /**
     * 被通知的方式,直接把内容推送
     * @param content 内容
     */
    public void update(Subject subject);

}

拉模型的具体观察者实现

public class ConcreteObserver implements Observer {
    ....

    //推模式
    public void update(Subject subject) {
        .....
    }

    .....
}

拉模型的目标对象

  • 注意 notifyObservers 方法; 传入 this
/**
 * 目标对象,作为被观察者
 */
public class Subject {
    /**
     * 用来保存注册的观察者对象,也就是报纸的订阅者
     */
    private List<Observer> observers = new ArrayList<Observer>();

    /**
     * 注册
     * @param observer 观察者
     * @return 是否注册成功
     */
    public void add(Observer observer) {
       readers.add(observer);
    }
    /**
     * 取消订阅
     * @param observer 观察者
     * @return 是否取消成功
     */
    public void delete(Observer observer) {
       readers.remove(observer);
    }
    
    /**
     * 通知每个订阅的观察者
     */
    protected void notifyObservers() {
       for(Observer observer : observers){
           reader.update(this);
       }
    }
}

推模型的具体目标实现

public class ConcreteSubject extends Subject{
    /**
     * 具体内容
     */
    private String content;
    
    /**
     * 获取具体内容
     * @return 报纸的具体内容
     */
    public String getContent() {
       return content;
    }

    /**
     * 示意,设置报纸的具体内容,相当于要出版报纸了
     * @param content 报纸的具体内容
     */
    public void setContent() {
       this.content = content;
       //内容有了,说明又出报纸了,那就通知所有的读者
       notifyObservers();
    }
}

关于两种模型的比较

在讨论观察者模式的实现方式时,可以将模型分为两种:推送(Push)模型和拉取(Pull)模型。

  • 推送模型:在这种机制下,目标对象明确知晓观察者所需的具体数据,并直接向观察者提供这些信息。

  • 拉取模型:相比之下,在拉取机制中,目标对象并不预先知道观察者需要哪些具体的数据。相反,它传递自身的引用给观察者,允许后者根据自身需求从中提取相关信息。

采用推送模型可能会降低观察者的复用性,因为每个update方法都是为特定需求定制的;遇到新场景时,可能需要重新编写或大幅修改这些方法。相比之下,拉取模型通过让update方法接收整个目标对象作为参数,能够更灵活地适应多种场景,从而增强了代码的通用性和可维护性。


参考

  1. 《图解设计模式 - Observer》
  2. 行为型 - 观察者(Observer)
  3. 【行为型模式十八】观察者模式(Observer)
  4. Carson带你学设计模式:观察者模式(Observer)