设计模式是程序员的内功,随着工作经验的增长,愈发认识到设计模式的重要性。本篇文章是对设计模式的总结,随着日后在开发和学习过程中接触和使用更多的设计模式再以实际的应用案例记录笔者的学习过程。
就一个类而言,应该仅有一个引起它变化的原因。
在系统中,一个类大到模块,小到方法,承担的职责越多,被复用的可能性就越小。
而且这个类承担的职责过多,就可能造成一个职责的改变影响其他职责。
设计类时需要设计人员发现类的不同职责并将其分离,这通常需要设计思维和实践经验。
软件实体(类或模块)应该对扩展开放,对修改关闭。即类模块应该是可扩展的,但是不可修改。
开闭原则就是指在尽量不修改原有代码的情况下进行扩展。
为了实现开闭原则,我们需要对系统进行抽象化设计,抽象化是开闭原则的关键。
我们通过接口,抽象类来定义系统的抽象层,在通过具体类来实现扩展。如果需要修改系统的行为,无需对抽象层进行任何的改动,只需要增加具体类来实现新的业务,实现在不修改已有代码的基础上扩展系统功能,达到开闭原则的要求。
带来的好处:
让系统具有良好的灵活性、复用性和可扩展性;
稳定性高,不需要反反复复修改源代码,导致原工程出错;
不用反复修改测试类;
实现方式:接口,抽象类。
将一个基类对象替换成子类对象,程序不应产生任何错误或异常。
由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型对对象进行定义,而在运行时在确认其子类类型,用子类对象替换父类对象。
通常做法:将父类设计为抽象类或者接口,让子类继承父类或实现父类接口,并实现在父类中声明的方法。运行时,子类实例代替父类实例。
好处:通过子类可以代替父类的特性,可以很方便的扩展系统的功能,无需修改原有子类的代码,增加了新功能可以通过增加一个新的子类来实现。
高层模块不应该依赖底层模块,它们都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
通常做法:使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型转换等,而不要用具体类来做这些事情。
在实现依赖倒转原则时,需要针对抽象层进行编程,而将具体类的对象通过依赖注入的方式注入到其他对象中,
依赖注入是指当一个对象要与其他对象发生关系时,通过方法参数来注入所依赖的对象。
常用方法有3种:
1)构造注入:构造注入是指通过构造函数来传入具体类的对象。
2)设值注入:是指通过Setter方法来传入具体类的对象。
3)接口注入:是指通过在接口中声明的业务方法来传入具体类的对象。
这些方法在定义时使用的是抽象类型,在运行时在传入具体类型的对象,由子类对象来覆盖父类对象。
原则: 一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,
好处: 在引入抽象层后,系统将具有很好的灵活性,在程序中应该尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
每个接口中不存在派生类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口。
实现:“定制服务”,将大接口的众多方法根据职责分别放在不同的小接口中,每个接口只包含一个子模块所需的方法即可,这种方法也成为了定制服务,即为不同模块提供宽窄不同的接口。
如果不遵循接口隔离原则,实现一个接口就需要实现该接口中定义的所有方法,就会带来具体类中出现大量空方法,无用代码。
造成实现类不知道用哪个方法或用错方法。
注意:
控制接口粒度,接口不能太小,太小会导致系统中接口泛滥,不利于维护。
太大不灵活,一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫实现类依赖那些他们不用的方法。
优先使用对象组合,而不是继承来达到复用的目的。
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
a)继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
b)子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
a)它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
b)新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
实现:比如大雁会飞,大雁有一双翅膀的引用,具体怎么飞,交给翅膀,往上飞还是向下飞,加速还是减速,只需要传递信息给翅膀即可。
在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应该发生任何直接的相互作用,如果其中一个对象需要调用另一个对象的方法,可以通过“第三者”转发这个调用。
简而言之,就是通过引入一个合理的“第三者”来降低现有对象之间的耦合度。
通俗比喻:不要和“陌生人”说话(Don’t talk to strangers)、只与你的直接朋友通信(Talk only to your immediate friends)等
那么他的朋友是谁呢,有以下几类,否则就是陌生人
1)当前对象本身(this)。
2)以参数形式传人到当前对象方法中的对象
3)当前对象的成员对象。
4)如果当前对象的成员对象是一个集-合,那么集-合中的元素也都是朋友。
5)当前对象所创建的对象。
优点:
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易。
应用迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。一个对象的改变不会给太多其他对象带来影响。
实现:
在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,越有利于复用;
每一个类都应当尽量降低其成员变量和成员函数的访问权限;
在类的设计上,只要有可能,一个类型应当设计成不变类;
在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承体系;
合成复用原则告诉我们优先使用组合而不是继承;
依赖倒置原则告诉我们要面向接口编程;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合;
开闭原则是总纲,要对扩展开放,对修改关闭。
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
工厂模式(Factory Pattern)
抽象工厂模式(Abstract Factory Pattern)
单例模式(Singleton Pattern)
建造者模式(Builder Pattern)
原型模式(Prototype Pattern)
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
适配器模式(Adapter Pattern)
装饰器模式(Decorator Pattern)
代理模式(Proxy Pattern)
外观模式(Facade Pattern)
桥接模式(Bridge Pattern)
组合模式(Composite Pattern)
享元模式(Flyweight Pattern)
这些设计模式特别关注对象之间的通信。
策略模式(Strategy Pattern)
模板模式(Template Pattern)
观察者模式(Observer Pattern)
迭代器模式(Iterator Pattern)
责任链模式(Chain of Responsibility Pattern)
命令模式(Command Pattern)
备忘录模式(Memento Pattern)
状态模式(State Pattern)
访问者模式(Visitor Pattern)
中介者模式(Mediator Pattern)
解释器模式(Interpreter Pattern)
地址:https://gpp.tkchu.me
作者:Bob Nystrom
基础设计模式
0、设计模式应用
除非确信代码必需存在灵活性,否则不要浪费时间用于抽象和解耦;
开发过程中对性能的思考和设计,要推迟那些降低灵活性、底层的、详尽的优化;
尽快探索设计空间,但不要太快成型,降低灵活扩展性;
以后要删除的代码不要浪费时间去整理得很整洁;
1、命令模式
把玩家和AI的指令做成命令流(用队列的方式),调用的时候只需要传入被控制角色参数即可——编程语言有闭包直接用闭包,没闭包建命令类。
2、享元模式
将能够共享的数据(例如纹理),做成单独的类,用API调用,提供共享数据和使用数据的实例列表和实例的差异化参数来调用,相同实例只需创建一次,就能在多处使用(地形系统),常用于GPU渲染。
3、观察者模式
发送消息通知对消息感兴趣的对象,不用关心谁接收到了通知(由外部代码控制谁接收通知)。观察者(例如成就系统的某一个成就)接收通知,被观察者(例如可被观察的物理模块)有一个观察者集合(如果需要在观察者类中添加状态,可以用链表),用于发送通知。C#中的event。
4、原型模式
使用特定的原型实例来创建特定种类的对象,并通过拷贝原型来创建新的对象。常用于怪物生成等游戏数据建模任务。
5、单例模式
确保一个类只有一个实例,并为其提供一个全局访问入口。
6、状态模式
允许一个对象在其内部状态改变时改变自身的行为,对象看起来好像是在修改自身类(有限状态机、并发状态机、层次状态机、下推自动机),常用于处理射击游戏主角的状态、游戏AI(行为树和规划系统)等。
序列型模式
1、双缓冲模式
用于解决状态在被修改的同时被读取的问题(周期性地交换两个缓冲区的引用)。
2、游戏循环模式
实现用户输入和处理器速度在游戏进行时间上的解耦。
3、更新方法模式
一系列几乎是相互独立的游戏对象需要同步运转,对象的行为与时间相关时用。
行为型模式
1、字节码
游戏引擎加载数据(模组)——解释器模式
2、沙盒模式
基类用于隐藏游戏引擎实现细节。
3、类型对象模式
当需要定义一系列不同的“种类”,但不想硬编码进类型系统(不知道将来会有什么类型),避开子类继承,保持多态灵活性。
解耦型模式
1、组件模式
单一实体横跨了多个域。每个域的代码都独立地放在组件类中,实体本身可以简化为这些组件的容器。对于庞大的类的解耦。例如unity3d的GameObject类的设计就是如此。
2、事件队列
对消息或事件的发送与受理进行时间上的解耦,例如窗体程序。
3、服务定位器
为某服务(特别是随时要被访问的服务,如游戏音频)。
优化型模式
1、数据局部性
通过合理组织数据,利用CPU的多级缓存机制来加快内存的访问速度(避免缓存未命中、分支预测失准、流水线停顿)。
2、脏标记模式
将工作推迟到必要时进行。例如缓存一些物体的世界变换,并用一个标记它是否过期,可以减少CPU的计算。
3、对象池
使用固定的对象池重用对象,取代单独地分配和释放对象。例如例子系统。让游戏能通过浸泡测试(防止内存扩张和泄露)
将相同类型的对象在内存上整合,能利用好CPU的缓存区提高效率。
4、空间分区模式
将数据存储在根据位置组织的数据结构中来高效地定位它们。
参考:
https://zhuanlan.zhihu.com/p/375287900
https://developer.unity.cn/projects/64a2d7b1edbc2a0ddca627bb
https://github.com/QianMo/Unity-Design-Pattern