观察者模式(结合C#,Unity)

文章目录

  • 前言
    • 概念简介
    • 观察者模式布-订阅模式/li>
  • 观察者(发布-订阅)模式应用
    • 不用设计模式实现
    • 用接口实现
      • 观察者模式代码结构介绍
      • 实现发布-订阅模式
    • 用事件实现
    • 改进
      • 接口法改进方式
      • 事件管理中心

前言

概念简介

先来看一段比较正式的介绍:
观察者模式是软件开发中一种十分常见的设计模式,又被称为发布-订阅(Publish/Subscribe)模式,属于行为型模式的一种。它定义了一种一对多的依赖关系,让多个观察者对象(Observer)同时监听某一个主题对象(Subject)。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

以运动会的跑步比赛为例,假设场上有这几个对象:裁判,运动员,观众。那怎么才能知道比赛开始呢时候运动员和观众就会作为观察者,“关注”裁判(此时裁判就是主题对象),当裁判的发令枪响起时(主题对象状态发生改变),标志着比赛开始。然后运动员和观众收到“比赛开始”的通知后,各自做出他们的响应(观察者状态更新):跑步运动员向终点奔去,观众开始注视场上的赛况。

所以整合一下,观察者模式要包括这些组成部分:
1)一个主题对象,这个名词看起来比较抽象,我们干脆叫它 “被观察者”
2)多个 关注/订阅 被观察者的 观察者
3)被观察者的状态发生改变时,观察者会收到通知,然后观察者会做出他们各自的响应(或者说改变他们自己的状态)

观察者模式布-订阅模式/h2>

关于“观察者模式和发布-订阅模式算不算两个独立的设计模式”这一讨论也是争议不断。
之前提过观察者模式的别称是“发布-订阅模式”,但是有些地方会说这两种模式是不同的两个模式。
从两者的实现结构来看,确实会有些不同。我这里用两张图来进行比较:

可以明显地看到,发布-订阅模式在原来的观察者和被观察者之间加了一个调度中心
那么消息发送者(Publisher)就不会将消息直接发送给订阅者(Subscriber),这意味着发布者和订阅者不知道彼此的存在。他们之间的通信全交给了作为第三方的调度中心。

同样举个生活中的例子:一个 CSDN 博主被好几个粉丝关注,这些粉丝充当了“订阅者”的角色,他们“关注”(订阅)了博主。每当博主(消息发送者)发了一条新的博客,这条博客是发到了 CSDN 平台(作为调度中心)上,那么 CSDN 平台会将“博主发了一条新博客”这个消息通知给关注博主的粉丝们,然后这些粉丝就会做出他们各自的响应(比如浏览博文,点赞之类的)。
有了调度中心后,博主只要安心地专注于发博客这件事情身上,他不用管谁是他的粉丝,因为“把更新消息发给粉丝”这件事是由 CSDN 平台这个调度中心来执行的,无需博主亲自通知;粉丝关注博主也是借由 CSDN 平台来记录的。
总结来说,此时 CSDN 平台知道一个博主的粉丝具体是谁,然后当博主在 CSDN 平台上发博客时,CSDN 平台就通知该博主的所有粉丝。

用程序的话语来解释:

订阅者把自己想订阅的事件注册到调度中心,在发布者发布该事件到调度中心后,由调度中心统一调度订阅者用于响应事件的处理代码(订阅者收到事件触发消息后所要做的事)。

那么对于发布-订阅模式:
1)一共有3个组成部分:发布者,订阅者,调度中心
2)发布者和订阅者完全不存在耦合
对于观察者模式:
1)一共有2个组成部分:被观察者,观察者
2)被观察者和观察者是直接关联的,但它们是松耦合。这个是指被观察者状态变化时会自动触发观察者状态的变化,只是被观察者需要知道谁是观察者。

但是发布-订阅模式弱化了对象之间的关联也会存在一些缺点,过度使用可能会使代码不好理解(这个后面会根据实际例子进行说明)

组成结构上来看,它们确实会有不同。
实现目的来看,它们是相同的,都是一个对象的状态发生改变时会通知那些与此对象关联的其他对象。
我的个人理解是,发布-订阅模式是观察者模式的变种,也可以说是观察者模式的优化升级。我们也许不必把太纠结于“它们是不是同一种设计模式”,而是要充分学习它们的思想,在合适的时候运用到实际的开发中,为我们带来便利。不过理解它们在组成结构上的区别还是有必要的,万一面试会考呢/p>

观察者(发布-订阅)模式应用

试想一个常见的游戏场景:玩家 Gameover(死亡)
玩家死亡时,会伴随着其他的一些事情发生,比如敌人停止移动,界面出现游戏失败 UI …
这里我们先来考虑玩家死亡时敌人停止移动怎么实现
注:这里就不展示敌人停止移动具体的代码实现了,我们用一个输出语句来表示;然后为了快速表示玩家死亡,我直接按下键盘的J键来触发

不用设计模式实现

那么借助 Unity 引擎和 C# 语言,我们用一种简单的实现方式:
首先简单搭下游戏场景

  1. 玩家类和其他与玩家死亡所关联的类会紧紧地耦合在一起,比如说 Enemy 类原先的 StopMove 方法换了个名字,那么我们不得不回到 Player 类中进行对应的修改。当一个类发生修改会影响到另一个类时,会对项目的维护和更新增添许多麻烦。
  2. 如果增加一个玩家死亡触发的事情,比如玩家死亡后播放一段音效,那么我们还要回到 Player 类对 PlayerDead 方法进行修改。

现在我们用观察者(发布-订阅)模式对代码进行优化。

用接口实现

观察者模式代码结构介绍

先看一种比较标准的观察者模式结构,这里用一种不大标准的 UML 图简要的表示下(用种简要的图来表示观察者模式中的各组成部分之间的联系):

而是存储统一的类似所有观察者基类的抽象观察者,所以我们能用抽象观察者去概括具体观察者,能用一个统一的列表去涵盖所有的观察者

然后每个观察者在自己的类中把自己添加到被观察者的观察者列表中(相当于订阅了被观察者),当被观察者发起通知时,会去遍历持有的观察者列表,调用每个抽象观察者的 “Update” 方法,那么调用抽象层实际上也就会调用具体观察者重写的抽象接口中的方法。
在被观察者的眼中,它所交互的都是抽象的观察者,因此不管观察者的代码怎么发生变动,在被观察者的眼中始终是一模一样的抽象观察者,只是实际运行时抽象才指代具体,这样对被观察者的代码本身没有任何影响。所以说观察者模式是低耦合的。

因此从代码层面理解观察者模式,就是在原先的结构上加了“抽象”层。

为什么被观察者的观察者列表要是抽象的什么抽象能帮助代码解耦果看到这里你能够在心中回答这两个问题,相信你有能力手写观察者模式的代码了。如果还是不太清楚也没关系,毕竟概念可能会有一点“抽象”,那么我们直接通过实战来学习!

下面用具体代码对之前玩家死亡的案例进行改进,来帮助大家加深对上述概念的理解。(这里只会给出部分代码,因为我把重点放在更实用的发布-订阅模式上)
如果用严格意义上的观察者模式,作为观察者的敌人需要把自己添加给被观察者的列表,但是添加的方法是定义在抽象被观察者接口中的。

那之前说了,观察者模式的升级版——发布-订阅模式添加了一个调度中心,能够使观察者和被观察者完全解耦,这在实际开发中是很实用的。因此接下来我会着重于用接口来实现发布-订阅模式。

实现发布-订阅模式

我们用一个 GameManager 作为调度中心,相当于一个管理者来管理所有的观察者,并且对外提供添加、移除观察者和通知的方法。那么原先的被观察者发布通知,直接调用的是 GameManager 的通知方法,观察者把自己添加到观察者列表,调用的是 GameManager 的添加方法。观察者与被观察者之间不建立任何联系,全靠第三方的调度中心通信,这样可实现跨模块的交互。
观察者接口:

这里因为有了 GameManger,我们就无需写个多余的被观察者接口。而且像 GameManager 这种作为管理者的脚本,整个游戏期间只需有唯一的对象,因此建议利用单例模式把管理器脚本设为单例,一旦将 GameManager 实例化后,之后使用的都是这个唯一存在的 GameManager【本篇博客不会详细介绍单例模式的相关知识点,但会演示如何使用,并且使用的也是简单的单例模式版本。想要了解更多关于单例模式的可以看这篇文章 Unity 单例基类(结合单例模式)。】

GameManager脚本(无需继承 MonoBehaviour,我们不必把此脚本挂到任何游戏物体上):

玩家脚本:

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年8月10日
下一篇 2022年8月10日

相关推荐