设计模式是高于语言的一种编程思维和习惯。每种模式在不同的语言中都可能有不同的实现方式,重要的是理解思想而非死记某个结构。
通过对以下几个教程的学习,本文对编程设计模式,尤其是游戏编程模式的思想进行总结。
23大经典设计模式:https://www.bilibili.com/video/av24176315
网友总结:https://github.com/liu-jianhao/Cpp-Design-Patterns
游戏编程模式:https://gpp.tkchu.me/
首先明确七个面对对象设计原则:
七个面对对象设计原则
原则一:(SRP:Single responsibility principle)单一职责原则又称单一功能原则
核心:解耦和增强内聚性(高内聚,低耦合)
类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题。
原则二:开闭原则(OCP:Open Closed Principle)
核心:对扩展开放,对修改关闭。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。
根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。
扩展开放:
某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
修改关闭:
某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求是修改关的。
原则三:里氏替换原则(LSP:Liskov Substitution Principle)
核心:
1.在任何父类出现的地方都可以用他的子类来替代(子类应当可以替换父类并出现在父类能够出现的任何地方),子类必须完全实现父类的方法。在类中调用其他类是务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
2.子类可以有自己的个性。子类当然可以有自己的行为和外观了,也就是方法和属性
3.覆盖或实现父类的方法时输入参数可以被放大。即子类可以重载父类的方法,但输入参数应比父类方法中的大,这样在子类代替父类的时候,调用的仍然是父类的方法。即以子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松。
4.覆盖或实现父类的方法时输出结果可以被缩小。
原则四:依赖倒置原则(DIP:Dependence Inversion Principle)
核心:要依赖于抽象,不要依赖于具体的实现。
1.高层模块不应该依赖低层模块,两者都应该依赖其抽象(抽象类或接口)
2.抽象不应该依赖细节(具体实现)
3.细节(具体实现)应该依赖抽象。
可参考的实现方法:
1.通过构造函数传递依赖对象
2.接口声明实现依赖对象
原则五:接口分离原则(ISP:Interface Segregation Principle)
核心:不应该强迫客户程序依赖他们不需要使用的方法。
一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口当中。分离接口的两种实现方法:
1.使用委托分离接口。(Separation through Delegation)
2.使用多重继承分离接口。(Separation through Multiple Inheritance)
原则六:合成复用原则(CRP:Composite Reuse Principle)
核心:尽量使用对象组合,而不是继承来达到复用的目的。
该原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过向这些对象的委派达到复用已有功能的目的。
复用的种类:
1.继承
2.合成聚合
注:在复用时应优先考虑使用合成聚合而不是继承
原则七:迪米特原则(LOD:Law of Demeter)
核心:一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。
意思就是降低各个对象之间的耦合,提高系统的可维护性;在模块之间只通过接口来通信,而不理会模块的内部工作原理,可以使各个模块的耦合成都降到最低,促进软件的复用。
注:
1.在类的划分上,应该创建有弱耦合的类;
2.在类的结构设计上,每一个类都应当尽量降低成员的访问权限;
3.在类的设计上,只要有可能,一个类应当设计成不变;
4.在对其他类的引用上,一个对象对其它对象的引用应当降到最低;
5.尽量降低类的访问权限;
6.谨慎使用序列化功能;
7.不要暴露类成员,而应该提供相应的访问器(属性)
下面开始总结几个游戏中很常用的经典设计模式。
1.命令模式
核心思想:
根本目的是将“行为的请求者”和“行为的实现者”解耦,在面对对象语言中常见的手段为:将行为抽象为对象。
类图:
要点说明:
该模式可理解为回调机制的面对对象版本,在这个模式中,命令是具现化的方法调用。
该模式很有用的场合是实现诸如撤消,重做,回放,时间倒流之类的功能。如下例是带有回撤功能的移动命令(c#)。当用户输入移动命令时,可创建一个该类的对象执行移动或撤回。
//命令基类
class Command
{
public:
virtual ~Command() {}
virtual void execute() = 0;
virtual void undo() = 0;
};
//带有回撤功能的移动命令类
class MoveUnitCommand : public Command
{
public:
MoveUnitCommand(Unit* unit, int x, int y)
: unit_(unit),
xBefore_(0),
yBefore_(0),
x_(x),
y_(y)
{}
virtual void execute()
{
// 保存移动之前的位置
// 这样之后可以复原。
xBefore_ = unit_->x();
yBefore_ = unit_->y();
unit_->moveTo(x_, y_);
}
virtual void undo()
{
unit_->moveTo(xBefore_, yBefore_);
}
private:
Unit* unit_;
int xBefore_, yBefore_;
int x_, y_;
};
该模式通过配合组件模式,可将多个命令封装为复合命令。
在C++中可以使用函数对象(https://blog.youkuaiyun.com/qq_37553152/article/details/85695416)来代替此模式以获得更高的效率,因为函数对象是编译时绑定,命令模式是运行时绑定。
2.享元模式
核心思想:
在软件系统中,采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价,主要指内存需求方面的代价。比如游戏中的森林场景中有很多模型相同的树对象,类似如下情况:
享元模式用共享技术来优化以上情况。享元模式中有两种状态,内蕴状态(Internal State)和外蕴状态(External State)。
1. 内蕴状态,是不会随环境改变而改变的,是存储在享元对象内部的状态信息,因此内蕴状态是可以共享的。对任何一个享元对象而言,内蕴状态的值是完全相同的。
2. 外蕴状态,是会随着环境的改变而改变的。因此是不可共享的状态,对于不同的享元对象而言,它的值可能是不同的。
享元模式通过共享内蕴状态,区分外蕴状态,有效隔离系统中的变化部分和不变部分。优化后的情况可以想象成下图。
类图:
传统GOF的享元模式类图如下,感觉看起来更像是对象池模式。图中右下角表示有些对象是支持分享的而有些对象不支持,在这里并不是重点,重点是分享的思想,也就是图中享元工厂中的逻辑。
要点说明:
此模式主要解决面对对象的代价问题。
此模式根据不同的需求,代码结构有很大的变化,重点在于分享的思想。
下面是在游戏编程中使用享元模式的例子(c++),该例的结构没有严格按类图编写,但体现了分享的思想。
class Terrain
{
public:
Terrain(int movementCost,
bool isWater,
Texture texture)
: movementCost_(movementCost),
isWater_(isWater),
texture_(texture)
{}
int getMovementCost() const { return movementCost_; }
bool isWater() const { return isWater_; }
const Texture& getTexture() const { return texture_; }
private:
int movementCost_;
bool isWater_;
Texture texture_;
};
class World
{
public:
World()
: grassTerrain_(1, false, GRASS_TEXTURE),
hillTerrain_(3, false, HILL_TEXTURE),
riverTerrain_(2, true, RIVER_TEXTURE)
{}
void generateTerrain();
const Terrain& getTile(int x, int y) const;
private:
Terrain* tiles_[WIDTH][HEIGHT];
Terrain grassTerrain_;
Terrain hillTerrain_;
Terrain riverTerrain_;
};
void World::generateTerrain()
{
// 将地面填满草皮.
for (int x = 0; x < WIDTH; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
// 加入一些丘陵
if (random(10) == 0)
{
tiles_[x][y] = &hillTerrain_;
}
else
{
tiles_[x][y] = &grassTerrain_;
}
}
}
// 放置河流
int x = random(WIDTH);
for (int y = 0; y < HEIGHT; y++) {
tiles_[x][y] = &riverTerrain_;
}
}
const Terrain& World::getTile(int x, int y) const
{
return *tiles_[x][y];
}
未完待续