python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(1)

概述

基本现在的编程语言都是支持甚至是鼓励人们使用面向对象编程(OOP)。虽然近两年编程界对"一切皆OO"的思想有一些微妙的转变,随之也出现了一些不是唯OOP的语言(比如Go,Rust,Elixir,Elm,Scala),虽然他们大多数也还支持对象。注意我们本文叙述设计原则也适用于非OOP语言。

要写出一手清晰,高质量,可维护和可扩展的代码,一个码农需要了解和利用几十年内业界积累下的经验和通过实践证明有效的设计原则。这就是本文主题的意义所在。

文中所及的方法将以Python为例。有些例子可以证明一个观点,其他方面可以不会论及太多。

对象类型

由于OOD整个过程是围绕对象对代码建模,因此我们首先要知道其不同的类型和和变体。总共有三种类型的对象:

1.实体对象

这类对象通常对应于问题空间中的现实世界的实体。我们假设要创建一个角色扮演游戏(RPG),我们可能需要设计一个实体是一个人物Hero类:

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(2)

这个类的对象一般会包含有关身体的属性(如健康或法力),并且可以通过某些规则进行修改。

2.控制对象

控制对象(有时也称为管理对象)负责协调其他对象。主要用于控制和调用其他对象的对象。比如RPG游戏中的一个很好的例子就是Fight类,它控制着两个英雄并使他们战斗。

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(3)

像这样在控制类中封装战斗的逻辑可以为我们提供了多种好处:其中之一是动作的可扩展性。比如非常方便的导入一个非玩家角色(NPC)类型,用以英雄战斗,只要给他相同的API。我们也可以非常轻松地继承该类并覆盖一些功能以满足需求。

3.边界对象

这些对象是位于系统边界。任何从另一个系统接受输入或产生输出的对象,无论该系统是用户,互联网还是数据库,都可以被划分为该类。

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(4)

边界对象负责将信息传入或者输出系统。比如在一个终端系统中,我们需要边界对象将键盘输入(比如Enter建)转换为可识别的系统事件(输入敲打的字符)。

奖励:值对象

值对象表示您的域中的一个简单值。他们是不变的,没有识别符。

类比我们的游戏系统,那么钱币和伤害类就是这类。所述对象使我们能够轻松地区分,查找和调试相关功能,而使用原始基本类型(整数或整型数组)的则不好做到。

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(5)

一般将它划分为实体对象的子类别。

关键设计原则

设计原则是软件设计中的基本规律,是多年来实践证明它们确实有效的做法。严格遵守这些规律,将帮由于我们开的软件的质量和开发进度。

抽象

抽象是在某些情况下将概念简化为其基本要素的概念。通过将其简化为可以简单表示的抽象版本,可以让我们更好地理解该概念。

我们游戏系统已经有用到抽象原则,战斗类的构造的。我们让他尽可能简单:实例化时给它两个英雄作为参数并且调用fight()方法。这就足够。

代码抽象需要遵循最少奇怪的原则。你的抽象不应该让任何有不必要和不相关的行为/属性的人感到奇怪。也就是说,需要直观。

请注意,我们的Hero#take_damage()函数不会出现意想不到的情况,比如在死后删除我们的角色。角色健康值低于零,角色必须死亡。

封装

封装就是将一些东西打包在一个对象内部,防止它于外暴露。在软件开发中,限制对内部对象和属性的访问有助于保证数据的完整性。

把内部逻辑封装到黑盒,会使类管理管理更简单,因为我们能清楚知道系统使用哪些部分需要对外调用,哪些不是。这样我们可以轻松地修改内部逻辑,而不会对其他对象造成影响破。从外部使用封装的功能也变得更简单,因为你需要的东西更少。

在大多数编程语言中,通过访问修饰符(私有,受保护等)来实现封装功能。 Python在这方面做的不是很好,它没有内置到运行时显式修饰符,但我们可以使用约定来解决这个问题。通过在变量和方法名称前添加"_"前缀表示私有的。

例如,假设我们将Fight#_run_attack方法更改为返回一个布尔值,该变量表示战斗是否结束而不是引发异常。我们知道我们可能出现异常的唯一代码是在Fight类中,因为我们将该方法设为私有函数。

请记住,经常更新代码,而不是重新编写。能够以尽可能清晰、影响最小的方式来改变代码,这才开发者需要的灵活性。

解耦

解耦是将对象分成多个单独的较小部分的操作。使其更易理解、维护和开发。

假设我们希望游戏系统的英雄要加入更多RPG功能,例如治疗和buff,库存,装备和角色属性:

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(6)

缺乏分解

假设你告诉我这将使代码变得非常混乱。英雄对象一次要做的东西太多,代码也变得非常脆弱。

例如,一个耐力值点是5生命值。如果想要改变设置使它变成6生命值,需要在很多地方做修改。

最佳的做法是将英雄对象分解成多个更小的对象,每个对象都包含一些功能。

更清晰的架构

现在,在将我们的英雄对象的功能分解为:HeroAttributes,HeroInventory,HeroEquipment和HeroBuff对象之后,添加新的功能将变得更容易,封装性和抽象性更好。而且代码也变得更清晰。

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(7)

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(8)

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(9)

有三种类型的解耦关系:

关联:定义两个组件之间的松散关系。这两个组件互相没有依赖,但可以一起工作。

实例:Hero和一个Zone对象。

聚合:定义整体与其部分之间的弱"包含一个"关系。说他们是弱关系,主要是因为这些部分可能没有整体存在。

实例:HeroInventory和Item。HeroInventory可以有许多物品,一个物品可以属于任何HeroInventory(例如交易物品)。

组成:一种强有力的"包含"关系,整体和部分不能彼此没有。部分不能共享,因为整体依赖那些确切的部分。

实例:Hero 和 HeroAttributes。

HeroAttributes就是Hero的属性,你不是其他阿猫阿狗什么的属性。

泛化

泛化可能是最重要的设计原则,它是提取共享特征并将它们合并到一起的过程。我们都知道函数类继承的概念,这都是一种泛化。

我们这样类比说明可能会清晰一点:抽象通过隐藏不必要的细节来降低复杂性,泛化则通过构造用来替换实现同样功能的多个实体来降低复杂性。

函数示例

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(10)

对象泛化示例

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(11)

在我们的游戏系统中,我们已经将我们常见的Hero和NPCclasses的功能概括为一个共同的祖先 Entity.。这都是通过继承来实现的。

在这里,我们不是让我们的NPC和Hero类实现所有方法两次并违反DRY的原则,而是通过将其通用功能移到基类中来降低复杂性。

注意:不要过度继承。许多有经验的人会推荐你使用组合。

继承经常被业余码农滥用,主要是因为它很简单,它是面向对象第一原则。

组合

组合是将多个对象组合成更复杂的对象的原则。更确切的说,它创建对象的实例并使用它们的功能而不直接继承它。

使用组合的对象可以称为复合对象。这个组合需要比其他成员的总和更简单。将多个类组合成一个类时,我们希望提高抽象级别并使对象更简单。

复合对象的API必须隐藏其内部组件以及它们之间的交互。让我们想想一个机械钟,它有三个指针来显示时间,有一个设置旋钮,但内部包含数十个移动齿轮和他们的连接件。

正如我前面提到的,组合比继承更受欢迎,这意味着你应该努力将通用功能转移到一个单独的对象中,然后使用这些对象,而不是将其存储在你继承的基类中。

让我们来说明一个可能存在的问题:过继继承:

我们只是给我们的游戏系统增加运动。

python 实现游戏(RPG游戏实例演示面向对象软件设计原则)(12)

正如我们所了解的,为了避免重复代码,我么使用泛化将move_right和move_left函数放入Entity类中。

好吧,现在如果我们想在游戏中引入坐骑怎么办?

坐骑也需要左右移动,但没有攻击能力。来想一想,他们甚至可能没有健康值!

我知道你的解决方案是:

只需将移动逻辑移动到仅具有MoveableEntity或MoveableObject功能的单独类中即可。坐骑类可以继承它。

那么,如果我们想坐骑具有健康但无法攻击,我们该怎么办?更多分裂成子类?我想这样依赖,我们系统的类层次结构会变得如何复杂?即使我们的业务逻辑仍然还非常简单。

一个更好的方法是将移动逻辑抽象为一个移动类(或一些更好的名称),并在可能需要它的类中实例化它。这将实现很好地包装功能,并使其可重用于各种不限于实体的对象。

万岁,组合!

批判性接受

尽管这些设计原则是通过几十年的实践经验形成,但在将其应用到具体项目代码之前,需要批判性地进行思考非常重要。

像所有事情一样,用的多可能也会坏事。有时候,原则可能会用的过了,你可能会对它们太过信仰,最终会导致一些实际上难以应付的问题。

作为一名工程师,主要特质是针对实际情况批判性地评估最佳方法,而不是盲目遵从和滥用一些规则。

内敛,耦合性和分离性内聚

内聚代表了模块内责任的明确性,换句话说,它的复杂性。

如果你的班级完成一项任务而没有其他任务,或者有明确的目标,那个班级的凝聚力很高。另一方面,如果它不清楚它在做什么或有多个目的,它的凝聚力低。

你希望你的课堂有很高的凝聚力。他们应该只有一个责任,如果你知道他们有很多的时候,则需要分解它。

耦合

耦合则代表互相关联的不同类的复杂性。你希望你的类与其他类尽可能少而简单地连接,这样你就可以在未来的事件中交换它们(比如改变web框架)。目标是松散耦合。

在很多编程语言中,这是通过大量使用接口来实现的,它们将处理逻辑的特定类抽象出来,做成任何类都可以插入的适配器层。

分离关注点

分离问题(SoC)是一个想法,即软件系统必须分成不重叠的部分。或者正如名称所述,关于提供问题解决方案的一般术语,必须分隔到不同的部分。

Web网页就是一个很好的例子,它有三个层(信息,表达和行为)分成三个部分(分别是HTML,CSS和JavaScript)。

实例:再来看游戏系统RPG Hero的例子,你会发现它在开始的时候有很多关注点(使用BUFF,计算攻击伤害,处理库存,装备物品,管理属性)。我们通过分解将这些问题分解为更加内聚的类,这些类抽象并封装了它们的细节。我们的Hero类现在作为一个复合对象,比以前简单得多。

劳有所获

对于这样一小段代码,应用这些原则可能看起来过于复杂。相比较计划在未来开发和维护的任何软件项目来说,这是必不可少的。编写这样的代码在开始时会有一些开销,但从长远来得会利市多倍。

这些原则确保我们的系统更加完善:

可扩展性:高内聚使实现新模块变得更加容易,无需担心无关的功能。低耦合性意味着新模块连接的对象更少,因此实施起来更容易。

可维护性:低耦合确保一个模块的更改通常不会影响其他模块。高内聚确保系统需求的变化需要修改的配置尽可能少。

可复用性:高内聚性确保模块的功能完整且定义明确。低耦合使模块更少地依赖于系统的其他部分,使其更易于在其他软件中重复使用。

总结

我们首先介绍一些基本的高级对象类型(实体,边界和控制)。

然后,我们通过一个游戏系统的实例,学习了构建对象(抽象,概括,组合,分解和封装)的关键原则。

为了跟进,我们引入了两个软件质量指标:内聚和耦合,并了解了应用所述原则的好处。

,