【设计模式之 Observer Mode】什么是观察者模式?看过NBA直播你就知道了!

【设计模式】观察者模式

1 场景

一场湖人的NBA的主场球赛将会有多种方式在各个平台直播,有视频直播与文字比分直播。而各个平台需要获取NBA比赛分数的数据,将NBA球馆内的分数显示在各个官方直播平台的比分数据栏上。并且各个官方直播平台会依据基本的数据情况进行各自的展示。

image-20210918151841108

数据中心会依据比赛情况不断更新比赛数据对象。

从数据中心获取数据并不是我们这次所关注的,我们关注的是如何将每次更新比赛数据对象的情况,同步给各个官方直播平台的方式。如何做到松耦合并且并且具备扩展性。

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的概念,观察者订阅了这个主题对象,主题对象就向订阅者推送消息。当不是这个主题对象的订阅者,就不会收到最新的推送。

image-20210918160633757

每个用户有自主的取消订阅与订阅的能力。

3.2 定义与类图

1、定义

观察者模式:定义了对象之间一对多的依赖,当被依赖者(主题对象)改变状态的时候,所有的依赖者对象(订阅者)都会收到依赖对象(主题对象)改变的通知并且自动更新状态。

简而言之,一对多的对象关系,"一"的对象改变状态,其他依赖者(观察者)都感知到。

2、类图

image-20210918163539743

通过抽象的方式,将统一的主题对象接口基本功能抽象出:register注册观察者、remove移除观察者、notify通知观察者(当主题对象状态发生改变通知所有的当前观察者)。

而观察者中抽象出update方法,当订阅的主题状态发生变化,调用update改变需要定订阅的数据。

例如微信公众号有获取最新一篇文章的功能,而update更新的就是最新文章对象。

3.3 松耦合设计优势

两个对象松耦合,但是之间仍然可以交互,只是不知道对方具体的细节,观察者模式是一种对象松耦合的对象设计方式,让主题对象与观察者之间松耦合。

1、主题对象不依赖具体的类

因为站在主题对象角度,主题对象只知道观察者实现了Observer接口。主题对象不需要知道观察者具体的类,在写代码的时候只要用接口来代表观察者,不需要知道具体的实现类。方便观察者增删。

2、新增观察者类型不需要修改主题对象代码

新类型的对象出现,只要实现Observer类,然后注册为观察者,主题对象不在意会直接发送通知给所有实现观察者接口的对象。

松耦合的设计模式可以构建更加有弹性的面向对象系统,更好应对对象的变化,因为对象依赖程度降低了。

4 观察者模式场景实现

4.1 类图

image-20210918175016978

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);
    }
}

测试结果

image-20210923014124558

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

这是【路遥知码力】的第一篇公众号文章,希望能每周输出一篇有质量的公众号文章,欢迎关注交流!感谢阅读!

欢迎微信搜索 路遥知码力 关注公众号!

不积跬步无以至千里,下次见!
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值