概述
设计原则
-
开放封闭原则 对扩展开放,对修改关闭。
-
依赖倒置原则 高层组件和底层组件(的实现),都依赖于抽象层,以降低耦合。
-
组合复用原则 组合和继承都能达到设计目的,优先考虑使用组合。
-
迪米特法则/最少知识原则 一个对象对其它对象的了解应尽可能少,从而降低对象之间的耦合,提高系统可维护性。
-
单一职责原则 一个类应该只有一个引起变化的原因,即一个类的职责应该单一,只做一类事情,或对外提供一种功能。
-
里氏替换原则 子类可以替换父类。任何时候,派生类可以替换基类,系统功能不会受到影响。
设计原则(版本2)
-
找出应用中可能需要变化之处,把它们独立出来,和不变的代码分开。变化是扩展/实现,不变是框架。
-
针对接口编程,而不是针对实现编程。
-
多用组合,少用继承
-
努力降低耦合
-
对扩展开放,对修改关闭
-
依赖抽象,不要依赖事项
-
最少知识原则
-
别调用(打电话给)我们,我们会调用(打电话给)你
-
一个类应该只有一个引起变化的原因
耦合的类型
-
内容耦合 一个模块直接修改或操作另一个模块的数据,或一个模块不通过正常入口而转入另一个模块。最高耦合,避免出现。
-
公共耦合 两个或以上模块共同引用一个全局数据项(数据结构,共享通信区,共享内存区)。难以确定哪个模块操作了数据。
-
外部耦合 一组模块都访问同一全局简单变量(而不是同一全局数据结构)。
-
控制耦合 一个模块,通过传送开关、标志、名字等控制信息(控制信号),明显控制选择另一个模块的功能。
-
标记耦合 两个模块之间传递的是数据结构,如数组,记录名,文件名,数据结构地址,等。
-
数据耦合 模块之间通过参数传递数据。
设计模式
创建型模式
关注对象创建,将对象创建和使用分离。包括:简单工厂,工厂方法,抽象工厂,原型,建造者,单例。
简单工厂 Simple Factory
定义工厂类,提供接口,根据不同参数类型创建并返回不同类型的对象,这些类型的对象通常具有相同基类。调用者无需关心创建细节。

简单工厂可以不简单
引擎当中,有很多简单工厂,工厂内部通过 map<key,Constructor>的方式记录了类型-创建方法/对象 的映射。比如资产管线里的 Importor,启动时,为每种资产后缀名注册导入器类型,当需要时,根据后缀名和对应的类型名,通过反射创建导入器。
工厂方法 Factory Method
定义一个用于创建对象的接口,由子类实现,使类的实例化延迟到子类。

抽象工厂 Abstract Factory
提供一组接口,让这些接口创建一系列相关,或者相互依赖的对象,即按照产品族生产产品。

引擎当中,对平台差异的封装,通常采用这种模式。比如文件系统,渲染设备,抽象一组逻辑相关的接口类,同一个抽象工厂创建的这些接口类的实现,可以协同工作,实现复杂的功能。
原型 Prototype
通过对指定对象的赋值/克隆来创建对象。该类对象的创建过程可能很复杂,或者创建过程未知,只能通过克隆创建。
引擎当中,GameObject是通过组织 Component 来实现特定 GameObject 功能,可以有任意多种组合类型,因此我们实现了 Instantiate 接口,通过已有对象,创建新对象,就是原型模式。
ECS 架构也是原型模式。

建造者 Builder
将复杂对象的创建,与它的表示分离,使得同样的构建过程,可以创建不同的表示。
建造者提供建造过程的接口,以及获取建造结果(对象)的接口,其建造过程由 Director 驱动。

引擎当中,对于图形API的封装,也会用到该模式:经过封装的图形设备的启动,有其固定的流程,但是不同设备类型,对于流程中的每个步骤,实现各不相同。所以,RHI 既用到了抽象工厂,也用到了建造者。建造者跟抽象工厂相比,除了创建对象,还负责组织这些对象之间的关系。
单件 Singleton
保证一个类仅有一个实例存在,并且提供全局接口访问该实例。
在实现上,根据实例创建时机,分为两种:
-
用到时才创建,也叫懒汉模式
cppclass Singleton{ public: static Singleton* Instance(){ static Singleton _inst; return &_inst; } };
-
程序初始化时创建,不管后面会不会用到,也叫饿汉模式。
cpp//.h class Singleton{ public: static Singleton* Instance(){return _Instance;} private: Singleton(){_Instance = this;} static Singelton* _Instance = nullptr; }; //.cpp Singleton theSingleton; // 创建方式1 int main(){ Singleton theSingleton; // 创建方式2 }
Github 上的单件实现 https://github.com/AlexWorx/ALib-Singletons。实现了基于模板的单例,其文档,同时描述了在PC dll 下,懒汉单例模式存在的问题,并给出了解决方案。
结构型模式
关注对象之间的关系,如何组合协作以获得灵活的结构,简化设计。包括:装饰,外观,组合,享元,代理,适配器,桥接。
装饰 Decorator
别名:包装器 Wrapper
动态地给一个对象添加一些额外的职责。
装饰器和被装饰对象具有相同的基类,这样装饰器就可以取代被装饰对象,且可以用一个或多个装饰器包装一个对象。
外观 Facade
提供统一的接口,来访问子系统中的一群接口,简化系统的使用,以及客户端和系统间的解耦。
整理系统功能,将客户端需要的系统接口封装到更少的类中。
ERA1.5 引擎中,对引擎封装的接口层,就是 Facade 模式。

组合 Composite
将对象组合成树形结构,以表示"部分-整体"的层次接口,使对单个对象和组合对象的操作/访问具有一致性。
引擎中的 GameObject-Component 就应用了这种模式,但是有针对性的做了一些优化
在此基础上,引擎进行了一些优化,以对组件做一些限制,因为作为叶子节点,很多功能是不需要的,同时游戏对象和组件行为差异化较大:
进一步思考,可以将差异,实现在一个组件里,像 unity 中的 gameobject 不负责层级树和父子关系维护,而是由 transform 来完成。
享元 Flyweight
运用共享技术,有效的支持大量细粒度的对象。
-
程序中某种对象,数量很大,或对象本身很大
-
这种对象可以被分为内部状态和外部状态
-
内部状态,所有对象都是一样的
-
所有对象维持外部状态,共享内部状态

引擎渲染时,要使用大量的网格,或贴图,而这些网格和贴图可以被多个模型共享,是典型的享元模式的应用:
代理 Proxy
为其它对象提供一种代理,以控制对这个对象的访问。主要目的是为客户端增加额外功能,约束,或隐藏复杂细节。
引擎中的对象引用 ObjectReference 就是该模式:
引擎主要功能,是利用资产组织场景,以进行计算/渲染。场景创建过程中,是对"磁盘"上资产文件的引用,比如硬盘某个目录下的文件/文件夹。但是资产文件不仅在本地磁盘,还可能由其它文件协议提供:ftp://, file://, http://, memory:// 等。如果不希望对每种资产文件形式实现场景组织逻辑,就需要定义抽象的代理层:
适配器 Adapter / 包装器 Wrapper
将一个类的接口转换成客户希望的另外一个接口。该模式使得原本由于接口不兼容而不能一起工作的类,可以一起共组。
适配器可以通过派生或组合的方式,实现被适配对象的引用。
stl 中的 stack, queue 就是对 dequeue 进行适配,表现出 stack , queue 的行为。
引擎中的物理系统,可以应用该模式:

桥接 Bridge
将抽象部分与实现部分分离,使它们可以独立的变化和扩展。
-
抽象部分一般指业务功能
-
实现部分一般指具体平台的实现
-
抽象部分,实现部分,还可以归结为系统功能的两个维度,在两个维度上,利用组合,而不是派生,这样每个维度可以分别自由变化/扩展。

更一般的,
-
Abstraction:抽象部分接口,同时包含指向 Implementor 类型对象的指针。
-
RefinedAbstraction:扩展抽象部分接口,主要是实现 Abstraction 定义的接口,并调用 Implementor 的接口。
-
Implementor:实现部分接口,定义实现类的接口。可能跟 Abstraction 一致,也可能不一致。
-
ConcreteImplementor:实现 Implementor 的接口

引擎中,渲染系统可以应用该模式:
-
Abstraction 侧,是渲染器抽象层及实现逻辑
-
Implementor 侧,是设备层抽象层及实现层逻辑
行为型模式
关注对象行为和交互,定义一组对象如何协作完成整体任务。包括:模板方法,策略,观察者,命令,迭代器,状态,中介者,备忘录,职责链,访问者,解释器。
模板方法 Template Method
定义一个功能的算法骨架,将骨架的一些步骤延迟到子类实现,使得子类可以不改变一个算法的结构,即可重定义一些步骤。

引擎中,该模式应用比较多,比如组件的多数接口,都应用了:
cpp
void Component::Update()
{
if (m_updateCount == 0)
{
Start(); // OnStart()
m_updateCount++;
}
OnUpdate();
}
引擎中,应用该模式实现了最基本的执行过程:(虽然不是一模一样,但是实现模式时根据具体情况作出以下变动也很正常)
策略 Strategy / Policy
定义一系列算法/功能,并将它们封装起来,使其可以根据上下文互换。
-
Strategy 策略,定义算法/功能接口
-
ConcreteStrategy 实现特定的策略
-
Context 上下文,用于确定使用一个策略

策略模式也是引擎中使用比较多的模式:可以用数据,或特定逻辑来定义上下文,以确定应用哪个算法,例如资产导入管线:
观察者 Observer
定义对象之间1对多的依赖关系
引擎中,为了降低耦合,大量使用该模式进行事件传递。核心是委托,但是其实现比观察者模式要复杂一些:
命令 Command
将一个请求封装成对象,从而可以将请求对象以参数形式进行传递;对请求排队,或记录,以实现重做或撤销操作。

引擎编辑器的用户操作,应用命令模式,封装成对象,以实现重做或撤销:

迭代器 Iterator / 游标 Cursor
提供一种方法顺序访问一个聚合对象中的各个元素,而不暴露内部表示。
stl 基本模式之一

引擎中的 GameComponentQuery 实践了该模式,提供了迭代器,来遍历收集到的组件

状态 State
允许一个对象,在其内部状态改变时,改变其行为,对象看起来似乎修改了它的类。
需要考虑状态转换:
-
由状态完成
-
由转换表完成
状态模式最常用法是实现状态机
参与者:
-
Context:上下文,比如FSM
-
定义用户感兴趣的接口
-
维护当前状态
-
可能还负责创建和维护所有状态
-
-
IState:定义状态接口,以完成 Context 功能
-
ConcreteState:具体的状态类

引擎中的动画状态机,就应用了状态模式,参考设计(实际上考虑很多其它方面,要比这复杂的多):

中介者 Mediator
用一个中介对象来封装一系列的对象交互,使各个对象不需要显式的互相引用,从而降低耦合,而且可以独立改变它们之间的交互。
该模式适合解决,一组对象,需要通过复杂的互相调用/通讯实现逻辑,导致产生复杂的依赖/引用关系。
我们最熟悉的应用中介者模式的模式,就是 MVC 模式。编辑器的大量逻辑,都实践了该模式
-
Mediator 中介者,定义接口,与各个同事通信。
-
ConcreteMediator 具体中介者,通过接口协调同事对象完成交互,实现逻辑。
-
Colleague 同事,参与系统,具体实现一些功能。同事之间通过中介者与其它同事通信。

备忘录 Memento
在不破环封装的前提下,捕获对象的内部状态,并在该对象以外保存这个状态,以后可以将该对象恢复到保存的状态。
引擎中的对象,很多需要保存(在内存/磁盘中),只需要将这些对象的关键数据成员(即内部状态)序列化到流,即可

职责链 Chain of Responsibility
使多个对象都有处理请求的机会,避免请求发送者和接收者之间的耦合关系。将这些对象串成一条链,沿着该链传递请求,直到有一个对象处理它。
提交请求的对象,并不明确哪个对象会处理它(甚至没有对象处理,或多个对象处理(某些系统的特殊需求))。
引擎中的输入系统,快捷键系统,都可以应用该模式。
访问者 Visitor
表示 作用于系统中对象的操作,使得可以在不改变这些对象的前提下,定义/扩展/改变作用于这些对象的操作。简单说,允许一个/一组操作,应用到一个/一组对象上,将对象和其上的操作解耦。
参与者:
-
Visitor 访问者,为系统中每种类型声明 visit 操作。
-
ConcreteVisitor 具体访问者,根据具体算法,实现访问系统中类型的接口。同时提供上下文,存储中间结果。
-
Element 元素,定义 accept 操作,以 visitor 为参数。
-
ConcreteElement 具体类型
-
ObjectStructure 对象结构,能够枚举元素,并提供接口访问元素。

引擎中的特效系统,粒子数据,和对其的各种操作,是需要分离的,可以参考该模式进行设计(根据具体情况,不会照搬模式)

解释器 Interpreter
定义一个语言的文法(语法规则),并建立一个解释器,解释执行该语言的句子。
参与者:
-
AbstractExpression 抽象表达式,声明一个抽象的解释操作。
-
TerminalExpression 终结符表达式,实现文法中终结符相关的解释操作。
-
NonterminalExpression 非终结符表达式,实现非终结符的解释操作。非终结符表达式可以包含终结符,非终结符表达式,因此该解释过程通常是递归的。
-
Context 上下文,包含解释器中的其它全局信息。

引擎中,FlowGraph 的执行,就是实践了该模式。由于并没有实现很底层的语法规则,仅仅是定义了节点,以及执行流程,所以设计上不是按照 terminal / nonterminal 来设计的,而是设计了过多样化的 Expression 来实现复杂功能。
