设计模式——2_6 观察者(Observer)

这世界没有一件事情是虚空而生的,站在光里,背后就会有阴影,这深夜里一片寂静,是因为你还没有听见声音

——马良《坦白书》

文章目录

  • 定义
  • 图纸
  • 一个例子:在RPG游戏里应对善变的天气
    • 定义元素
          • Area & Weather
    • 给 Area 和 Knight 建立联系
    • 善变的天气
  • 碎碎念
    • 定时器的方案一无是处吗?
    • 观察者和中介者
    • 可以抽象出来的Subject和Observer

定义

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

二十年前如果你想知道明天的天气怎么样,你用不着隔几分钟就到气象台问问有没有最新的情报,而是可以在气象台登记一下你的电话号码。每当有最新的天气预报发布的时候,气象台会自动给所有进行登记过的电话号码发短信(这里面就包括你的)

这种模式其实就是观察者模式,而你就是被记录在册的观察者




图纸

在这里插入图片描述




一个例子:在RPG游戏里应对善变的天气

假定现在我们有一个RPG对战游戏,有这样的设定:

  • 天气分为:晴天、大雾和下雨
  • 玩家可以选择火元素、水元素或者风元素的骑士
  • 骑士一定是在某个区域内活动,而区域有对应的天气。每种元素的骑士在不同的天气下会变化自己的属性



定义元素

很显然,天气是区域的一种属性,就像这样:

在这里插入图片描述

Area & Weather
/**
 * 骑士可以活动的区域
 */
public class Area {

    /**
     * 当前区域的天气
     */
    private Weather weather;

    public Area(Weather weather) {
        this.weather = weather;
    }

    public Weather getWeather() {
        return weather;
    }

    public void setWeather(Weather weather) {
        this.weather = weather;
    }
}

public enum Weather {

	sunny,fog,rain
}

骑士也应当有自己的类簇,就像这样:

在这里插入图片描述

/**
 * 骑士
 */
public class Knight {

    /**
     * 攻击力
     */
    private int attack;

    /**
     * 生命值
     */
    private int healthPoint;

    /**
     * 骑士名称
     */
    private String name;

    /**
     * 骑士所在区域
     */
    private Area area;

    public Knight(String name, Area area) {
        this.name = name;

        setAttack(10);//默认10点攻击力
        setHealthPoint(100);//默认100点生命值
      	setArea(area);
    }

    public int getAttack() {
        return attack;
    }

    public void setAttack(int attack) {
        this.attack = attack;
    }

    public int getHealthPoint() {
        return healthPoint;
    }

    public void setHealthPoint(int healthPoint) {
        this.healthPoint = healthPoint;
    }

    public String getName() {
        return name;
    }

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

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }
    
    @Override
    public String toString() {
        return String.format("%s:攻击力=%s", name, attack);
    }
}

/**
 * 火属性骑士
 */
public class FireKnight extends Knight {

    public FireKnight(Area area) {
        super("火属性骑士", area);
    }
}

/**
 * 风属性骑士
 */
public class WindKnight extends Knight {

    public WindKnight(Area area) {
        super("风属性骑士", area);
    }
}

/**
 * 水属性骑士
 */
public class WaterKnight extends Knight {

    public WaterKnight(Area area) {
        super("水属性骑士", area);
    }
}

我们创建了天气的枚举 Weather 用来表示所有当前可能出现的天气,并把 Weather 作为 Area 的内部属性

对于骑士,我们创建 Knight 根类用于存放所有的骑士都有的一些属性,再根据不同的元素分出三个子类


现在我们实现了前两步,至于最后一步,想必我们需要在 AreaKnight 之间建立一些联系



给 Area 和 Knight 建立联系

那你会说了,不对啊,Knight 里面有自己当前所处的 Area 的引用,这不就是很好的联系吗?

Knight 里面的引用,实现出来的效果是这样的:

在这里插入图片描述


/**
 * 骑士
 */
public class Knight {

    ……

    public void setArea(Area area) {
        this.area = area;
        updateByWeather();
    }

    protected void updateByWeather(){
        //不实现,也不强制子类实现她,所以留空
    }
    
    /**
     * 把属性复原
     */
    protected void reset(){
        setAttack(10);
    }
}

/**
 * 火属性骑士
 */
public class FireKnight extends Knight {

    public FireKnight(Area area) {
        super("火属性骑士", area);
    }

    @Override
    protected void updateByWeather() {
        reset();

        Weather weather = getArea().getWeather();
        if (weather.equals(Weather.sunny)) {
            //如果是晴天,攻击力+10
            setAttack(getAttack() + 10);
        } else if (weather.equals(Weather.rain)) {
            //如果是雨天,攻击力减半
            setAttack(getAttack() / 2);
        }
    }
}

/**
 * 风属性骑士
 */
public class WindKnight extends Knight {

    public WindKnight(Area area) {
        super("风属性骑士", area);
    }

    //什么天气都跟他没关系 所以不需要重写
}

/**
 * 水属性骑士
 */
public class WaterKnight extends Knight {

    public WaterKnight(Area area) {
        super("水属性骑士", area);
    }

    @Override
    protected void updateByWeather() {
        reset();

        Weather weather = getArea().getWeather();
        if (weather.equals(Weather.rain) || weather.equals(Weather.fog)) {
            //如果是雾天或者下雨,攻击力翻倍
            setAttack(getAttack() * 2);
        } else if (weather.equals(Weather.sunny)) {
            //如果是晴天,攻击力降为1
            setAttack(1);
        }
    }
}

采用这种方案,我们在 Knight 为自己设定 Area 的时候就读取了天气信息,同时更新自己的属性,使用 updateByWeather 方法



善变的天气

可是问题很快出现了,我们玩这个游戏的时候发现,所有的玩家都会根据即将进入的区域选择合适的骑士,没有人蠢到故意在晴天选水骑士,或者在下雨时选火骑士

所以为了增加可玩性,我们设定了第四点需求:

  • 一个区域内的天气不是一成不变的,他会进行随机的变化

想法很好,实践起来却有点麻烦了

根据前面的设计,我们在set Area 的时候变化了自己的属性,之后 Area 里面的 Weather 会如何变化,Knight 是不知道的


怎么让他知道呢?我们有两种方案:

  1. Knight 里面添加一个定时器,固定时间去查 Area 里面的 Weather 属性,如果出现了变化,更新自己
  2. 想个办法让 Area 在更新 Weather 的时候去通知 Knight,让 Knight 及时更新

一看就知道后者明显优于前者,那能做到吗?

可以的,就像我们之前注册迭代器一样。我们只需要在 Area 里面维护一个 Knight 列表,然后在 set Weather 的时候通知 Knight 就完事了,就像这样:

在这里插入图片描述

/**
 * 骑士
 */
public class Knight {

    ……
        
    public void setArea(Area area) {
        //注销
        if (this.area != null) {
            this.area.removeKnight(this);
        }

        this.area = area;

        //注册
        area.addKnight(this);

        //第一次执行
        updateByWeather();
    }

    public void update(){
        updateByWeather();
    }
}

/**
 * 骑士可以活动的区域
 */
public class Area {
    
    ……

    public void setWeather(Weather weather) {
        this.weather = weather;
        notifyKnight();
    }

    private final List<Knight> knightList = new ArrayList<>();

    public void addKnight(Knight knight){
        knightList.add(knight);
    }

    public void removeKnight(Knight knight){
        knightList.remove(knight);
    }

    public void notifyKnight(){
        for (Knight knight : knightList) {
            knight.update();
        }
    }
}

我们让 Area 去维护一个 knightList ,并在 Knight 设定 Area 的时候把自己写到 knightList 里面去。这就实现了一个可以从 Area 发指令给 Knight 的通道。接着,我们需要发送指令的时候,可以通过 notifyKnight 方法通知 knightList 里面所有的 Knight

这样一来,第四点需求得以实现,就像这样:

public static void main(String[] args) {
	Area area = new Area(Weather.sunny);

	Knight fire = new FireKnight(area);
	Knight wind = new WindKnight(area);
	Knight water = new WaterKnight(area);

	System.out.println("晴天");
	System.out.printf("%s \n%s \n%s \n", fire, wind, water);

	System.out.println("*************************************************************");

	area.setWeather(Weather.rain);
	System.out.println("雨天");
	System.out.printf("%s \n%s \n%s \n", fire, wind, water);
}

在这里插入图片描述

而这正是一个标准的观察者实现


观察者的结构和原理简单到一眼就能望到头,但是这个简单的结构解决了无数个问题。这有点像多线程里面的 生产-消费者模型,也是结构简单但极其实用。也许这就是大道至简吧




碎碎念

定时器的方案一无是处吗?

其实并不是的,上例的情况是因为 KnightArea 可以双向主动向对方发起请求,所以可以用观察者,但是很多时候连接是单向的

比如说 http,这就是个无状态协议,除非用一些比较特殊的手法(比如 WebSocket),否则服务器是没办法主动向客户机发送请求的

这时候如果你有时效性不那么高的类似请求(游戏的时效性要求肯定不允许你用定时器),那么定时器和长连接就是你需要考虑的解决方案了



观察者和中介者

到了行为型模式这一篇,其实有很多模式关注的内容是类似的

比如前面讲过的 职责链(Chain of Responsibility)命令(Command)

职责链和命令都是通过参数化请求,以求实现请求者和处理器之间的解耦

之后还会提到的 状态模式(State)策略模式(Strategy)状态模式简直就是策略模式水里的倒影


以及现在要讲的 观察者(Observer)中介者(Mediator)

观察者和中介者都是为对象和对象之间通讯而存在的

这种通讯相当于,对象A执行了某个动作(在面向对象中其实就是某个函数被调用),对象B就要针对这个行为执行自己的动作

这就像自行车的主动轮和从动轮之间的关系


假定我们现在有A和B两个对象,A发出通知,B接收A的通知并执行操作

在这种情况下,如果让A直接调用B,那就意味着他们之间建立紧耦合;如果想要解耦,那么对象之间的通信方式基本上有两种

  1. 在A和B之间建立一个平台,让A和B都去跟平台打交道而不知道对方的存在,这个平台就是 中介者
  2. 在A里面,维护一个监听者列表,形成一个 1→N 的关系,这时候我会把B写入A的监听者列表里(这时候建立的是抽象耦合)。当发生某个事件的时候,A会通知所有监听者进行更新(其中就包含B),这时候的B就是 观察者

先说两种方式的共同点,两种做法都可以解除A和B之间的紧耦合。A可以不知道这个通知会被传递到哪里去,可以不知道B的数量,甚至可以不知道B的具体类型


但两种设计模式又各有千秋:

  • 中介者 内部的对象没有明确的主次,任何对象都可以通过平台发出信息或对某个信息进行响应
  • 观察者 不需要这个平台,subject和observer之间存在明确的主次关系,信息传递的方向也永远是 S u b j e c t → O b s e r v e r Subject → Observer SubjectObserver

这是他们好的一面,而他们的缺点和优点一样明显:

  • 中介者 的平台随着所要维护的对象数量增加,需要处理的关联也越来越多,这最终会让中介者平台变成一个庞然大物,所有的关联都集中到一种,最终形成一个错综复杂的线团,把他理清是很痛苦的事情

  • 观察者 不需要第三方平台,这是便利,也是缺陷。因为subject和observer都对对方太不了解了,所以在后期维护的时候,如果不了解程序结构的人调用了subject里某个会在observer里产生副作用的方法,程序可能出现一些诡异的行为,而很难发现是哪个观察者的问题。这些诡异的行为包括但不限于:

    • 调用一个更新数据的方法,但另一个看似风马牛不相及的视图也被更新了

    • subject通知observer进行操作,但是observer又会调用subject里的行为,到最后形成死循环

      那你会说,我又不是傻,为什么要这样写?

      现实是这种情况时有发生,因为整个项目的结构里不会只有 subject 和 observer 这两者。也许你通过subject 通知 observer 后,observer 又会去调用其他对象,其他对象又调用其他对象,以此往复,最终跑回 subject 来



可以抽象出来的Subject和Observer

你可能发现了,其实所有在观察者模式中发出信息的 变化主体,或者说 subject,在维护观察者列表的时候,都需要三个方法:

  1. addObserver

    增加观察者

  2. removeObserver

    删除观察者

  3. notifyObserver

    通知观察者进行更新

在观察者,或者说Observer里,则需要用于更新的 update 方法


既然有通用的部分,那我们其实就可以把他们抽象出来,就像这样:

/**
* 被观察者 主体
**/
public class Subject {

    private final List<Observer> observerList = new ArrayList<>();

    public void notifyObserver() {
        for (Observer observer : observerList) {
            observer.update(this);
        }
    }

    public void addObserver(Observer observer) {
        observerList.add(observer);
    }

    public void removeObserver(Observer observer) {
        observerList.remove(observer);
    }
}

/**
* 观察者
**/
public interface Observer {

    void update(Subject subject);
}

在Java里面,Subject和Observer甚至不需要你自己写,因为在 java.util 里面就有对应的工具类,可以直接继承

虽然说从Java9 开始这玩意就废弃了,我也是写这个的时候才发现

在这里插入图片描述


不过值得一提的是,Observer 通常可以作为接口存在,但如果你需要 Subject 帮你维护观察者列表,那么 Subject 至少得是一个抽象类,那就只能用继承

在Java这个单继承的语言里,使用继承要慎重,就比如上例的 Area,就算我有 Subject 工具类,我也不会让 Area 去继承她,我宁可自己写

因为这会破坏整体的语法,区域怎么可能是主体的子类呢?这会让继承我的代码的后辈产生误解,这虽然只是编程风格的问题,但是我坚信细节决定成败,所以该抠的地方还是严谨一点的好




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/482555.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux--Ubuntu安装【保姆级教程】

Linux操作系统时程序员必须要学的操作系统。接下来我们就来看一下Linux操作系统是如何安装的 我们在 Vmware 虚拟机中安装 linux 系统&#xff0c;所以需要先安装 vmware 软件&#xff0c;然后再 安装 Linux 系统。 一.所需安装文件&#xff1a; Vmware 下载地址(现在最新版的…

蓝桥刷题--N皇后和最近公共祖先

1.N皇后 #include<iostream> using namespace std;const int N 12; int vis[N][N], n, ans;void dfs(int dep) {// 在这个搜索中dep表示行&#xff0c;i表示列// 1 搜索出口if(dep n 1){ans;return;}// 2 继续搜索for(int i 1; i < n; i){// 2.1 排除非法情况if(v…

SQL-Labs靶场“34-35”关通关教程

君衍. 一、34关 POST单引号宽字节注入1、源码分析2、联合查询注入3、updatexml报错注入4、floor报错注入 二、35关 GET数字型报错注入1、源码分析2、联合查询注入3、updatexml报错注入4、floor报错注入 SQL-Labs靶场通关教程&#xff1a; SQL注入第一课 SQL注入思路基础 SQL无列…

TWT:一个让WiFi6更省电的特性

更多精彩内容在公众号。 再wifi6前&#xff0c;已经有了不少节能特性&#xff1a;PSM,PSMP,APSD。在一个 Beacon 周期内&#xff0c;终端 会观察 AP 是否会向其发送数据&#xff0c;如果是&#xff0c;那么终端就保持等待&#xff0c;直到接收完成后&#xff0c; 才会进入休眠模…

【C语言】动态内存分配

1、为什么要有动态内存分配 不管是C还是C中都会大量的使用&#xff0c;使用C/C实现数据结构的时候&#xff0c;也会使用动态内存管理。 我们已经掌握的内存开辟方式有&#xff1a; int val 20; //在栈空间上开辟四个字节 char arr[10] { 0 }; //在栈空间…

Yocto学习笔记1-下载与首次编译

Yocto学习笔记1-下载与首次编译 1、基础环境介绍2、注意点3、安装依赖3.1 yocto常规系统构建所需依赖库&#xff08;较全&#xff09;3.2 龙芯适配时的最小依赖库&#xff08;最小&#xff09; 4、下载4.1 通过git克隆4.2 查看所有远程分支4.3 签出一个长期支持的稳定版本4.4 查…

leetcode 15.三数之和 JAVA 双指针法

题目 思路 双指针法 去重 为啥要去重呢&#xff1f;因为题目中说了要返回不重复的三元组。拿示例1来看&#xff0c;&#xff08;-1&#xff0c;0&#xff0c;1&#xff09;和&#xff08;0&#xff0c;1&#xff0c;-1&#xff09;虽然都等于0&#xff0c;但其实它们里面的数…

【python_往企业微信群中发送文件】

python_往企业微信群中发送文件 这个是用企业微信群机器人的功能&#xff0c;没有用到后台应用。群机器人 #-*- coding:utf-8-* import requests#类型&#xff1a;voice,file file_type"file" file_path"D:\desktop\不过.jpg" webhookkey"xxxx"#…

ShuffleNet模型详解

ShuffleNet论文地址&#xff1a;1707.01083.pdf (arxiv.org) ShuffleNetv2论文地址&#xff1a;1807.11164.pdf (arxiv.org) ShuffleNetv1 简介 ShuffleNet 是专门为计算能力非常有限的移动设备设计的。架构采用了逐点分组卷积和通道shuffle两种新的运算&#xff0c;在保持…

【异或】Leetcode 136. 只出现一次的数字

【异或】Leetcode 136. 只出现一次的数字 解法1 只需要全部异或一下&#xff0c;剩下的就是剩下的元素 ---------------&#x1f388;&#x1f388;题目链接 136. 只出现一次的数字&#x1f388;&#x1f388;------------------- 解法1 只需要全部异或一下&#xff0c;剩下的…

Fast-R-CNN论文笔记

目标检测之Fast R-CNN论文精讲&#xff0c;Fast RCNN_哔哩哔哩_bilibili 一 引言 1.1 R-CNN和SPPNet缺点 &#x1f600;R-CNN Training is a multi-stage pipeline 多阶段检测器&#xff08;两阶段和一阶段检测器&#xff09; 1️⃣首先训练了一个cnn用来提取候选区域的特征…

深入浅出Reactor和Proactor模式

Reactor模式和Proactor模式是两种常见的设计模式&#xff0c;用于处理事件驱动的并发编程。它们在处理IO操作时有着不同的工作方式和特点。 对于到来的IO事件&#xff08;或是其他的信号/定时事件&#xff09;&#xff0c;又有两种事件处理模式&#xff1a; Reactor模式&…

jupyter | 查询/列出available kernels

jupyter kernelspec list 添加kernel python -m ipykernel install --user --name 虚拟环境名 --display-name 在jupyter中显示的环境名称 移除kernel jupyter kernelspec remove 环境名

部标JT808车辆定位监控平台单服务器13.6万接入压力测试记录(附源码)

之前经常有人问平台能支持多少设备同时在线&#xff0c;由于事情多没时间做。最近刚好有机会做下压力测试。在不间断的连续压测三天&#xff0c;最终结果为13.6万TCP连接&#xff0c;30秒上报频率。 一、测试目的 测试平台同时接入设备数量与并发处理能力。 二、准备环境 一…

javaweb--JavaScript

一&#xff1a;简介 JavaScript 是一门跨平台、面向对象的脚本语言 &#xff0c;用来控制网页行为的&#xff0c;它能使网页可交互 JavaScript 和 Java 是完全不同的语言&#xff0c;不论是概念还是设计&#xff0c;只是名字比较像而已&#xff0c;但是基础语法类似 JavaScri…

揭秘国产龙蜥OS操作系统:高效学习之路等你开启!

介绍&#xff1a;Anolis OS是一个完全开源、中立且开放的Linux发行版&#xff0c;专为多种计算场景设计&#xff0c;特别适合云端环境。 Anolis OS的推出旨在为广大开发者和运维人员提供一个稳定、高性能、安全、可靠且开源的操作系统服务。以下是Anolis OS的几个重要特点&…

mysql80-DBA数据库学习1

掌握能力 核心技能 核心技能 mysql部署 官网地址www.mysql.com 或者www.oracle.com https://dev.mysql.com/downloads/repo/yum/ Install the RPM you downloaded for your system, for example: yum install mysql80-community-release-{platform}-{version-number}.noarch…

window10系统~如何关闭电脑的防火墙?

电脑桌面左下角选择放大镜&#xff0c;搜索&#xff1a;防火墙2. 点击【防火墙和网络保护】 3. 把下面三个地方都关闭掉&#xff1a; 点击【域网络】&#xff0c;关闭如下按钮&#xff1a; 再返回到上层&#xff0c;如下的界面&#xff1a; 用上面相同的方法&#xff0c;依…

阿里云有免费服务器吗?有的,附送免费服务器申请流程

阿里云服务器免费试用申请链接入口&#xff1a;aliyunfuwuqi.com/go/free 阿里云个人用户和企业用户均可申请免费试用&#xff0c;最高可以免费使用3个月&#xff0c;阿里云服务器网分享阿里云服务器免费试用申请入口链接及云服务器配置&#xff1a; 阿里云免费服务器领取 阿里…

APP测试中ios和androis的区别,有哪些注意点

目录 一、运行机制不同 二、对app内存消耗处理方式不同 三、后台制度不同 四、最高权限指令不同 五、推送机制不同 六、抓取方式不同 七、灰度发版机制不同 八、审核机制不同 一、运行机制不同 IOS采用的是沙盒运行机制&#xff0c;安卓采用的是虚拟机运行机制。 1、…