【设计模式】观察者模式
1 场景
一场湖人的NBA的主场球赛将会有多种方式在各个平台直播,有视频直播与文字比分直播。而各个平台需要获取NBA比赛分数的数据,将NBA球馆内的分数显示在各个官方直播平台的比分数据栏上。并且各个官方直播平台会依据基本的数据情况进行各自的展示。
数据中心会依据比赛情况不断更新比赛数据对象。
从数据中心获取数据并不是我们这次所关注的,我们关注的是如何将每次更新比赛数据对象的情况,同步给各个官方直播平台的方式。如何做到松耦合并且并且具备扩展性。
2 场景实现
很直观我们需要做的任务就是,每当比赛数据对象被更改的时候,我就去将这些数据通知给各种各样的平台。
伪代码可能长这样:
public class GameData {
// 比分数据
// 总比分
private Score score;
// 比赛时间戳 单位 秒
private Long currentTime;
// 球员得分情况
private PlayersData players;
// 各大平台的比分板实例变量(需要实例化)
private TencentSportBoard txboard;
private CCTVSportBoard cctvBoard;
private WeiboSportBoard WeiboBoard;
public void dataChanged() {
// getXXX() 方法为GameData类中 向数据中心取得最新测量值的方法
Score score = getScore();
Long curTime = getCurrentTime();
PlayersData ps = getPlayersData();
// 更新
txboard.update(score, curTime, ps);
cctvBoard.update(score, curTime, ps);
WeiboBoard.update(score, curTime, ps);
}
// GameData其他方法
}
这样实现方式不好的地方在哪里?
这个GameData更新各个平台的方式,是将具体的平台对象进行更新,这会导致后续如果我们要增加或者去除某个平台的接入,我们都需要去修改这个GameData对象中将更新数据通知出去部分的代码。并且通知的步骤的代码update(score, curTime, ps)是可以被复用的,可以复用封装。
3 观察者模式
3.1 生活栗子
更好一点的实现方式就要使用设计模式中的观察者模式。什么是观察者模式?可以从微信公众号的使用方式理解这个模式。当微信公众号推送了一篇新的文章,微信公众号订阅者都会收到一个新推文的消息。
(1)用户可以订阅微信公众号
(2)微信公众号作者写一篇新文章在微信公众号上发布
(3)微信公众号向所有订阅者推送新文章,所有订阅者会收到该新文章推送
(4)当订阅者不满意该作者的公众号,用户可以取消订阅不会再收到该公众号新文推送
而我们这里可以将微信公众号称为 Subject 主题对象,订阅者称为 Observer 观察者。
观察者模式 = 发布者(主题对象) + 订阅者(观察者)
主题对象中,有微信文章列表的List,当主题对象的数据发生改变的时候,就会将对应的数据发送到观察者的手上。例如微信公众号有新文章,就会将文章推送到观察者手中。
而这个过程并不会感知到具体订阅者是什么对象,在主题对象的视角下,只有订阅者Observer的概念,观察者订阅了这个主题对象,主题对象就向订阅者推送消息。当不是这个主题对象的订阅者,就不会收到最新的推送。
每个用户有自主的取消订阅与订阅的能力。
3.2 定义与类图
1、定义
观察者模式:定义了对象之间一对多的依赖,当被依赖者(主题对象)改变状态的时候,所有的依赖者对象(订阅者)都会收到依赖对象(主题对象)改变的通知并且自动更新状态。
简而言之,一对多的对象关系,"一"的对象改变状态,其他依赖者(观察者)都感知到。
2、类图
通过抽象的方式,将统一的主题对象接口基本功能抽象出:register注册观察者、remove移除观察者、notify通知观察者(当主题对象状态发生改变通知所有的当前观察者)。
而观察者中抽象出update方法,当订阅的主题状态发生变化,调用update改变需要定订阅的数据。
例如微信公众号有获取最新一篇文章的功能,而update更新的就是最新文章对象。
3.3 松耦合设计优势
两个对象松耦合,但是之间仍然可以交互,只是不知道对方具体的细节,观察者模式是一种对象松耦合的对象设计方式,让主题对象与观察者之间松耦合。
1、主题对象不依赖具体的类
因为站在主题对象角度,主题对象只知道观察者实现了Observer接口。主题对象不需要知道观察者具体的类,在写代码的时候只要用接口来代表观察者,不需要知道具体的实现类。方便观察者增删。
2、新增观察者类型不需要修改主题对象代码
新类型的对象出现,只要实现Observer类,然后注册为观察者,主题对象不在意会直接发送通知给所有实现观察者接口的对象。
松耦合的设计模式可以构建更加有弹性的面向对象系统,更好应对对象的变化,因为对象依赖程度降低了。
4 观察者模式场景实现
4.1 类图
4.2 具体实现
1、比赛数据定义
public class Player {
private String name;
private Integer score;
private Integer fouls; // 犯规数
}
public class Score {
private Integer homeTeamScore; // 主队得分情况
private Integer awayTeamScore; // 客队得分情况
}
2、接口实现
(1)主题对象接口
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
(2)观察者对象接口
public interface Observer {
public void update(Score score, Long currentTime, ArrayList<Player> playersData);
}
(3)展示接口
public interface DisplayElement {
public void display();
}
3、GameData主题对象实现主题接口
public class GameData implements Subject{
private ArrayList<Observer> observers;
private Long currentTime;
private ArrayList<Player> playerDatas;
private Score score;
public GameData() {
observers = new ArrayList<Observer>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if(i >= 0) {
observers.remove(i);
}
}
@Override
public void notifyObservers() {
for(int i = 0; i < observers.size(); i++) {
Observer observer = (Observer) observers.get(i);
observer.update(score, currentTime, playerDatas);
}
}
public void dataChanged() {
notifyObservers();
}
public void measureGameData(Score score, Long currentTime, ArrayList<Player> playerDatas) {
this.score = score;
this.currentTime = currentTime;
this.playerDatas = playerDatas;
dataChanged();
}
// 其他方法
}
4、比分牌实现
(1)Tx比分牌
public class TencentSportBoard implements Observer, DisplayElement{
private Long currentTime;
private ArrayList<Player> playerDatas;
private Score score;
private Subject gameData;
public TencentSportBoard(Subject gameData) {
this.gameData = gameData;
// 当前观察者注册到主题对象中
gameData.registerObserver(this);
}
@Override
public void display() {
System.out.println("Here is TencentSport: ");
System.out.println(this.score);
System.out.println(this.currentTime);
for(Player player : playerDatas) {
System.out.println(player);
}
}
@Override
public void update(Score score, Long currentTime, ArrayList<Player> playersData) {
this.currentTime = currentTime;
this.playerDatas = playersData;
this.score = score;
display();
}
}
(2)CCTV比分牌
public class CCTVSportBoard implements Observer, DisplayElement{
private Long currentTime;
private ArrayList<Player> playerDatas;
private Score score;
private Subject gameData;
public CCTVSportBoard(GameData gameData) {
this.gameData = gameData;
gameData.registerObserver(this);
}
@Override
public void display() {
System.out.println("Here is CCTVSport: ");
System.out.println(score);
}
@Override
public void update(Score score, Long currentTime, ArrayList<Player> playersData) {
this.currentTime = currentTime;
this.playerDatas = playersData;
this.score = score;
display();
}
}
(3)weibo比分牌
public class WeiboSportBoard implements Observer, DisplayElement {
private Long currentTime;
private ArrayList<Player> playerDatas;
private Score score;
private Subject gameData;
public WeiboSportBoard(GameData gameData) {
this.gameData = gameData;
gameData.registerObserver(this);
}
@Override
public void display() {
System.out.println("Here is WeiboSport: ");
ArrayList<Player> mvps = getMvp(playerDatas);
for(Player p : mvps) {
System.out.println(p);
}
}
public ArrayList<Player> getMvp(ArrayList<Player> ps) {
int maxScore = 0;
ArrayList<Player> ans = new ArrayList<Player>();
for(int i = 0; i < ps.size(); i++) {
if(maxScore < ps.get(i).getScore()) {
maxScore = ps.get(i).getScore();
ans.clear();
ans.add(ps.get(i));
} else if(maxScore == ps.get(i).getScore()) {
ans.add(ps.get(i));
}
}
return ans;
}
@Override
public void update(Score score, Long currentTime, ArrayList<Player> playersData) {
this.currentTime = currentTime;
this.playerDatas = playersData;
this.score = score;
display();
}
}
5、测试类与测试结果
public class StaplesCenter {
public static void main(String[] args) {
GameData gameData = new GameData();
TencentSportBoard tencentSportBoard = new TencentSportBoard(gameData);
CCTVSportBoard cctvSportBoard = new CCTVSportBoard(gameData);
WeiboSportBoard weiboSportBoard = new WeiboSportBoard(gameData);
Score score = new Score(0, 0);
Long currentTime = 1631966274L;
ArrayList<Player> playerDatas = new ArrayList<Player>();
Player p1 = new Player("Kobe", 0, 0);
playerDatas.add(p1);
gameData.measureGameData(score, currentTime, playerDatas);
System.out.println("=========================");
playerDatas.clear();
p1.setScore(81);
playerDatas.add(p1);
currentTime = 1631966551L;
score.setAwayTeamScore(102);
score.setHomeTeamScore(122);
gameData.measureGameData(score, currentTime, playerDatas);
}
}
输出:
Here is TencentSport:
Score{homeTeamScore=0, awayTeamScore=0}
1631966274
Player{name='Kobe', score=0, fouls=0}
Here is CCTVSport:
Score{homeTeamScore=0, awayTeamScore=0}
Here is WeiboSport:
Player{name='Kobe', score=0, fouls=0}
=========================
Here is TencentSport:
Score{homeTeamScore=122, awayTeamScore=102}
1631966551
Player{name='Kobe', score=81, fouls=0}
Here is CCTVSport:
Score{homeTeamScore=122, awayTeamScore=102}
Here is WeiboSport:
Player{name='Kobe', score=81, fouls=0}
5 Java内置观察者模式
5.1 重写栗子
原来的方式我们是自己实现观察者模式,Subject为我们的主题扩展为Observable接口,观察者从原来自己写的Observer接口改为实现Observaer接口。
在原有的Player、Score对象,以及Display接口保持不变的情况下,我们以TencentSportBoard进行改造。
(1)主题对象继承Observable类
首先我们对GameData这个主题Subject进行重写。
package Observer_util;
import Observer.Observer;
import java.util.ArrayList;
import java.util.Observable;
// 实现Observable 为Subject
public class GameData extends Observable {
private Long currentTime;
private ArrayList<Player> playerDatas;
private Score score;
public GameData() { }
// 不需要追踪、注册、删除观察者(超类已经完成)
public void dataChanged() {
// 在调用notifyObservers() 之前需要调用setChanged来表明数据状态已经改变
setChanged();
// 在明确了数据改变过后才会通知观察者
// 我们没有传输GameData的数据 使用的是“拉” 的方式
notifyObservers();
}
public void measureGameData(Score score, Long currentTime, ArrayList<Player> playerDatas) {
this.score = score;
this.currentTime = currentTime;
this.playerDatas = playerDatas;
dataChanged();
}
// 这种写法需要使用“拉”的方式 所以观察者会使用这个方法来获取GameData的数据
public Long getCurrentTime() {
return currentTime;
}
public ArrayList<Player> getPlayerDatas() {
return playerDatas;
}
public Score getScore() {
return score;
}
}
我们对比原来的gameData中的measureGameData方法调用dataChanged()为一个个观察者进行更新,是一种直接将修改的数据"推"给observer的方式。
自己实现的推的方式:
@Override
public void notifyObservers() {
for(int i = 0; i < observers.size(); i++) {
Observer observer = (Observer) observers.get(i);
observer.update(score, currentTime, playerDatas);
}
}
public void dataChanged() {
notifyObservers();
}
public void measureGameData(Score score, Long currentTime, ArrayList<Player> playerDatas) {
this.score = score;
this.currentTime = currentTime;
this.playerDatas = playerDatas;
dataChanged();
}
而现在是交由Observable进行setChange通知发生改变,再进行notifyObsers()。由父类进行完成信息的更新,没有传入我们修改的参数,而是在需要使用的时候将obs对象"拉"过来。
(2)观察者实现Observer接口
package Observer_util;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;
// 观察者需要实现java.util内置的Observer
public class TencentSportBoard implements Observer, DisplayElement {
// 需要定义Observable类属性 在构造器启动的时候将参数初始化
Observable observable;
private Long currentTime;
private ArrayList<Player> playerDatas;
private Score score;
// 构造器 需要一个Observer当做参数 并且将TencentSportBoard.observable作为参数传入
public TencentSportBoard(Observable observable) {
this.observable = observable;
// 将当前对象登记注册为观察者
observable.addObserver(this);
}
/**
* 这里是以"拉"的方式表示将数据从Observable(Subject)拉取的
* @param obs 对象为Observable对象
* @param arg 数据对象
*/
@Override
public void update(Observable obs, Object arg) {
// 先确定这个数据对象是GameData类型 然后利用get方法获取对应的数据 并且展示
if(obs instanceof GameData) {
GameData gameData = (GameData) obs;
this.currentTime = gameData.getCurrentTime();
this.playerDatas = gameData.getPlayerDatas();
this.score = gameData.getScore();
display();
}
}
@Override
public void display() {
System.out.println("Here is TencentSport: ");
System.out.println(this.score);
System.out.println(this.currentTime);
for(Player player : playerDatas) {
System.out.println(player);
}
}
}
这里着重看update的方法,实际上是Observable对象发生了数据更新,会调用update方法,使得观察者拉取到了最新的主题对象,从而进行展示。数据是带在obs对象上,没有直接将数据参数带过来。
(3)测试类StaplesCenter
package Observer_util;
import java.util.ArrayList;
public class StaplesCenter {
public static void main(String[] args) {
GameData gameData = new GameData();
TencentSportBoard tencentSportBoard = new TencentSportBoard(gameData);
Score score = new Score(0, 0);
Long currentTime = 1631966274L;
ArrayList<Player> playerDatas = new ArrayList<Player>();
Player p1 = new Player("Kobe", 0, 0);
playerDatas.add(p1);
gameData.measureGameData(score, currentTime, playerDatas);
}
}
测试结果
5.2 Observer类与Observable类分析
1、Observer类分析
public interface Observer {
// 需要实现这个接口完成成为观察者 所以观察者要有 Observable observable的成员属性
void update(Observable e, Object arg);
}
GameDate在初始化的时候会把这个主题Observable在初始化观察者的时候传入 相当于将观察者注册到了这个主题下面。
// 观察者需要实现java.util内置的Observer
public class TencentSportBoard implements Observer, DisplayElement {
// 需要定义Observable类属性 在构造器启动的时候将参数初始化
Observable observable;
private Long currentTime;
private ArrayList<Player> playerDatas;
private Score score;
// 构造器 需要一个Observer当做参数 并且将TencentSportBoard.observable作为参数传入
public TencentSportBoard(Observable observable) {
this.observable = observable;
// 将当前对象登记注册为观察者
observable.addObserver(this);
}
...
}
在构造函数的时候会把observable初始化
public class StaplesCenter {
public static void main(String[] args) {
GameData gameData = new GameData();
TencentSportBoard tencentSportBoard = new TencentSportBoard(gameData);
...
}
}
2、Observable类分析
源码内容与注释分析:
public class Observable {
// 用于标记当前主题的数据是否被更改了 主题数据如果被更改要先setChanged() 将这个flag变为true代表被修改了
private boolean changed = false;
// 把所有的观察者对象都存在这个Vector里面 通过这样的方式统一通知观察者
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
// 观察者调用 将观察者自己注册到主题Observable类obs对象当中
public synchronized void addObserver(Observer o) {
if(o == null) {
throw new NullPointerException();
} (!obs.contains(o)) {
obs.addElement(o);
}
}
// 观察者可以将自己从订阅的主题去掉
publc synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
// 主题对象GameData可以调用父类的方法 进行对观察者的统一更新数据
public void notifyObservers() {
// 通知所有的观察者
notifyObservers(null);
}
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized(this) {
if(!changed) {
// 如果没更改就直接返回 不会出现数据更新的通知
return;
}
// 将注册的观察者vector列表转换为数组对象
arrLocal = obs.toArray();
// 将数据更改恢复为false 可见通知观察者是一次性的
clearChanged();
}
for(int i = arrLocal.length - 1; i >= 0; i--) {
// 此处实际有数据的是this,而参数arg实际上为null 这里为实际上推送观察者进行update的地方
// 思考这个的arrLocal的对象的次序就不会是我们加入的次序的方式,这里和toArray的方式顺序有关系。
((Observer) arrLocal[i]).update(this, arg);
}
}
public synchronized void deleteObservers() {
obs.removeAllElements();
}
// protected 的变量可以同一个包中的其他类访问
// 声明为protected类型是为了只能在继承的时候访问从基类继承的protected方法 但是不能访问这个基类的实例对象的protected的方法。只能继承使用,不能外部实例对象调用。 下同
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
public synchronized boolean hasChanged() {
return changed;
}
public synchronized int countObservers() {
return obs.size();
}
}
Observable是一个类,所以主题在使用这个对象的时候,只能继承下来,如果某个类想要实现主题需要继承Observable同时又想拥有某个其他超类的能力,就会出现困难,Java不支持多继承。所以Observable的复用潜力有所下降,而我们使用设计模式的目标之一就是提升代码的复用能力。
并且因为Observable没有接口类,自己无法建立实现。这里注意setChanged方法使用protected类型,这导致你必须继承这个Observable,否则不能创建自己的实例对象调用这个protected方法,无法组合使用。而面向对象设计原则:“面向接口编程,不针对实现编程“,”多用组合,少用继承“ 也与这样的方式有所违背。
你也可以自己实现一套观察者模式,善于自己灵活运用,增加扩展性。
6 小结
(1)观察者模式完成对象一对多的信息传达方式,Subject(Observable可观测者)使用统一的接口来更新观察者Observer。
(2)观察者和可观察者松耦合的方式,可观察者不知道观察者的细节只知道实现了观察者的接口(update方法)(留心推和拉的方式)
(3)有Java内置类和自己实现的方式,各有优劣,主要是掌握设计的思想。
(4)面向对象的原则:封装。多用组合少用继承。针对接口编程,不针对实现编程。松耦合,提高复用率。
(5)一句话描述观察者模式:在对象之间定义一对多的依赖,当一个对象发生变化,依赖这个对象的观察者都会收到通知并且更新。
在其他的一些框架与应用场景中也使用了观察者模式,大家可以主动探索与发现。
参考文献:
《Head First 设计模式》
完整代码已上传github:https://github.com/gaolijiemathcs/DesignPatterns
这是【路遥知码力】的第一篇公众号文章,希望能每周输出一篇有质量的公众号文章,欢迎关注交流!感谢阅读!
欢迎微信搜索 路遥知码力 关注公众号!
不积跬步无以至千里,下次见!