通过实施角色扮演游戏的课程来演示
目录
介绍
对象类型
1.实体对象
2.控制对象
3.边界对象
奖励:值对象
关键设计原则
抽象化
封装
分解
泛化
组成
批判性思维免责声明
关注的凝聚,耦合和分离
凝聚
耦合
关注点分离
结束语
概要
进一步阅读
大多数现代编程语言都支持并鼓励面向对象编程(OOP)。虽然最近我们似乎看到了一点点偏离,因为人们开始使用不受OOP 严重影响的语言(例如Go,Rust,Elixir,Elm,Scala),大多数仍然有对象。我们将在此概述的设计原则也适用于非OOP语言。
为了成功编写清晰,高质量,可维护和可扩展的代码,您需要了解经过数十年经验证明自己有效的设计原则。
披露:我们将要进行的示例将使用Python。这方面的例子证明了一点,并且可能以其他明显的方式草率。
由于我们将围绕对象建模代码,因此区分它们的不同职责和变体会很有用。
有三种类型的对象:
该对象通常对应于问题空间中的一些现实世界实体。假设我们正在构建角色扮演游戏(RPG),实体对象将是我们的简单Hero
类:
class Hero:
def __init__(self, health, mana):
self._health = health
self._mana = mana
def attack(self) -> int:
"""
Returns the attack damage of the Hero
"""
return 1
def take_damage(self, damage: int):
self._health -= damage
def is_alive(self):
return self._health > 0
这些对象通常包含关于它们自身的属性(例如health
或mana
),并且可以通过某些规则进行修改。
控制对象(有时也称为Manager对象)负责协调其他对象。这些是控制 和使用其他对象的对象。我们的RPG类比中的一个很好的例子就是这个Fight
类,它控制着两个英雄并使它们战斗。
class Fight:
class FightOver(Exception):
def __init__(self, winner, *args, **kwargs):
self.winner = winner
super(*args, **kwargs)
def __init__(self, hero_a: Hero, hero_b: Hero):
self._hero_a = hero_a
self._hero_b = hero_b
self.fight_ongoing = True
self.winner = None
def fight(self):
while self.fight_ongoing:
self._run_round()
print(f'The fight has ended! Winner is #{self.winner}')
def _run_round(self):
try:
self._run_attack(self._hero_a, self._hero_b)
self._run_attack(self._hero_b, self._hero_a)
except self.FightOver as e:
self._finish_round(e.winner)
def _run_attack(self, attacker: Hero, victim: Hero):
damage = attacker.attack()
victim.take_damage(damage)
if not victim.is_alive():
raise self.FightOver(winner=attacker)
def _finish_round(self, winner: Hero):
self.winner = winner
self.fight_ongoing = False
在这样的类中封装战斗的逻辑可以为您提供多种好处:其中之一是操作的简单可扩展性。只要它暴露相同的API,您就可以非常轻松地传入非玩家角色(NPC)类型以供英雄战斗。您还可以非常轻松地继承该类并覆盖某些功能以满足您的需求。
这些是位于系统边界的对象。从另一个系统获取输入或产生输出的任何对象 - 无论该系统是用户,互联网还是数据库 - 都可以归类为边界对象。
class UserInput:
def __init__(self, input_parser):
self.input_parser = input_parser
def take_command(self):
"""
Takes the user's input, parses it into a recognizable command and returns it
"""
command = self._parse_input(self._take_input())
return command
def _parse_input(self, input):
return self.input_parser.parse(input)
def _take_input(self):
raise NotImplementedError()
class UserMouseInput(UserInput):
pass
class UserKeyboardInput(UserInput):
pass
class UserJoystickInput(UserInput):
pass
这些边界对象负责将信息转换为我们的系统。在我们采用用户命令的示例中,我们需要边界对象将键盘输入(如空格键)转换为可识别的域事件(例如字符跳转)。
值对象表示域中的简单值。它们是不变的,没有身份。
如果我们将它们融入到我们的游戏中,那么一个Money
或一个Damage
类将非常适合。所述对象让我们可以轻松地区分,查找和调试相关功能,而使用原始类型(一个整数数组或一个整数)的天真方法则不然。
class Money:
def __init__(self, gold, silver, copper):
self.gold = gold
self.silver = silver
self.copper = copper
def __eq__(self, other):
return self.gold == other.gold and self.silver == other.silver and self.copper == other.copper
def __gt__(self, other):
if self.gold == other.gold and self.silver == other.silver:
return self.copper > other.copper
if self.gold == other.gold:
return self.silver > other.silver
return self.gold > other.gold
def __add__(self, other):
return Money(gold=self.gold + other.gold, silver=self.silver + other.silver, copper=self.copper + other.copper)
def __str__(self):
return f'Money Object(Gold: {self.gold}; Silver: {self.silver}; Copper: {self.copper})'
def __repr__(self):
return self.__str__()
print(Money(1, 1, 1) == Money(1, 1, 1))
# => True
print(Money(1, 1, 1) > Money(1, 2, 1))
# => False
print(Money(1, 1, 0) + Money(1, 1, 1))
# => Money Object(Gold: 2; Silver: 2; Copper: 1)
它们可以归类为Entity
对象的子类别。
设计原则是软件设计中的规则,多年来已被证明是有价值的。严格遵循它们将帮助您确保您的软件具有一流的质量。
抽象是在某些情况下将概念简化为其基本要素的想法。它允许您通过将其简化为简化版本来更好地理解概念。
上面的例子说明了抽象 - 看看Fight
类的结构。你使用它的方式尽可能简单 - 你在实例化中给它两个英雄作为参数并调用fight()
方法。没有更多,没有更少。
代码中的抽象应该遵循最少的惊喜规则。你的抽象不应该让任何有不必要和不相关的行为/属性的人感到惊讶。换句话说 - 它应该是直观的。
请注意,我们的Hero#take_damage()
功能不会出现意外情况,例如在死亡时删除我们的角色。但如果他的健康状况低于零,我们可以预期它会杀死我们的角色。
封装可以被认为是将一些东西放在胶囊内 - 你限制它暴露在外面的世界。在软件中,限制对内部对象和属性的访问有助于数据完整性。
封装黑盒内部逻辑,使您的类更容易管理,因为您知道其他系统使用哪个部分,哪些不是。这意味着您可以轻松地修改内部逻辑,同时保留公共部分并确保您没有破坏任何内容。作为一种副作用,使用外部封装的功能变得更简单,因为您需要考虑的事情较少。
在大多数语言中,这是通过所谓的访问修饰符(私有,受保护等)来完成的。Python不是最好的例子,因为它缺少运行时内置的显式修饰符,但我们使用约定来解决这个问题。_
变量/方法的前缀表示它们是私有的。
例如,假设我们改变我们的Fight#_run_attack
方法以返回一个布尔变量,该变量指示战斗是否结束而不是引发异常。我们将知道我们可能已经破坏的唯一代码是在Fight
类中,因为我们将该方法设为私有。
请记住,代码更改频繁而不是重新编写。能够以尽可能明显的影响更改代码是您希望作为开发人员的灵活性。
分解是将对象分成多个单独的较小部分的动作。所述部件更易于理解,维护和编程。
想象一下,我们希望在我们的基础上加入更多RPG功能,如增益,库存,设备和角色属性Hero
:
class Hero:
def __init__(self, health, mana):
self._health = health
self._mana = mana
self._strength = 0
self._agility = 0
self._stamina = 0
self.level = 0
self._items = {}
self._equipment = {}
self._item_capacity = 30
self.stamina_buff = None
self.agility_buff = None
self.strength_buff = None
self.buff_duration = -1
def level_up(self):
self.level += 1
self._stamina += 1
self._agility += 1
self._strength += 1
self._health += 5
def take_buff(self, stamina_increase, strength_increase, agility_increase):
self.stamina_buff = stamina_increase
self.agility_buff = agility_increase
self.strength_buff = strength_increase
self._stamina += stamina_increase
self._strength += strength_increase
self._agility += agility_increase
self.buff_duration = 10 # rounds
def pass_round(self):
if self.buff_duration > 0:
self.buff_duration -= 1
if self.buff_duration == 0: # Remove buff
self._stamina -= self.stamina_buff
self._strength -= self.strength_buff
self._agility -= self.agility_buff
self._health -= self.stamina_buff * 5
self.buff_duration = -1
self.stamina_buff = None
self.agility_buff = None
self.strength_buff = None
def attack(self) -> int:
"""
Returns the attack damage of the Hero
"""
return 1 + (self._agility * 0.2) + (self._strength * 0.2)
def take_damage(self, damage: int):
self._health -= damage
def is_alive(self):
return self._health > 0
def take_item(self, item: Item):
if self._item_capacity == 0:
raise Exception('No more free slots')
self._items[item.id] = item
self._item_capacity -= 1
def equip_item(self, item: Item):
if item.id not in self._items:
raise Exception('Item is not present in inventory!')
self._equipment[item.slot] = item
self._agility += item.agility
self._stamina += item.stamina
self._strength += item.strength
self._health += item.stamina * 5
缺乏分解
我假设你可以告诉这段代码变得非常混乱。我们的Hero
目标是同时做太多的东西,这个代码变得非常脆弱。
例如,一个耐力点值5健康。如果我们希望将来改变它以使其值得健康,我们需要在多个地方更改实施。
答案是将Hero
对象分解为多个较小的对象,每个对象都包含一些功能。
更清洁的架构
from copy import deepcopy
class AttributeCalculator:
@staticmethod
def stamina_to_health(self, stamina):
return stamina * 6
@staticmethod
def agility_to_damage(self, agility):
return agility * 0.2
@staticmethod
def strength_to_damage(self, strength):
return strength * 0.2
class HeroInventory:
class FullInventoryException(Exception):
pass
def __init__(self, capacity):
self._equipment = {}
self._item_capacity = capacity
def store_item(self, item: Item):
if self._item_capacity < 0:
raise self.FullInventoryException()
self._equipment[item.id] = item
self._item_capacity -= 1
def has_item(self, item):
return item.id in self._equipment
class HeroAttributes:
def __init__(self, health, mana):
self.health = health
self.mana = mana
self.stamina = 0
self.strength = 0
self.agility = 0
self.damage = 1
def increase(self, stamina=0, agility=0, strength=0):
self.stamina += stamina
self.health += AttributeCalculator.stamina_to_health(stamina)
self.damage += AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility)
self.agility += agility
self.strength += strength
def decrease(self, stamina=0, agility=0, strength=0):
self.stamina -= stamina
self.health -= AttributeCalculator.stamina_to_health(stamina)
self.damage -= AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility)
self.agility -= agility
self.strength -= strength
class HeroEquipment:
def __init__(self, hero_attributes: HeroAttributes):
self.hero_attributes = hero_attributes
self._equipment = {}
def equip_item(self, item):
self._equipment[item.slot] = item
self.hero_attributes.increase(stamina=item.stamina, strength=item.strength, agility=item.agility)
class HeroBuff:
class Expired(Exception):
pass
def __init__(self, stamina, strength, agility, round_duration):
self.attributes = None
self.stamina = stamina
self.strength = strength
self.agility = agility
self.duration = round_duration
def with_attributes(self, hero_attributes: HeroAttributes):
buff = deepcopy(self)
buff.attributes = hero_attributes
return buff
def apply(self):
if self.attributes is None:
raise Exception()
self.attributes.increase(stamina=self.stamina, strength=self.strength, agility=self.agility)
def deapply(self):
self.attributes.decrease(stamina=self.stamina, strength=self.strength, agility=self.agility)
def pass_round(self):
self.duration -= 0
if self.has_expired():
self.deapply()
raise self.Expired()
def has_expired(self):
return self.duration == 0
class Hero:
def __init__(self, health, mana):
self.attributes = HeroAttributes(health, mana)
self.level = 0
self.inventory = HeroInventory(capacity=30)
self.equipment = HeroEquipment(self.attributes)
self.buff = None
def level_up(self):
self.level += 1
self.attributes.increase(1, 1, 1)
def attack(self) -> int:
"""
Returns the attack damage of the Hero
"""
return self.attributes.damage
def take_damage(self, damage: int):
self.attributes.health -= damage
def take_buff(self, buff: HeroBuff):
self.buff = buff.with_attributes(self.attributes)
self.buff.apply()
def pass_round(self):
if self.buff:
try:
self.buff.pass_round()
except HeroBuff.Expired:
self.buff = None
def is_alive(self):
return self.attributes.health > 0
def take_item(self, item: Item):
self.inventory.store_item(item)
def equip_item(self, item: Item):
if not self.inventory.has_item(item):
raise Exception('Item is not present in inventory!')
self.equipment.equip_item(item)
现在,分解我们的英雄对象的功能集成到后HeroAttributes
,HeroInventory
,HeroEquipment
和HeroBuff
对象,添加未来的功能会更容易,更封装和更好的抽象。您可以告诉我们的代码更清晰,更清晰。
有三种类型的分解关系:
示例: Hero
和一个Zone
对象。
示例: HeroInventory
和Item
。
A HeroInventory
可以有许多Items
,并且Item
可以属于任何HeroInventory
(例如交易项目)。
示例: Hero
和HeroAttributes
。
这些是英雄的属性 - 你不能改变他们的主人。
泛化可能是最重要的设计原则 - 它是提取共享特征并将它们组合在一个地方的过程。我们所有人都知道函数和类继承的概念 - 两者都是一种泛化。
比较可能会让事情变得清晰:虽然抽象通过隐藏不必要的细节来降低复杂性,但泛化通过用单个构造替换执行类似功能的多个实体来降低复杂性。
# Two methods which share common characteristics
def take_physical_damage(self, physical_damage):
print(f'Took {physical_damage} physical damage')
self._health -= physical_damage
def take_spell_damage(self, spell_damage):
print(f'Took {spell_damage} spell damage')
self._health -= spell_damage
# vs.
# One generalized method
def take_damage(self, damage, is_physical=True):
damage_type = 'physical' if is_physical else 'spell'
print(f'Took {damage} {damage_type} damage')
self._health -= damage
功能实例
class Entity:
def __init__(self):
raise Exception('Should not be initialized directly!')
def attack(self) -> int:
"""
Returns the attack damage of the Hero
"""
return self.attributes.damage
def take_damage(self, damage: int):
self.attributes.health -= damage
def is_alive(self):
return self.attributes.health > 0
class Hero(Entity):
pass
class NPC(Entity):
pass
广义对象示例
在给定的示例中,我们将公共Hero
和NPC
类的功能概括为一个名为的共同祖先Entity
。这总是通过继承来实现的。
在这里,而不是我们NPC
和Hero
类实现所有的方法两次,违反了DRY原则,我们通过移动他们共同的功能集成到一个基类降低了复杂性。
作为预警 - 不要过度继承。许多有经验的人建议你喜欢组合而不是继承。
业余程序员经常滥用继承,可能是因为它是由于其简单性而首先掌握的OOP技术之一。
组合是将多个对象组合成更复杂的对象的原则。实际上说 - 它创建对象的实例并使用它们的功能而不是直接继承它。
使用合成的对象可以称为复合对象。重要的是,这种复合比其对等的总和更简单。当将多个类组合成一个时,我们希望提高抽象级别并使对象更简单。
复合对象的API必须隐藏其内部组件以及它们之间的交互。想想一个机械时钟,它有三只手显示时间和一个旋钮进行设置 - 但内部包含许多移动和相互依赖的部分。
正如我所说,组合比继承更受欢迎,这意味着你应该努力将常用功能移动到一个单独的对象中,然后使用这些类 - 而不是将它存放在你继承的基类中。
让我们举例说明过度继承功能可能存在的问题:
我们刚刚为游戏添加了动作。
class Entity:
def __init__(self, x, y):
self.x = x
self.y = y
raise Exception('Should not be initialized directly!')
def attack(self) -> int:
"""
Returns the attack damage of the Hero
"""
return self.attributes.damage
def take_damage(self, damage: int):
self.attributes.health -= damage
def is_alive(self):
return self.attributes.health > 0
def move_left(self):
self.x -= 1
def move_right(self):
self.x += 1
class Hero(Entity):
pass
class NPC(Entity):
pass
正如我们所知,不是复制代码,而是使用泛化将函数move_right
和move_left
函数放入Entity
类中。
好的,现在如果我们想在游戏中引入坐骑怎么办?
一个很好的坐骑:)
坐骑也需要左右移动,但没有攻击能力。想一想 - 他们甚至可能没有健康!
我知道你的解决方案是什么:
只需将move
逻辑移动到仅具有该功能的单独MoveableEntity
或MoveableObject
类中。然后Mount
该类可以继承它。
那么,如果我们想要有健康却无法攻击的坐骑,我们该怎么办?更多分裂为子类?我希望你能看到我们的类层次结构如何开始变得复杂,即使我们的业务逻辑仍然非常简单。
一种更好的方法是将移动逻辑抽象为Movement
类(或更好的名称),并在可能需要它的类中实例化它。这将很好地打包功能,并使其可以在不限于的各种对象中重复使用Entity
。
万岁,作文!
尽管这些设计原则是通过数十年的经验形成的,但在盲目地将原则应用于代码之前,能够批判性思考仍然非常重要。
像所有事情一样,太多可能是一件坏事。有时原则可能会走得太远,你可能会对它们过于聪明,并最终得到一些实际上更难处理的东西。
作为一名工程师,您的主要特点是批判性地评估针对您的独特情况的最佳方法,而不是盲目地遵循和应用任意规则。
凝聚力代表了模块内部责任的清晰度,换句话说 - 复杂性。
如果你的班级执行一项任务而没有其他任务,或者有明确的目的 - 该班级具有很高的凝聚力。另一方面,如果它在做什么或者有多个目的有些不清楚 - 它具有低凝聚力。
你希望你的课程具有很高的凝聚力。他们应该只有一个责任,如果你抓住他们有更多 - 可能是分开它的时候了。
耦合捕获了连接不同类之间的复杂性。您希望您的类与其他类具有尽可能少的简单连接,以便您可以在将来的事件中交换它们(例如更改Web框架)。目标是松耦合。
在许多语言中,这是通过大量使用接口来实现的 - 它们抽象出处理逻辑的特定类,并表示任何类可以插入其中的适配器层。
关注点分离(SoC)是指软件系统必须分成不与功能重叠的部分。或者正如名称所说的那样 - 关注 - 关于能够解决问题的任何事物的一般术语 - 必须分成不同的地方。
网页就是一个很好的例子 - 它有三个层(信息,演示和行为)分为三个地方(分别是 HTML,CSS和JavaScript)。
如果你再看一下RPG的Hero
例子,你会发现它在一开始就有很多顾虑(应用buff,计算攻击伤害,处理库存,装备物品,管理属性)。我们通过分解将这些问题分解为更具凝聚力的类,这些类抽象并封装了它们的细节。我们的Hero
类现在充当复合对象,比以前简单得多。
对于如此小的代码,应用这些原则可能看起来过于复杂。事实上,您计划在未来开发和维护的任何软件项目都是必须的。编写这样的代码在开始时会有一些开销,但从长远来看会多次付出代价。
这些原则确保我们的系统更多:
我们首先介绍了一些基本的高级对象类型(实体,边界和控制)。
然后,我们学习了构造所述对象的关键原则(抽象,泛化,组合,分解和封装)。
为了跟进,我们引入了两个软件质量指标(耦合和内聚),并了解了应用所述原则的好处。
我希望本文对一些设计原则提供了有用的概述。如果您希望进一步了解这方面的知识,我建议您使用以下资源。
设计模式:可重复使用的面向对象软件的元素 - 可以说是该领域最具影响力的书籍。在其示例(C ++ 98)中有点过时,但模式和想法仍然非常相关。
以测试为指导的不断发展的面向对象软件 - 一本伟大的书,通过一个项目来展示如何实际应用本文中概述的原则(以及更多)。
有效的软件设计 - 一个包含远远超过设计见解的顶级博客。
软件设计和架构专业化 - 一系列4个视频课程,在整个项目中为您提供有效的设计,涵盖所有四门课程。
如果这个概述对您有所帮助,请考虑给予它您认为应得的拍手数量,以便更多人可以偶然发现并从中获取价值。
原文:https://medium.freecodecamp.org/a-short-overview-of-object-oriented-software-design-c7aa0a622c83