|
游戏开发设计模式回忆-导语
为什么要回忆?
- 在项目推进的过程中,个人做了很多需求开发。
- 在版本迭代中发现,不好的代码设计其实是在给自己埋无数的坑。 终有一天问题集中爆发的时候,版本风险就极其不可控了,甚至改一处就需要全量测试。
- 本人也是在之前学过全套的理论,现在浅薄地自认为有了一些经验。
- 现在回过头看可以更加深刻理解一下这些前辈们留下的成熟经验。
回忆中的核心思想
痛点,这是本人在编码过程中的最 Care (放句洋屁)的一个点。 *
只有了解原先的设计,设计的缺陷及痛点,更好的设计,才是学习代码,精进自己代码的最优解。
为什么还要提及一些老生常谈的概念?
为什么还要提及烂到大街的概念?
因为其实每个最简单的知识,背后隐含的东西都是庞大的。 很多理论我们自以为是地认为知道,但是实际上一被问到就拉稀。
如何在向别人讲述起理论的时候,有整套自己精简过的知识体系对答如流?
这个问题一直是鄙人研究的核心问题。
由核心思想指导你解决问题,而不是由解决问题的过程去推导你自己有的核心思想。
这是一个自上而下的过程而不是自下而上的过程。
起航
面向对象编程
主旨
- 面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式。
- 个人简化:在编程过程中把问题里面的内容进行概念抽象,以抽象出的个体类为核心解决问题的编程方法。
特性
- 该编程方法需要编程语言有以下特性进行支持,同时以下特性也是设计思想。
- 封装:
- 目的是为了隔离变化,隐藏内部实现细节,简化外部调用复杂度。
- 把稳定的对外接口设置为public,易变的内部实现代码细节设置为private。
- 把大量不需要的接口不允许外部调用,简化调用者的可调用函数数量。
class CalculateProcess
{
private:
void Step1(){};
void Step2(){};
void Step3(){};
void Step4(){};
public:
//启动计算流程
//其实我们可以看到,对外只需要看到启动和结束流程
//两个人分工的话,一个人关注这个类的维护,不需要关注调用者如何调用。调用者只需要关注Start()和End接口就行。
//从这个角度看,其实把变化对外部隔离开了,我自己怎么变都行,外部不需要关心。
void Start()
{
Step1();
Step2();
Step3();
Step4();
};
void End(){};
};
<li data-pid="G37IDyC-">多态:
- 目的是为了子类能够代替父类,隔离变化,对接口的设计可以对依赖关系进行解耦。
//目前以我的观点看继承+多态有三种最显著的应用。
//1.解耦依赖,缩短编译时间。
//2.隐藏实现,保护源代码。
//1和2一起来说一下
//以游戏引擎引入的第三方库为例
//对于游戏引擎开发者来说,只需要关注接口就行。
//第三方库的开发者来说,只需要关注库本身的开发
//节省了各自开发的时间
//而且我们只把.h和lib给使用者,.h里面只有虚函数接口,保护了自己的实现代码。
//.h
enum InstanceType
{
EPoint2D,
EPoint3D,
EPoint4D,
}
class Interface_PointA
{
public:
static Interface_PointA* GetInstance(InstanceType InInstanceType);
private:
};
//.cpp编译出来的.lib
#include &#34;XXX.h&#34;
class Point2D :public Interface_PointA
{
public:
private:
};
static Interface_PointA* GetInstance(InstanceType InInstanceType)
{
switch(InInstanceType)
{
case InstanceType::EPoint2D:
return new Paper2D();
break;
}
return nullptr;
}
//3.搭建框架,分工合作,隔离变化。
//搭框架的人可以和细化框架的人分工合作
//各自处理各自的需求
class CalculateProcess
{
protected:
virtual void Step1(){};
//子类可以重写
virtual void Step2(){};
virtual void Step3(){};
//子类可以重写
virtual void Step4(){};
public:
void Start()
{
Step1();
Step2();
Step3();
Step4();
};
void End(){};
};
class CalculateProcessDetail:CalculateProcess
{
virtual void Step1(){};
virtual void Step2(){};
virtual void Step3(){};
virtual void Step4(){};
};
接口类与抽象类的区别
- 接口类一般支持多继承,不能包含属性
- 抽象类一般只应用在单继承场景,可以包含属性。
设计模式中的各种原则
<hr/>
- -SRP 单一职责原则 一个类或者模块只完成一个功能分支。 个人觉得功能分支更为恰当.
<hr/>
- -OCP 开闭原则 对扩展开放,对修改关闭。 提前留好拓展点
//伪代码如下:
//改写Monster类,受到伤害给其他东西一个通知
void takedamage(AActor* InDamagedResource,int DamageAmount)
{
}
//=============改写方法1
//Monster类
//声明变量
AActor *NotifyedActor1;
AActor *NotifyedActor2;
AActor *NotifyedActor3;
///...
AActor *NotifyedActor100;
void takedamage(AActor* InDamagedResource,int DamageAmount)
{
NotifyedActor1.Notify();
NotifyedActor2.Notify();
NotifyedActor3.Notify();
///...
NotifyedActor100.Notify();
}
//=============改写方法2
//向扩展开放,向修改关闭修改
//省略了验证指针有效性步骤
//示例代码太复杂就看不懂了
//Monster类
class AActor:public Interface_A
{
public:
virtual void Notify()
{
//
}
};
class AActor1 :public Interface_A
{
public:
virtual void Notify()
{
//
}
};
typedef void (Interface_A::*DelegateFunc)() ;
class Monster
{
public:
struct DelegateData
{
Interface_A* P;
DelegateFunc FUNC;
DelegateData(Interface_A* InNewActor, DelegateFunc InNewFunc)
{
P = InNewActor;
FUNC = InNewFunc;
}
};
vector<DelegateData > NotifyedActorArray;
void AddNotify(Interface_A* InNewActor,DelegateFunc InNewFunc)
{
NotifyedActorArray.push_back(DelegateData(InNewActor, InNewFunc));
}
void TakeDamage()
{
for (size_t i = 0; i < NotifyedActorArray.size(); i++)
{
(NotifyedActorArray.P->*NotifyedActorArray.FUNC)();
}
}
};
- 每次增加通知都需要修改Monster类
- 只能调用Actor类的Notify函数
<li data-pid="q9yxtXyY">如果此时有需要,同时需要通知多个非Actor对象的多个函数? 你该如何处理?试想一下:
每个版本只要修改了这个部分, 一定是要进行测试的,
修改了Monster类, 意味着影响范围会非常大。
没有哪个程序员能保证自己修改100%没有问题。
付出的成本会很高。
测试时间+CoderReview时间
============================
- 增加了代理机制。
- 同时需要通知多个非Actor对象的多个函数。
- 每次新加通知不需要修改Monster类。
<li data-pid="RCjWjh-k">如果此时有需要,同时需要通知多个非Actor对象的多个函数? 你该如何处理?<li data-pid="c7yHNlzg">需要给每个通知都加一个变量和加一行代码。试想一下:
每个版本只要修改了这个部分, 一定是要进行测试的,
这种类似的按照开闭原则设计的类或者模块大量存在,
其实会大大降低测试时间+CoderReview时间,
在大规模商业化程序设计中,时间就是金钱。
<hr/>
- -LSP 里式替换原则
- Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
- 在不知道子类具体内容的情况下,在程序中父类指针指向的对象可以直接拿子类对象来替换。
- 换句话说:你的子类对象是可以在你没有看过子类代码的情况下,直接替换父类对象的。
- 满足这个定义就要求:
- 子类不能改变父类的基本流程。
- 子类的修改仅限于对父类拓展与增强。
<hr/>
- -ISP 接口隔离原则 Clients should not be forced to depend upon interfaces that they do not use 当前的类不应该强行继承他们根本用不到的接口.
- 个人认为两个方向看这个问题
<li data-pid="WMc1MUM0">包含多个功能分支的类依赖了多个包含多个功能分支的接口,部分接口压根用不到。<li data-pid="QrOLe81T">修改建议:
- 拆分用到接口的业务类,保证单一职责。
- 拆分臃肿的接口,保证单一职责。
<hr/>
- -DIP 依赖倒置原则
- High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
- 翻译过来:高层模块不应该依赖低层模块。高层模块和低层模块依赖抽象,抽象之间互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
//给各位看一段代码
//我们此时有一个第三方库
//此时我们想在引擎中引入,而且在调试的时候希望利用引擎Log系统输出第三方的LOG信息
//而且有一个要求:第三方组件不能依赖引擎,因为第三方组件是面向所有引擎的。
//你应该如何去做?
//================================第三方库.h==================
class Abstarct_LogManager
{
public:
virtual void Print(const char* InTest) = 0;
};
void Start(Abstarct_LogManager* B);
//================================第三方库.CPP(.lib)==================
//不给到用户,只给.lib
#include &#34;Test.h&#34;
Abstarct_LogManager* log;
void Start(Abstarct_LogManager* B)
{
log = B;
log->Print(&#34;Hello1&#34;);
log->Print(&#34;Hello2&#34;);
log->Print(&#34;Hello3&#34;);
log->Print(&#34;Hello4&#34;);
}
//==============================给第三方库创建中间层Middle.h==================
#include&#34;Test.h&#34;
template<typename T>
class Impl_Abstarct_LogManager:public Abstarct_LogManager
{
public:
T* Outer;
Impl_Abstarct_LogManager(T* InOuter)
{
Outer = InOuter;
}
virtual void Print(const char* InTest)
{
Outer->EnginePrint(InTest);
};
};
//==============================引擎层==================
//引擎链接.lib,拿过来.h
#include&#34;Test.h&#34;
class LogManager
{
public:
LogManager();
~LogManager();
void EnginePrint(const char* InTest)
{
cout << InTest <<endl;
}
private:
};
void InitialEngine()
{
LogManager Logmanager;
Impl_Abstarct_LogManager<LogManager>b(&Logmanager);
Start(&b);
}
//为什么这么做?因为这么做实现了
//1.依赖关系正确:
//引擎层依赖中间层和第三方库,
//中间层依赖第三方库,
//单向依赖,
//没有任何双向依赖。
//2.保证了第三方库的通用性
//第三方库不需要针对引擎进行单独修改
//3.第三方库通过抽象和引擎层交互,而具体实现依赖于抽象
//如此设计第三方库不需要修改即可适配所有引擎,对应引擎只需要自己实现一个中间层就行。<hr/>
- DRY 原则: 不要写重复的代码。 我觉得这一条最难应用,因为没人愿意写重复代码。 功能重复,流程校验重复这些都算,但是如果考虑到风险原因,改与不改就真成问题了。
<hr/>
- YAGNI 原则: 当前的设计满足当前的需求且留有一定拓展性,不需要过度设计。
<hr/>
- KISS 原则:Keep it Simple and Stupid 其实就是在保证代码质量的前提下,让代码设计的尽量简单易读,别整一些花里胡哨的~
<hr/>
- LOD 法则: 高内聚,低耦合 高内聚是一个功能分支相关的高度依赖的集中到一起。 低耦合是减少多个功能分支之间的直接依赖关系。
|
|