# 前言

这本书是紧接着《CLR VIR C#》 开始的,依然是回家的时候看,顺便在手机语雀上记笔记:

今天是 2023 年 5 月 1 日
也算是过了一遍了,这本书其实不是很厚 —— 至少跟 《CLR VIR C#》 相比就薄多了,也花了差不多一个月,主要是因为每周加两天班变成了常驻,另外还在追 DOTS1.0 的教程,导致时间上更短了。
这两天五一放假 (实际上前一天 28 号就提前请假了),然后回来从责任链模式继续看,认真专心点,还是挺快的。

本文总结的主要是部分重要的理论描述,—— 文字上的,毕竟是手机记录。很多重要概念的还是得结合代码。

# 简介

  • Gof:《设计模式》一书由四人合著(Gang of Four),代表 23 种设计模式
  • 23 种设计模式可分为三大类
    • 生成模式(creational)
      • 产生对象的过程及方式
    • 结构模式(structural)
      • 类或对象之间组合的方式
    • 行为模式(behavioral)
      • 类或对象之间互动或责任分配的方式
  • 六大设计原则
    • 开闭原则
    • 里氏替换原则
    • 依赖倒置原则
    • 单一职责原则
    • 迪米特法则
    • 接口隔离原则
    • 合成复用原则(六大原则有七个是常识对吧?)

# 设计模式

# 状态模式(state)

让一个对象的行为随着内部状态的改变而变化,而该对象也像是换了类一样

  • 当某个对象状态改变时,虽然其『表现的行为』会有所变化,但对于其它对象来说,并不会因为这样的变化而改变对它的『操作方法』或『信息沟通』的方式
  • 即对象与外界的对应方式不会发生改变,一切只发生在对象内部
  • 例子:场景管理
  • 其它应用
    • 角色 AI
    • 服务器连接状态
    • 关卡进行状态

# 单例模式(singleton)

『确认类只有一个对象,并提供一个全局的方法来获取这个对象』
生活中许多物品都是唯一的:地球是唯一的、太阳是唯一的 等

  • 同时只存在一个对象
  • 提供一个快速获取这个对象的方法

哪些系统类适合以单例模式实现,需要精挑细选
缺点:

  • 不必为 如何传递对象、设置对象引用 而进行过多考虑,容易造成『单例癖』,过于沉迷单例模式带来的直接访问好处,导致设计思考不周,造成单例滥用
  • 单例模式违反了 开闭原则,因为获取的是直接实现类
  • 单例出现继承,出现 白马非马 问题

# 外观模式(facade)

为子系统定义一组统一的接口,这个高级接口会让子系统更容易被使用

  • 以简单的行为操作复杂的接口,所使用的接口就是以外观模式定义的高级接口
  • 重点在于,能将系统内部的互动细节隐藏起来,并提供一个简单方便的接口
  • 优点
    • 减少耦合度、易于分工开发、增加系统安全性
    • 随着开发需求变更,任何子系统的修改都被限制在系统内部
  • 注意
    • 由于将所有子系统集中于外观模式接口类中,会导致该类变得过于庞大,因此可以继续拆分

# 中介者模式(mediator)

定义一个接口用来封装一群对象的互动行为,中介者通过移除对象之间的引用,减少它们之间的耦合度,并能改变它们之间的互动独立性

  • 每一个系统除了会引用与之相关的类型外,无论是对信息获取或传递,都只通过中介者完成
  • 这使得每一个系统对外依赖度缩小到只有一个类

注意:

  • 避免担任过多中介者角色而出现 操作接口爆炸 的情况
  • 可以搭配其它设计模式:如观察者模式

其它应用:

  • 网络引擎,如通过中介者沟通,方便更换 tcp 或 udp 等连接方式

# GameLoop

不继承 Monobehavior ,自己控制更新逻辑,包括游戏更新的时间点和方式

# 桥接模式(Bridge)

将抽象与实现分离,使二者可以独立地变化
注:并非 『依赖倒置原则』的另一个解释:定义一个接口类,然后将实现部分在子类完成
可以将两个群组有效分离,让两个群组彼此互不影响
如:

  • 角色 (不同角色) 与武器 (不同武器)
  • 渲染引擎(不同渲染图形,不同平台选择 dx、opengl 等)

# 策略模式(Strategy)

  • 将复杂的公式独立出来成为一个群组,之后可以按情况来决定使用的计算公式策略,提高了系统应用的灵活程度
  • 由一群没有任何关系的类所组成,不知彼此的存在
  • 是由封装计算算法而形成的一种设计模式,算法之间不存在任何依赖关系,有新增算法可以马上加入或替换
  • 应用:
    • 属性系统
    • 熟练度系统
    • 登录策略

# 模板方法模式(template method)

在一个操作方法中定义算法流程,其中某些步骤由子类完成,模板方法模式让子类在不变更原有算法流程的情况下,还能重新定义其中步骤

  • 定义算法流程中,某些步骤需要由执行时 『当下环境』来决定
  • 定义算法的流程中,针对每个步骤都提供了预设方案,但有时这个步骤中处理会出现『更好的解决方法』
  • 优点:将可能出现重复的 算法流程 ,从子类提升到父类中,减少重复的发生,并且也开发了子类参与算法中各个步骤的执行或优化
  • 注意:开放流程的平衡,开放太多并要求子类全部实现,反而造成困难
  • 应用:
    • 登录流程
    • RPG 释放法术流程
    • 释放技能流程
    • 攻击流程

# 工厂方法模式(factory method)

定义一个可以产生对象的接口,但是让子类决定要产生哪一个类的对象。工厂方法模式让类的实例化程序延迟到子类中实施
将类产生流程集合管理的模式

  • 能针对对象产生流程制定规则
  • 减少客户端参与对象生成过程,尤其是对象生产过程比较复杂的,降低生产耦合

C# 支持泛型,泛型工厂比较方便
优点:将类群组对象的产生流程整合于同一个类下实现,并提供唯一的工厂方法,让项目内 对象产生流程 更加独立

  • 不过,类群组过多时,无论使用哪种方式,都存在工厂子类爆量或 switch case 语句过长问题

应用:

  • UI 管理器
  • 角色工厂
  • 资源加载工厂
    • 真机资源加载与编辑器资源加载方式
  • 武器工厂
  • 属性生产工厂
  • 当产生对象时,需要
    • 复杂的流程
    • 需要加载外部资源
    • 有对象上限或管理
    • 可重复使用
    • 就可以考虑采用工厂方法模式,将对象产生及相关初始化集中在一个地方,让对象的产生与管理更有效率

# 建造者模式(builder)

『将一个复杂对象的构建流程与它的对象表现分离出来,让相同的构建流程可以产生不同的对象行为表现』
工厂方法模式将生产对象全部集中管理,为了使生产对象过程更有效率和弹性,通常可以搭配建造者模式

  • 将复杂的构建流程独立出来,并将整个流程分成几个步骤,其中每个步骤可以是一个功能组件的设置,也可以是参数指定,并在一个构建方法中讲这些步骤串接起来
  • 定义一个专门实现这些步骤的实现者,这些实现者知道如何完成每一部分,并能接受参数决定要产出功能,但不知道整个流程要组装的是什么
  • 主要:流程分析安排、功能分开实现

# 享元模式(flyweight)

『使用共享的方式,让一大群小规模对象能更有效地运行』

  • 享元模式用来解决 大量且重复对象 的管理问题,特别是 虽小却大量重复对象

应用:

  • 道具属性(感觉并不合适)

# 组合模式(composite)

『将对象以树状结构组合,用以表现部分 - 全体的层次关系,让客户端在操作各个对象或组合对象时是一致的』

# 命令模式(command)

『将请求封装成为对象,让你可以将客户端的不同请求参数化,并配合队列、记录、复原等方法来执行请求的操作』

  • 请求的封装
    • 封装参数,亦可封装执行
  • 请求的操作
    • 存储:排序、排队、移动、删除、暂缓执行等
    • 记录:可记录已执行记录,查看过去执行流程和轨迹
    • 复原:若请求执行了反向操作,则可将已执行请求复原

可以独立排队执行
注意:使用命令模式也要注意情况,例如是否需要排队执行、命令被对象化后,是否对其还有管理需求。
其它应用

  • 网络 client/server 数据传递,侧重执行与记录

# 责任链模式(chain of responsibility)

『让一群对象都有机会来处理一项请求,以减少请求发送者与接收者之间的耦合度。将所有的接收者对象串联起来,让请求沿着串接传递,直到有一个对象可以处理为止』

  • 可以解决请求的接收者对象,能够了解请求并判断自身能否解决
  • 接收者对象的串联:将每一个可能解决问题的接收者串接,对于被串接的接收者来说,若判断自身无法解决,则利用串接机制传递请求给下一个
  • 请求自动转移:发出请求后,请求会自动往下转移传递,不需要发送者特别转换

注:

  • 责任链模式根据需求,其实也可以不必从头开始判断
  • 该模式让信息判断上有一致的操作接口,不必因不同接收者而执行 类转换操作
  • 让所有信息接收者都有机会可以判断是否提供服务或将需求转为下一个,对后续系统修改维护也有利

# 观察者模式(observer)

『在对象之间定义一个一对多的连接方法,当一个对象变换状态时,其它关联对象都会自动收到通知』
观察者模式与命令模式相似,都是希望 『事件发生』与『功能执行』之间不要有太多依赖
例如:

  • 社交软件上的 关注 功能
  • 早期的 报社 - 订阅

信息的 『推』 与 『拉』
主题(subject)改变时,改变内容如何让观察者(observer)得知,运行方式分为两种:

  • 推(push):主题将变动内容主动 “推” 给观察者。一般在调用观察者进行通知时,同时将更新内容当做参数传递给观察者。
    • 优点:省去观察者再向主题查询操作,主题也不需要定义额太多额外接口供查询调用
    • 缺点:如果推送内容过多,容易使观察者收到不必要信息或造成查询困难,也可能降低系统性能
  • 拉(pull):主题变动时,只是先通知观察者当前内容已发生变动,观察者按照系统需求,再向主题查询所需信息
    • 优点:观察者更知道自己需要哪些信息,避免获取到不必要的冗余信息
    • 缺点:主题必须提供查询方式,容易造成主题接口方法过多

一般为字典,key 为枚举或整型,value 为一个观察者类,或直接是个回调。
采用观察者类的方式,可能造成过多观察者类,因此采用回调可以更有效减少类的产生。
其它应用:

  • 机关
  • 剧情触发

# 备忘录模式(memento)

『在不违反封装的原则下,获取一个对象内部状态并保留在外部,让该对象可以在日后恢复到原先保留时的状态』
接口隔离原则 (isp):除非必要,否则类应该尽量减少对外显示的内部数据结构,减少对外公布的操作方法

  • 为了不违背封装的原则,其数据由类主动提供
  • 即让有记录保存需求的类,自行产生要保存的数据,外界完全不用了解这些记录产生的过程和来源

# 访问者模式(visitor)

『定义一个能够在一个对象结构中对所有元素执行的操作,访问者让你可以定义一个新的操作,而不必更改到被操作元素的类接口』
重点:定义一个新的操作,而不必更改到被操作元素的类接口
优点

  • 新增功能只需要实现新的访问者
  • 增加系统稳定性,减少对类接口不必要修改

缺点

  • 被访问者封装性变差:因为需要尽可能提供所有可能的操作和信息

其它应用

  • 需要使用 遍历所有对象 的功能
  • 道具包
  • 可使用角色

# 装饰模式(decorator)

『动态地附加额外的责任给一个对象,装饰模式提供了一个灵活的选择,让子类可以用来扩展功能』
装饰模式具有很高的灵活度和透明性,可以一直不断地包覆下去
应用:

  • 属性前缀、后缀
  • 网络协议加密

优点在于不必更改太多现有实现类就能完成功能强化
注:适用于 目标已经存在,而装饰需求之后 出现的情况,不应滥用,过多装饰堆砌也会增加复杂度。

# 适配器模式(adapter)

『将一个类的接口转换成为客户端期待的类接口。适配器模式让原本接口不兼容的类能一起合作』
优点:不必使用复杂的方法,就能将两个不同接口的类对象交换使用。
应用:

  • 第三方库隔离职责
  • UI 组件适配

# 代理模式(proxy)

『提供一个代理者位置给一个对象,好让代理者可以控制存取这个对象』
类似装饰模式,不用之处在于:

  • 对代理模式来说,它可以选择新功能是否执行;而装饰模式则是一定会一并执行
  • 即代理模式是按职权『有选择』是否需要将需求转交给原始类。
  • 而装饰模式则是必须在原始类被调用之前或之后,按照自己职权『增加』原始类没有的功能
  • 与适配器模式差异:适配器着重在于『不同实现的转换』

使用场景

  • 远程代理 (remote proxy):常见于网页浏览器中代理服务器的设置
  • 虚拟代理 (virtual proxy):可作为 延后加载 功能的实现,让资源可以在真正要使用时才进行加载操作,其它情况都只是虚拟代理所呈现的一个假象
  • 保护代理 (protection proxy):代理者有职权可以控制是否要真正取用原始对象的资源
  • 智能引用 (smart reference):主要用于强化 C/C++ 语言对于指针控制的功能,减少内存遗失 (memory leak) 和空指针 (null pointer) 等问题

优点

  • 可判断是否要将原始类的工作交由代理者类来执行,可以免去修改原始类的接口及实现

其它应用

  • 临时资源代理呈现
  • 服务器玩家在不同地图区块信息同步

# 其它模式

  • 迭代器模式
    • 在不知道集合内部细节的情况下,提供一个按序方法存取一个对象集合体的每一个单元
    • C# 已直接提供支持
  • 原型模式
    • 使用原型对象来产生指定类的对象,所以产生对象时,是使用复制原型对象来完成
    • 例如实例化
  • 解释器模式
    • 定义一个程序设计语言所需要的语句,并提供解释来解析执行
  • 抽象工厂模式
    • 工厂方法模式:定义一个可以产生对象的接口,但是让子类决定要产生哪一个类的对象。工厂方法模式让类的实例化延迟到子类执行
    • 抽象工厂模式:提供一个能够建立整个类群组或有关联的对象,而不必指明它们的具体类
    • 即:例如两组继承统一抽象工厂的工厂,根据不同环境调用哪一种生成不同的两组对象

# 总结

不知道为啥,作者是把 abstract 父类命名为接口,也就是说,这本书所说的接口是将继承关系的抽象父类称为接口,所以其实并没有使用语言『规则』上的『接口』。抽象类虽然可以将方法实现延迟到子类,但它是单继承且依然代表一类对象,它的子类必然还是具有父类所有成员的。

整书完整贴代码还是挺多的,也就是更多偏向于展示作者的代码,理论反而比较少,基本结构大概就是:描述《P 级阵地》需求 ->(硬编码示例)-> 引用 Gof 下对应设计模式的描述 -> 想如何设计 -> 然后代码 -> 结论

要是之前没有概念、并且想从实践入手的话,可以考虑一看(可能需要多一点耐心,代码还是比较基础)
但也因为上面说的原因,有些代码有些冗余。例如,访问者模式下的示例,要是利用泛型就不用写那么多方法及手动调用了。
当然,也可能是作者是为了『更简单』描述出原理。

本书将一整个 23 种设计模式全都囊括且给出了游戏应用示例 —— 说实话,除了这一本和另一本叫《游戏编程模式》的之外,我还没见到另外以『游戏』作为设计模式讲解的书 (如果有的话,希望提醒下)