Alistair Doulin:阐述游戏开发者应重视的SOLID原则

SOLID原则是由著名的“Uncle Bob”(Robert C. Martin)所提出并且由5个软件开发原则组合在一起的。它们是一组面向对象设计(OOD)的指南,特别是关于类设计。这些原则非常受敏捷开发项目程序员的欢迎,但是却甚少被游戏开发者所利用。所以我将通过本篇文章详细介绍这些原则并阐述如何将其运用于游戏开发。

solid principles(from doolwind.com)

solid principles(from doolwind.com)

单一责任原则(Single Responsibility Principle)

“类的改变总是只存在一个原因。”

single_responsibility_principle(from globalnerdy.com)

single_responsibility_principle(from globalnerdy.com)

第一个原则是设置基础,如果能够正确遵循这一原则的话便能够创造出不错的效果。它指出每个类必须只拥有单一责任以及一个改变原因。确保每个类够小且够集中,从而让开发者清楚该去哪里找自己所需要的内容或者该在游戏中添加哪些特殊功能等。

为何就不能拥有多个责任?多个责任也就意味着每个独立的代码间存在着连结。这时候一种责任的改变将降低类的功能而导致它难以满足其它责任的要求,并且最终只能创造出一个糟糕的设计。“为何渲染API的改变会破坏整体游戏状态?”

修复代码以打破这一原则的方法便是将每个责任按照各自的类进行区分。第一步便是从每个责任中提取一个界面。从而让其它类能够依赖于这些界面而不是类本身。我们便可以基于不同责任(执行单一界面)将这些类区分为不同的类。

你何时明确这一原则?——通常情况下打破这种原则的罪魁祸首便是拥有成百上千行的类。也就是我们所熟知的“GameObject”或“Entity”类,即人们总是会在其中盲目地添加各种代码。这种类通常会有500个以上的改变原因,这也就等于它将应对500个责任。所以这里总会不断涌现各种可怕的漏洞。

开闭原则(Open Closed Principle)

“软件实体(游戏邦注:也就是类,模块和函数等内容)应该能够扩展但却不易修改。”

Open Closed Principle(from doolwind.com)

Open Closed Principle(from doolwind.com)

这一原则的目标是确保每个类尽可能频繁地发生改变,并能够将被用于多种情况中。尽管这两种要求听起来相互矛盾,但是它们却能够通过互补而创造出强大的设计。类能够进行扩展也就意味着随着类的行为能够根据需求改变而以新方式发生改变。而当这些改变都不需要任何源代码变化时类便不能进行修改。

我们可以使用受数据驱动的设计来解释这一原则。通过将所需要的配置数据转到类中,我们便可以轻松地扩展类而不需要对其做出修改。同时我们还应该将任何变量(从数学意义上看)转移到类中从而确保类本身的定义不会只是关于类本身的功能。如果是从基本的OOD原则的数据和操作来看,这应该是最简单的方法吧。类能够定义自身功能的操作以及相关操作数据。而我们则需要尽可能将这些数据转移到类/功能中。这将能够把类本身以外的数据配置转移到调用代码中,从而提高类的改变能力。

这应该是你最害怕签到的文件吧,因为似乎所有人都在使用这一文件。但是不管你致力于创造何种系统,总是不可避免需要用到这些文件。

里氏替换原则(Liskov Substitution Principle)

“使用指标或参考基本类的函数必须能够使用派生类对象,并且无需了解它。”

Liskov Subtitution Principle(from ianfnelson.com)

Liskov Subtitution Principle(from ianfnelson.com)

继承性与多态性是两种非常强大的机制,能够使用一些简单的方法去解决各种复杂的问题。同时它们也有可能创造出漏洞和问题代码。基于这种原则我们需要确保继承体系的合理性,并且不会被代码所滥用而引出各种难以发觉的漏洞。尽管从表面上看来这种原则很简单,但是我们却很难正确去理解它们。

解决这一问题的第一步便是找到实例以核查对象类型——包括其本身及其目标对象。在这个简单的步骤中蕴含着一个基本原则,即“契约式设计”。我们必须确保在调用每个函数前它们都拥有一组真实的条件(前提条件),并且在完成调用后所有函数都将符合自己所对应的条件(后置条件)。所有致力于这项工作的程序员内心都清楚这些条件。而我们的第一步便是将这些条件转换成代码。当我们完成了这一步骤便能够满足以下规则了,即“派生类只会削弱前提条件而加强后置条件。”换句话说,派生类的功能既不应该超过也不该弱于它们的基础类。这一原则非常重要,因为一个被孤立看待的模块总是难以生效。你只有在“Tank”类的根源,同科或其它游戏系统环境下进行它时,你才能清楚它是否真正有效。

我们很容易明找到违背了这一原则的类。只要去找到使用RTTI的基础类去明确它自己所属类型(或它所面向的对象的类型)即可。当“ GameEntity”类校验它是否属于使用特殊码的“Tank”类,这就说明你打破了这一原则。这种类必须能够在忽视对象类型的前提下多形态地调用功能。

界面分隔原则(Interface Segregation Principle)

“不应该强迫用户依赖于他们未曾使用的界面。”

Interface Segregation Principle(from doolwind)

Interface Segregation Principle(from doolwind)

我们应该使用界面去推动两种不同对象间的交流,并创造出整洁,标准的代码。如果我们能够保证自己所使用的界面本身就足够整洁且标准,我们便能够基于这一界面推动理念的进一步发展。界面越大,客户端便会越发依赖于其它对象的功能。而如果我们能够提供一些较小且相互隔离的界面,那么每个对象便能够依赖于它所需要的一些小套的功能。这便减少了对象间连接的复杂性,更重要的是能够让别人在阅读了你的代码后便能立刻知晓每种类所依赖的对象。比起提供一个广大的界面,我们选择将界面分割成具有各种功能的群组,并且每个群组面向于不同客户端。

这一原则能够与单一责任原则有效地联系在一起。在这种情况下每个界面都拥有自己的单一原则,从而让我们能够基于界面的要求清楚地呈现出每个对象的功能要求。

着眼于你的所有界面(抽象类)并确保它们的所有功能列表都具有同质性。如果在界面定义中出现了一些功能分组,便说明你违背了这一原则(这是一种简单的判断方法)。在这里空格便是关键,也就是在函数群组间存在着越多空格便意味着它们彼此间越分散。

依赖倒置原则(Dependency Inversion Principle)

“高层次的模块不应该依赖于低层次的模块,这两种模块必须依赖于抽象体。”

“抽象体不应该依赖于细节内容。而细节内容则应该依赖于抽象体。”

Dependency Inversion Principle(from doolwind)

Dependency Inversion Principle(from doolwind)

但是关键却在于,直到最近我也从未在任何游戏开发中听到过这一原则。这一原则与众多开发者所坚持的做法截然不同。通常情况下如果一种类基于另外一种类,那么客户端必将会认为这一对象的类亦是如此并依此行动。而依赖倒置(游戏邦注:也被称为控制倒置)则与此大相径庭。比起让客户端负起创造对象的责任,这一原则将根据所所依赖的对象做出选择。这就抵消了客户端的控制权,而将其转移到客户端的所有者身上——通常情况也就是游戏引擎。

渲染系统便是一个典型的例子。比起实例化一个渲染对象或直接调用渲染API的类,渲染系统应该接纳一个具有低层次渲染功能的界面。通过依赖于面向渲染系统的界面,我们便能够在不对客户端渲染系统造成破坏性改变的前提下改变低层次的渲染API了。很明显,如果出现了破坏性改变,那么低层次的渲染API界面也会要求做出改变。

两个系统是如何做到彼此间的对话?如果它们正在使用固体类并不断寻找能够依赖于界面的机遇便有可能进行对话。而最佳方法还是让类能够作为构造函数(这是它所面对的类的界面参考)中的一个参数。这同样也意味着子系统是一种可依赖的特殊类。

游戏邦注:原文发表于2011年2月28日,所涉事件和数据均以当时为准。

via:游戏邦/gamerboom

感谢支持199IT
我们致力为中国互联网研究和咨询及IT行业数据专业人员和决策者提供一个数据共享平台。

要继续访问我们的网站,只需关闭您的广告拦截器并刷新页面。
滚动到顶部