您现在的位置是:首页 >学无止境 >设计模式之观察者模式网站首页学无止境

设计模式之观察者模式

King Gigi. 2024-06-19 13:56:19
简介设计模式之观察者模式

观察者模式

1、基本介绍

观察者模式(Observer Design Pattern):

  • 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会得到通知并自动更新。

说人话:

  • 也叫发布订阅模式(推模式)

一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)

观察者主要应用于通知的场景,并且通知的对象可能需要频繁的改变。它可以解耦被观察者与观察者。

观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、redis的发布订阅,本质上都是观察者模式。

1、场景推导

业务需求:在一个游戏中,两个英雄角色,分别有hp、mp,游戏界面中分别有游戏英雄和游戏面板来展示该角色的hp、mp

在这里插入图片描述

当敌方英雄攻击我方英雄的时候,我方英雄的血条会变化

我们看一下这个业务怎么实现

package com.hh.demo.designpattern.d;
//我方英雄
class Role{
    private String name;
    private Integer hp;
    private Integer mp;
    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        //当hp 发生变化时,必须要通知2个地方:1.血条 2.面板
        System.out.println("我方英雄:"+ getName());
        System.out.println("血条更新为:"+ hp);
        System.out.println("面板更新为:"+ hp);
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
//敌方英雄
class Enemy{
    public void attack(Role role){
        role.setHp(role.getHp() - 10);
    }
}
public class AppTest {
    public static void main(String[] args) {
        Role role = new Role();
        Enemy enemy = new Enemy();

        role.setName("艾琳");
        role.setHp(100);
        role.setMp(100);
        //攻击
        enemy.attack(role);

    }
    /**
     * 我方英雄:艾琳
     * 血条更新为:100
     * 面板更新为:100
     * 我方英雄:艾琳
     * 血条更新为:90
     * 面板更新为:90
     *
     * Process finished with exit code 0
     */
}

变化来了,产品经理要求在游戏界面新增加一个球形面板来显示英雄的血条和蓝条

在这里插入图片描述

怎么办呢?改源代码?
违反开闭原则,还违反了单一职责原则

这个时候,我们看看观察者模式是怎么解决的。

package com.hh.demo.designpattern.d;

//我方英雄
class Role{
    private String name;
    private Integer hp;
    private Integer mp;
	//集合
    private List<Observer> observers = new ArrayList();
	//操作集合的方法
    public void addObservers(Observer observer){
        observers.add(observer);
    }

    public void removeObservers(Object obj){
        observers.remove(obj);
    }
	//通知
    public void notifyObservers(){
        for (Observer observer : observers) {
            observer.update(hp);
        }
    }
    public Integer getHp() {
        return hp;
    }
	//当 hp 变化,就给 hp 属性赋值
    public void setHp(Integer hp) {
        this.hp = hp;
        //每当hp发生变化,都通知所有观察者
        notifyObservers();
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

interface Observer{
    //需要一个方法,来接收主体的数据
    public void update(int hp);
}
//游戏面板
class Panel implements Observer{

    @Override
    public void update(int hp) {
        System.out.println("在游戏面板中更新数据:"+ hp);
    }
}
//球形面板
class BallPanel implements Observer{

    @Override
    public void update(int hp) {
        System.out.println("在球形面板中更新数据:"+ hp);
    }
}
//英雄头顶
class HeaderPanel implements Observer{

    @Override
    public void update(int hp) {
        System.out.println("在英雄头顶上更新数据:"+ hp);
    }
}

//敌方英雄
class Enemy{
    public void attack(Role role){
        role.setHp(role.getHp() - 10);
    }
}
public class AppTest {
    public static void main(String[] args) {
        Role role = new Role();
        Enemy enemy = new Enemy();

        role.setName("艾琳");
        role.setHp(100);
        role.setMp(100);
        
        Panel panel = new Panel();
        BallPanel ballPanel = new BallPanel();
        HeaderPanel headerPanel = new HeaderPanel();

        role.addObservers(panel);
        role.addObservers(ballPanel);
        role.addObservers(headerPanel);
        //攻击
        enemy.attack(role);
        enemy.attack(role);
        enemy.attack(role);
    }
     /**
     * 在游戏面板中更新数据:90
     * 在球形面板中更新数据:90
     * 在英雄头顶上更新数据:90
     * 在游戏面板中更新数据:80
     * 在球形面板中更新数据:80
     * 在英雄头顶上更新数据:80
     * 在游戏面板中更新数据:70
     * 在球形面板中更新数据:70
     * 在英雄头顶上更新数据:70
     *
     * Process finished with exit code 0
     */
}

上面代码做到了解耦

  • 当添加一个新的面板显示数据时,不会违反开闭原则;

  • 每个面板的算法,都被隔离在不同的类中了,这也符合了单一职责原则

想想目前这个设计的缺点是什么?

  • 目前主体只会把自己的 hp 广播给所有的观察者,那么如果想把 mp 也一起广播呢?必然要违反开闭原则
  • 而且游戏业务经常变化,经常加入新的玩法,导致Role类的属性越来越多,难道每次多一个属性,都要修改Observer的update 方法吗?

那有的同学就讲了,我直接将Role传进去就好啦,没错,我们看看怎么玩

package com.hh.demo.designpattern.d;

//我方英雄
class Role{
    private String name;
    private Integer hp;
    private Integer mp;

    private List<Observer> observers = new ArrayList();

    public void addObservers(Observer observer){
        observers.add(observer);
    }

    public void removeObservers(Object obj){
        observers.remove(obj);
    }

    public void notifyObservers(){
        for (Observer observer : observers) {
            //这里传this,而不是role,role是在后面定义的
            observer.update(this);
        }
    }
    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        //每当hp发生变化,都通知所有观察者
        notifyObservers();
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Role{" +
                "name='" + name + ''' +
                ", hp=" + hp +
                ", mp=" + mp +
                '}';
    }
}

interface Observer{
    //需要一个方法,来接收主体的数据
    public void update(Role role);
}


class Panel implements Observer{

    @Override
    public void update(Role role) {
        System.out.println("在游戏面板中更新数据:"+ role);
    }
}

class BallPanel implements Observer{

    @Override
    public void update(Role role) {
        System.out.println("在球形面板中更新数据:"+ role);
    }
}

class HeaderPanel implements Observer{

    @Override
    public void update(Role role) {
        System.out.println("在英雄头顶上更新数据:"+ role);
    }
}

//敌方英雄
class Enemy{
    public void attack(Role role){
        role.setHp(role.getHp() - 10);
    }
}
public class AppTest {
    public static void main(String[] args) {
        Role role = new Role();
        Enemy enemy = new Enemy();

        role.setName("艾琳");
        role.setHp(100);
        role.setMp(100);

        Panel panel = new Panel();
        BallPanel ballPanel = new BallPanel();
        HeaderPanel headerPanel = new HeaderPanel();

        role.addObservers(panel);
        role.addObservers(ballPanel);
        role.addObservers(headerPanel);
        //攻击
        enemy.attack(role);
    }
    /**
     * 在游戏面板中更新数据:Role{name='艾琳', hp=90, mp=100}
     * 在球形面板中更新数据:Role{name='艾琳', hp=90, mp=100}
     * 在英雄头顶上更新数据:Role{name='艾琳', hp=90, mp=100}
     *
     * Process finished with exit code 0
     */
}

目前:每当主体的状态发生变化,都会把主体整个对象的属性都广播给所有观察者

缺点是什么

  • 作为一个接口,Observer接口中的update方法,居然出现了具体类名,这违反了依赖倒置原则

    interface Observer{
        //需要一个方法,来接收主体的数据
        //这是不是意味着,所有观察者只能观察Role角色了?
        public void update(Role role);
    }
    

    这是不是意味着,所有观察者只能观察Role角色了?那我想观察天气,又或者我想观察?呢?

我们来看看怎么优雅的解决这个问题

package com.hh.demo.designpattern.d;

//我方英雄
class Role{
    private String name;
    private Integer hp;
    private Integer mp;

    private List<Observer> observers = new ArrayList();

    public void addObservers(Observer observer){
        observers.add(observer);
    }

    public void removeObservers(Object obj){
        observers.remove(obj);
    }

    public void notifyObservers(){
        for (Observer observer : observers) {
            observer.update();
        }
    }
    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        //每当hp发生变化,都通知所有观察者
        notifyObservers();
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Role{" +
                "name='" + name + ''' +
                ", hp=" + hp +
                ", mp=" + mp +
                '}';
    }
}

interface Observer{
    //需要一个方法,来接收主体的数据
    public void update();
}

class Panel implements Observer{
    //你想观察谁,直接将谁接进来
    private Role role;

    public Panel(Role role) {
        this.role = role;
    }

    @Override
    public void update() {
        System.out.println("在游戏面板中更新数据:"+ role);
    }
}

class BallPanel implements Observer{

    private Role role;

    public BallPanel(Role role) {
        this.role = role;
    }
    @Override
    public void update() {
        System.out.println("在球形面板中更新数据:"+ role);
    }
}

class HeaderPanel implements Observer{

    private Role role;

    public HeaderPanel(Role role) {
        this.role = role;
    }
    @Override
    public void update() {
        System.out.println("在英雄头顶上更新数据:"+ role);
    }
}

//敌方英雄
class Enemy{
    public void attack(Role role){
        role.setHp(role.getHp() - 10);
    }
}
public class AppTest {
    public static void main(String[] args) {
        Role role = new Role();
        Enemy enemy = new Enemy();

        role.setName("艾琳");
        role.setHp(100);
        role.setMp(100);
        //观察者将主体包起来
        Panel panel = new Panel(role);
        BallPanel ballPanel = new BallPanel(role);
        HeaderPanel headerPanel = new HeaderPanel(role);
        //主体加上每一个观察者
        role.addObservers(panel);
        role.addObservers(ballPanel);
        role.addObservers(headerPanel);
        //攻击
        enemy.attack(role);
    }
    /**
     * 在游戏面板中更新数据:Role{name='艾琳', hp=90, mp=100}
     * 在球形面板中更新数据:Role{name='艾琳', hp=90, mp=100}
     * 在英雄头顶上更新数据:Role{name='艾琳', hp=90, mp=100}
     *
     * Process finished with exit code 0
     */
}

我直接什么都不传,彻底抽象

此时,Panel 、BallPanel、HeadPanel都是专门观察主体的,想观察谁,直接接进来;每个观察者new的时候,将主体传进去;

这样做到了双向交互。

每个观察者都拥有主题的引用,通过引用能得到主体的变化;

这样就实现了接口之间的解耦。

2、气象站的栗子

业务场景:气象站需要检测 温度、湿度、气压,并将这些数据传到用户的手机,以及用户家的窗户上 ,方便用户及时了解到天气状况

package com.hh.demo.designpattern.d;

/**
 * 被观察者
 * 气象站
 */
class WeatherStation {

    private Integer temperature;
    private Integer humidity;
    private Integer pressure;

    private List<Observer> observers = new ArrayList();

    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers(){
        for(Observer observer : observers){
            observer.update();
        }
    }
    //传感器可以感知温度、湿度、气压,一旦感知到就给属性赋值
    public void setData(Integer temperature, Integer humidity, Integer pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        //数据一旦变化,就通知给观察者
        notifyObservers();
    }

    public Integer getTemperature() {
        return temperature;
    }

    public Integer getHumidity() {
        return humidity;
    }

    public Integer getPressure() {
        return pressure;
    }
}

/**
 * 观察者接口
 */
interface Observer{
    void update();
}

/**
 * 具体的观察者
 */
//手机
class Phone implements Observer{

    private WeatherStation ws;
    public Phone(WeatherStation ws) {
        this.ws = ws;
        ws.addObserver(this);
    }

    @Override
    public void update() {
        System.out.println("手机显示:");
        System.out.println("温度:" + ws.getTemperature());
        System.out.println("湿度:" + ws.getHumidity());
        System.out.println("气压:" + ws.getPressure());
    }
}
//窗户
class Window implements Observer{

    private WeatherStation ws;
    public Window(WeatherStation ws) {
        this.ws = ws;
        ws.addObserver(this);
    }

    @Override
    public void update() {
        System.out.println("窗户显示:");
        System.out.println("温度:" + ws.getTemperature());
        System.out.println("湿度:" + ws.getHumidity());
        System.out.println("气压:" + ws.getPressure());
    }
}
//-------------------------------------------------------------------------
//广告牌
class AdvertisingBoard implements Observer{

    private WeatherStation ws;
    public AdvertisingBoard(WeatherStation ws) {
        this.ws = ws;
        ws.addObserver(this);
    }

    @Override
    public void update() {
        //实际逻辑业务都是网络编程
        //分布式远程
        System.out.println("广告牌显示:");
        System.out.println("温度:" + ws.getTemperature());
        System.out.println("湿度:" + ws.getHumidity());
        System.out.println("气压:" + ws.getPressure());
    }
}
//客户端
public class AppTest2 {
    public static void main(String[] args) {
        WeatherStation ws = new WeatherStation();

        Phone p = new Phone(ws);

        Window window = new Window(ws);

        AdvertisingBoard advertisingBoard = new AdvertisingBoard(ws);


        ws.setData(20,22,40);
    }
    /**
     * 手机显示:
     * 温度:20
     * 湿度:22
     * 气压:40
     * 窗户显示:
     * 温度:20
     * 湿度:22
     * 气压:40
     * 广告牌显示:
     * 温度:20
     * 湿度:22
     * 气压:40
     *
     * Process finished with exit code 0
     */
}

观察者模式很优雅的完成了上述业务需求,并扩展了在广告牌上显示数据,这时代码似乎很完美了

仔细想想,作为主体有什么共同点?

每个主体都有一个集合和操作集合的方法,既然有共性,那就上提

interface Subject{

    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

然后让主体实现该接口。

3、观察者模式的UML图

在这里插入图片描述

4、观察者模式的优点

①观察者和被观察者之间是抽象耦合

  • 不管是增加观察者还是被观察者都非常容易扩展,在系统扩展方面会得心应手。

②建立一套触发机制

  • 被观察者变化引起观察者自动变化。但是需要注意的是,一个被观察者,多个观察者,Java的消息通知默认是顺序执行的,如果一个观察者卡住,会导致整个流程卡住,这就是同步阻塞。

  • 所以实际开发中没有先后顺序的考虑使用异步,异步非阻塞除了能够实现代码解耦,还能充分利用硬件资源,提高代码的执行效率。

另外还有进程间的观察者模式,通常基于消息队列来实现,用于实现不同进程间的观察者和被观察者之间的交互。

5、观察者模式应用场景

①关联行为场景。

②事件多级触发场景。

③跨系统的消息交换场景, 如消息队列的处理机制。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。