【Python】面向对象软件设计简述(OOP)

通过实施角色扮演游戏的课程来演示

目录

介绍

对象类型

1.实体对象

2.控制对象

3.边界对象

奖励:值对象

关键设计原则

抽象化

封装

分解

泛化

组成

批判性思维免责声明

关注的凝聚,耦合和分离

凝聚

耦合

关注点分离

结束语

概要

进一步阅读


介绍

大多数现代编程语言都支持并鼓励面向对象编程(OOP)。虽然最近我们似乎看到了一点点偏离,因为人们开始使用不受OOP 严重影响的语言(例如Go,Rust,Elixir,Elm,Scala),大多数仍然有对象。我们将在此概述的设计原则也适用于非OOP语言。

为了成功编写清晰,高质量,可维护和可扩展的代码,您需要了解经过数十年经验证明自己有效的设计原则。

披露:我们将要进行的示例将使用Python。这方面的例子证明了一点,并且可能以其他明显的方式草率。

 

对象类型

由于我们将围绕对象建模代码,因此区分它们的不同职责和变体会很有用。

有三种类型的对象:

1.实体对象

该对象通常对应于问题空间中的一些现实世界实体。假设我们正在构建角色扮演游戏(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

这些对象通常包含关于它们自身的属性(例如healthmana),并且可以通过某些规则进行修改。

2.控制对象

控制对象(有时也称为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)类型以供英雄战斗。您还可以非常轻松地继承该类并覆盖某些功能以满足您的需求。

3.边界对象

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

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)

现在,分解我们的英雄对象的功能集成到后HeroAttributesHeroInventoryHeroEquipmentHeroBuff对象,添加未来的功能会更容易,更封装和更好的抽象。您可以告诉我们的代码更清晰,更清晰。

有三种类型的分解关系:

  • association - 定义两个组件之间的松散关系。两个组件不相互依赖,但可以一起工作。

示例: Hero和一个Zone对象。

  • 聚合  - 定义整体及其各部分之间的弱“has-a”关系。认为很弱,因为零件可以存在而不是整体。

示例: HeroInventoryItem。 
HeroInventory可以有许多Items,并且Item可以属于任何HeroInventory(例如交易项目)。

  • 构成  - 一种强烈的“有一种”关系,其中整体和部分不能彼此存在。部件不能共享,因为整体取决于那些确切的部分。

示例: HeroHeroAttributes。 
这些是英雄的属性 - 你不能改变他们的主人。

泛化

泛化可能是最重要的设计原则 - 它是提取共享特征并将它们组合在一个地方的过程。我们所有人都知道函数和类继承的概念 - 两者都是一种泛化。

比较可能会让事情变得清晰:虽然抽象通过隐藏不必要的细节来降低复杂性,但泛化通过用单个构造替换执行类似功能的多个实体来降低复杂性。

# 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

广义对象示例

在给定的示例中,我们将公共HeroNPC 类的功能概括为一个名为的共同祖先Entity。这总是通过继承来实现的。

在这里,而不是我们NPCHero类实现所有的方法两次,违反了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_rightmove_left函数放入Entity类中。

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

一个很好的坐骑:)

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

我知道你的解决方案是什么:

只需将move逻辑移动到仅具有该功能的单独MoveableEntityMoveableObject类中。然后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

猜你喜欢

转载自blog.csdn.net/ChenVast/article/details/82734538