浅谈游戏开发中常见的设计模式
前言
因为游戏开发在架构上要考虑性能和实际需求,在尽量不引入第三方库、框架的前提下进行开发,所以在编码时候会经常用到设计模式对代码进行复用,对业务逻辑架构解耦,尽量减少hard code。
单例模式(Singleton Pattern)
单例模式经常会用在逻辑上唯一的对象(通常用于重量级资源)上,如Factory、Context、Resource、Pool和Service等,但在代码细节上需要注意开放出去的接口以及该接口的严格语义。单例模式通常带有生命周期函数,有利于结合框架自己的生命周期管理进行初始化或销毁操作。在开发过程中会遇到一些不好的例子,如变量都是类静态成员变量、方法都是静态方法,这样写的代码可能在调用上的结果符合“单例”这个语义,但是会让其他人感到困惑。
工厂模式(Factory Pattern)
使用工厂模式来统一创建对象有利于管理对象的生命周期,通常会组合单例模式、代理模式、策略模式,对复杂的对象进行组装,对创建的对象进行统一管理。统一对象入口的好处在开发初期可能不明显,但随着开发进度的推动,业务的越来越复杂,统一入口容易更好地跟踪对象的生命周期,也容易的对某类对象初始化时进行统一的操作。
策略模式(Strategy Pattern)
对于一些逻辑相似但实现的细节不同粒度又比较细的业务,可以将保证语义粒度适中的接口提取出来,按不同的实现逻辑来封装成不同的策略,在通过策略容器(通常是工厂容器)在上层业务中进行组合调用。这样既可以保证逻辑结构的清晰又便于扩展。在游戏开发中,状态机就是一个使用策略模式的例子,状态机本身就是一个策略容器,它将与状态相关的行为从大量的跳转中抽离处理,让代码结构更清晰明了。代码结构越清晰,跳转越少意味着出现BUG机率越低,调试起来更容易。个人觉得策略模式的关注点应该在接口的粒度上,它跟代理模式不同,要求粒度适中,根据具体的业务去动态选择策略使得封装的代码脱离于客户代码。使用策略模式来解耦代码是一个很好的选择。
代理模式(Proxy Pattern)
代理模式跟策略模式其实很相似,粒度比策略模式粗,用于控制访问对象。举个例子,有一个复杂的数据对象有一个通用的二进制序列化接口,因为业务扩大,序列化数据越来越大,而网络层限制了协议的最大长度。这时是需要改网络底层?把公共的序列化接口改掉?还是在调用的代码上hard code一次裁剪后的序列化代码?答案是显而易见的:只需要一个面对这个协议的对象访问器,而这个访问器只需要根据业务实现裁剪后的序列化接口。使用代理模式,更好的保护对象的封装,让频繁改变的业务跟稳定的对象隔离开来。
观察者模式(Observer Pattern)/订阅发布模式(Subscribe/Publish)
这两个模式主要为了解耦独立对象间的耦合,在这里放在一起来讲,因为这两个模式主要区别在结构上,观察者模式是直接耦合的,发布订阅模式是松散耦合的。在游戏开发上中经常看到XXXListener,这就是用了这两个模式(大多数用订阅发布模式)。在游戏开发中,除了框架提供的事件生命周期外,其实可以用这些模式去松散具体业务的耦合。举个例子,在一个复杂的养成系统上有很多条养成线,养成线互相有勾连,养成线的变动除了系统与角色的交互操作外,其他的养成线的交互操作应该只停留在这个养成系统本身,这时候就需要观察者模式/发布订阅模式将这些养成线的交互隔离开来。
模板模式(Template Pattern)
这个模式偏向于框架编程,在实际开发中需要考虑到common——domain的比重,在考虑实际需要的抽象程度和业务的粒度。抽象程度太高会导致开发效率降低和实现难度增大,抽象程度太低又会导致业务性太强以致不好扩展和维护,这是一个值得深思的问题。
组合模式(Composite Pattern)
面向对象最重要的复用方式多态和组合,组合模式将不稳定的对象隔离,以树状结构连接起来。使用组合模式的好处是频繁改动的部分不会影响到稳定的整体,但缺点是不能使用多态这个语法糖来进行直接复用。在开发中严谨地使用组合模式和继承会让代码结构清晰和更从容面对业务地变更。关注点在于变的部分,就像阴阳两极一样,在实际开发中怎么用组合/继承也是一个值得深思的问题。
后话
其实还有很多设计模式我没有提到,因为我的实际开发场景很少遇到。我认为,设计模式是死的,而业务是活的。按照《Clean Architecture》中Bob大叔介绍的软件设计原则,再结合实际加以运用,不断地提炼代码,相信你也会发现编码的乐趣。本人拙见,谨在此抛砖引玉。