介绍
定义对象之间的一种一对多的依赖关系,当一个对象发生改变的时候,所有依赖于它的对象都会得到通知并且被自动更新。
定义
定义对象之间的一种一对多的依赖关系,当一个对象的状态发生了改变的时候,所有依赖于它的对象都通知并被自动更新
观察者模式可以处理这种问题:
- 多个订阅者:
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字段中保存有观察NumberGenerator的ObserveraddObserver(NumberGenerator generator)方法用于注册Observer,而deleteObserver方法用于删除 ObservernotifyObserver()会向所有的 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 及通知所有观察者的功能,通过维护观察者列表实现。观察者注册需调用 Subject 的 addObserver() 或者类似作用的方法。
| 组件 | 简介 | 功能 |
|---|---|---|
| Subject (目标对象) | 观察对象。定义了注册观察者和删除观察者的方法,声明了获取现在的状态的方法 | 1. 可以被多个观察者观察 2. 提供了对观察者注册和退订的维护 3. 当目标的状态变化的时候,负责通知所注册的、有效的观察者 |
| ConcreteSubject (具体的目标实现对象) | 自身状态变化之后,会通知所有已经注册的 Observer 角色 | 维护目标状态,当目标状态改变,通知所有注册有效的观察者,让观察者执行相应的处理 |
| Observer (观察者的接口) | 负责接收来自 Subject 角色的状态变化的通知 | 提供目标通知时对应的更新方法,可以在这个方法里面 ⌈回调目标对象(推荐,也有其他方案)⌋,以获取目标对象的数据 |
| ConcreteObserver (观察者的具体实现对象) | 表示具体的 Observer。当它的 update 方法被调用之后,会去获取要观察的对象的最新状态。 | 用来接收目标的通知,并且进行相应的后续处理 |
好处
观察对象无需关心观察自己的是哪一种具体观察类。
因为这些都是通过 addObserver() 注册的,确保了它们一定都实现了 Observer接口,因此它们肯定都实现了 update方法。可以通知并调用其 update实现
同时具体观察者类,比如 DigitObserver 类仅知道它们是观察对象的子类,并且持有 getNumber()方法
注意
- 注册的 Observer 角色顺序会涌向这些 Observer 具体实现类的 update 方法调用顺序;
- 由于 Observer 角色并非主动的去观察,而是被动地接受来自 Subject 角色的通知,因此 Observer 模式也被称为
Publish-Subscribe(发布 - 订阅)模式 - 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) {
.....
//拉模式
}
.....
}
入门案例扩展
- 编写一个
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:
====
***********************....
*/
}
- 增加一个
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 设计
由于输入限制,类统一不添加修饰符
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());
}
}
小结 - 观察者模式
实现要点
- 具体目标
ConcreteSubject要能够维护观察者的注册信息 - 具体目标
ConcreteSubject需要维护引起通知的状态 - 具体观察者
ConcreteObserver需要能接收目标的通知 - 如果一个观察观察着多个目标,那么更新方法里面需要判断时哪一个目标的通知
- 命名规范,目标接口的定义,建议在名称后面跟
Subject;观察者接口的定义,建议在名称后面加Observer
优缺点
优点
- 实现了目标类和具体的观察者类之间解耦
- 观察者模式对观察者实行注册管理,可以实现动态联动
- 支持目标类进行广播通信
缺点
- 可能会引起一些冗余操纵。每次都是广播通信,不管观察者是否需要,每个观察者都会被调用
update方法,如果观察者不需要执行相应处理,那么这次操作就浪费了。
适用场景
-
当抽象模型中的两个部分相互依赖,一方的状态变化影响另一方。当目标对象状态改变时,所有观察者自动更新。提高了代码的独立性和复用性。
-
对于需要同时修改一个对象及其数量不确定的相关联对象的情况。直接修改的对象作为目标对象,受间接影响的对象作为观察者。这样设计能灵活处理未知数量的对象变更。
-
如果一个对象需向多个其他对象发送通知且希望保持低耦合。发起通知的对象是目标对象,接收并响应通知的对象为观察者。
观察者模式推拉模型
推模型
目标对象主动向观察者推送目标的详细信息
推模型的观察者接口
需要把传递的数据直接推给观察者对象
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方法接收整个目标对象作为参数,能够更灵活地适应多种场景,从而增强了代码的通用性和可维护性。
参考
- 《图解设计模式 - Observer》
- 行为型 - 观察者(Observer)
- 【行为型模式十八】观察者模式(Observer)
- Carson带你学设计模式:观察者模式(Observer)