大班单责任


13

我有一个2500行Character类:

  • 跟踪游戏中角色的内部状态。
  • 加载并保持该状态。
  • 处理大约30个传入命令(通常=将它们转发到Game,但是一些只读命令会立即得到响应)。
  • Game正在采取的行动和他人的相关行动中收到约80个电话。

在我看来,这Character只有一个责任:管理角色的状态,在传入的命令和游戏之间进行调解。

还有其他一些职责已经分解:

  • Character具有Outgoing调用的,以为客户端应用程序生成传出更新。
  • Character有一个Timer跟踪下一次允许它做某事的时间。对此验证了传入的命令。

所以我的问题是,在SRP和类似原则下拥有如此大的课程是否可以接受?是否有任何最佳实践来减轻它的麻烦(例如,将方法拆分为单独的文件)?还是我错过了一些东西,真的有很好的方法将其拆分吗?我意识到这是非常主观的,希望获得其他人的反馈。

这是一个示例:

class Character(object):
    def __init__(self):
        self.game = None
        self.health = 1000
        self.successful_attacks = 0
        self.points = 0
        self.timer = Timer()
        self.outgoing = Outgoing(self)

    def load(self, db, id):
        self.health, self.successful_attacks, self.points = db.load_character_data(id)

    def save(self, db, id):
        db.save_character_data(self, health, self.successful_attacks, self.points)

    def handle_connect_to_game(self, game):
        self.game.connect(self)
        self.game = game
        self.outgoing.send_connect_to_game(game)

    def handle_attack(self, victim, attack_type):
        if time.time() < self.timer.get_next_move_time():
            raise Exception()
        self.game.request_attack(self, victim, attack_type)

    def on_attack(victim, attack_type, points):
        self.points += points
        self.successful_attacks += 1
        self.outgoing.send_attack(self, victim, attack_type)
        self.timer.add_attack(attacker=True)

    def on_miss_attack(victim, attack_type):
        self.missed_attacks += 1
        self.outgoing.send_missed_attack()
        self.timer.add_missed_attack()

    def on_attacked(attacker, attack_type, damage):
        self.start_defenses()
        self.take_damage(damage)
        self.outgoing.send_attack(attacker, self, attack_type)
        self.timer.add_attack(victim=True)

    def on_see_attack(attacker, victim, attack_type):
        self.outgoing.send_attack(attacker, victim, attack_type)
        self.timer.add_attack()


class Outgoing(object):
    def __init__(self, character):
        self.character = character
        self.queue = []

    def send_connect_to_game(game):
        self._queue.append(...)

    def send_attack(self, attacker, victim, attack_type):
        self._queue.append(...)

class Timer(object):
    def get_next_move_time(self):
        return self._next_move_time

    def add_attack(attacker=False, victim=False):
        if attacker:
            self.submit_move()
        self.add_time(ATTACK_TIME)
        if victim:
            self.add_time(ATTACK_VICTIM_TIME)

class Game(object):
    def connect(self, character):
        if not self._accept_character(character):
           raise Exception()
        self.character_manager.add(character)

    def request_attack(character, victim, attack_type):
        if victim.has_immunity(attack_type):
            character.on_miss_attack(victim, attack_type)
        else:
            points = self._calculate_points(character, victim, attack_type)
            damage = self._calculate_damage(character, victim, attack_type)
            character.on_attack(victim, attack_type, points)
            victim.on_attacked(character, attack_type, damage)
            for other in self.character_manager.get_observers(victim):
                other.on_see_attack(character, victim, attack_type)

1
我想这是一个错字:db.save_character_data(self, health, self.successful_attacks, self.points)您是说self.health对吗?
candied_orange

5
如果您的角色停留在正确的抽象级别,则不会出现问题。另一方面,如果它确实处​​理了诸如加载和持久化这样的所有细节,那么您就不必遵循单一的责任。在这里,授权确实是关键。看到您的角色知道一些诸如计时器之类的低级细节,我感到它已经知道太多了。
菲利普·斯图克

1
该类应在单个抽象级别上进行操作。它不应该涉及例如存储状态的细节。您应该能够分解负责内部的较小块。命令模式在这里可能很有用。另请参阅google.pl/url?sa=t&source=web&rct=j&url=http://...
彼得·Gwiazda

谢谢大家的评论和回答。我想我只是分解的东西不够多,并且一直坚持在大型模糊类中保持过多。到目前为止,使用命令模式一直是一个很大的帮助。我也一直将内容分解为可以在不同抽象级别(例如套接字,游戏消息,游戏命令)运行的层。我正在进步!
user35358

1
解决此问题的另一种方法是将“ CharacterState”作为一个类,将“ CharacterInputHandler”作为一个类,将“ CharacterPersistance”作为另一个类……
T. Sar

Answers:


14

在尝试将SRP应用于问题时,我通常会发现坚持每个班级单一职责的一个好方法是选择暗示其职责的班级名称,因为这通常有助于更清晰地思考某些功能是否真的“属于”那个班级。

此外,我觉得简单的名词,如Character(或EmployeePersonCarAnimal,等),常常使很差类的名字,因为他们真正说明实体(数据)在应用程序中,当作为类来看待它往往是太容易落得东西很肿。

我发现“好”类名往往是标签,可以有意义地传达程序行为的某些方面-即,当另一个程序员看到您的类名时,他们已经对该类的行为/功能有了基本了解。

根据经验,我倾向于将实体视为数据模型,将视为行为的代表。(尽管大多数编程语言当然都使用class关键字,但是将“普通”实体与应用程序行为分开的想法与语言无关)

考虑到您提到的角色类的各种职责的细分,我将开始倾向于其名称基于其履行的要求的类。例如:

  • 考虑一个CharacterModel没有行为的实体,仅维护您角色的状态(保存数据)。
  • 对于持久性/ IO,请考虑诸如CharacterReaderCharacterWriter (或可能是CharacterRepository/ CharacterSerialiser/ etc)的名称。
  • 考虑一下命令之间存在哪种模式;如果您有30个命令,则可能有30个单独的职责;其中一些可能会重叠,但它们似乎是分离的一个不错的选择。
  • 考虑一下您是否也可以对操作执行相同的重构-同样,80个操作可能建议多达80个单独的职责,也可能有些重叠。
  • 命令和动作的分离也可能导致另一个类,该类负责运行/触发这些命令/动作;也许某种CommandBrokerActionBroker行为类似于您的应用程序的“中间件”在不同对象之间发送/接收/执行那些命令和动作

还请记住,与行为相关的所有内容不一定都必须作为类的一部分存在;例如,您可能会考虑使用功能指针/代理/闭包的映射/字典来封装您的动作/命令,而不是编写数十个无状态单方法类。

在不编写任何使用共享签名/接口的静态方法构建的类的情况下,看到“命令模式”解决方案是相当普遍的:

 void AttackAction(CharacterModel) { ... }
 void ReloadAction(CharacterModel) { ... }
 void RunAction(CharacterModel) { ... }
 void DuckAction(CharacterModel) { ... }
 // etc.

最后,关于要实现单一责任应该走多远没有硬性规定。出于复杂性考虑,复杂性不是一件好事,但是巨石类本身往往相当复杂。SRP以及其他SOLID原则的主要目标是提供结构,一致性,并使代码更易于维护-这通常会使事情变得更简单。


我认为这个答案解决了我的问题的症结,谢谢。我一直在将其用于重构应用程序的某些部分,到目前为止,情况看起来更加清晰。
user35358

1
你必须要小心的贫血模型,这是完全可以接受的人物模型有类似的行为WalkAttackDuck。不行的是拥有SaveLoad(持久性)。SRP指出,一个类仅应承担一个责任,但角色的责任是成为角色,而不是数据容器。
克里斯·沃勒特

1
@ChrisWohlert这是名字的原因CharacterModel,其职责为数据容器,从业务逻辑层去耦数据层的关注。行为Character类也确实确实希望存在于某个地方,但是通过80个动作和30个命令,我倾向于进一步分解它。大多数时候,我发现实体名词是类名的“红鲱鱼”,因为很难从实体名词中推断出责任,而且它们很容易变成瑞士军刀。
本·科特雷尔

10

您始终可以使用更抽象的“责任”定义。这不是判断这些情况的好方法,至少直到您有丰富的经验为止。注意,您轻松地提出了四个要点,对于您的类粒度,我将其称为更好的起点。如果您真正遵循SRP,那么很难提出这样的要点。

另一种方法是查看您的类成员,然后根据实际使用它们的方法进行拆分。例如,使所有的实际使用方法一类了self.timer,其它类出所有的实际使用方法self.outgoing,和其他类出余数。从以db引用作为参数的方法中创建另一个类。当您的班级太大时,通常会有这样的分组。

不要害怕将其拆分成比您认为合理的实验小。这就是版本控制的目的。距离太远之后,更容易看到正确的平衡点。


3

众所周知,“责任”的定义含糊不清,但是如果您将其视为“改变的理由”,它的含糊性就会变小。仍然含糊不清,但是您可以进行更直接的分析。更改的原因取决于您的域和软件的使用方式,但是游戏是很好的示例,因为您可以对此做出合理的假设。在您的代码中,我在前五行中数出了五种不同的职责:

self.game = None
self.health = 1000
self.successful_attacks = 0
self.points = 0
self.timer = Timer()

如果游戏要求以下列任何一种方式发生变化,则您的实现也会发生变化:

  1. 构成“游戏”的概念发生了变化。这可能是最不可能的。
  2. 您如何衡量和跟踪健康点变化
  3. 您的攻击系统发生了变化
  4. 您的积分系统发生了变化
  5. 您的计时系统发生了变化

您正在从数据库加载,解决攻击,与游戏链接,安排时间;在我看来,职责清单已经很长了,而我们在Character班级里只看到了一小部分。因此,对您问题的一部分的答案是否定的:您的课程几乎可以肯定不遵循SRP。

但是,我会说在某些情况下,在SRP下具有2500行或更长的类是可以接受的。一些示例可能是:

  • 高度复杂但定义明确的数学计算,它接受定义明确的输入并返回定义明确的输出。这可能是需要数千行的高度优化的代码。经过验证的数学方法可用于定义明确的计算,没有太多理由需要更改。
  • 充当数据存储的类,例如仅具有yield return <N>前10,000个素数或前10,000个最常用英语单词的类。有可能的原因导致这种实现优于从数据存储或文本文件中提取的原因。这些类更改的原因很少(例如,您发现需要超过10,000个)。

2

每当您与其他实体合作时,您都可以引入第三个对象来代替处理。

def on_attack(victim, attack_type, points):
    self.points += points
    self.successful_attacks += 1
    self.outgoing.send_attack(self, victim, attack_type)
    self.timer.add_attack(attacker=True)

在这里,您可以引入“ AttackResolver”或类似的东西来处理统计信息的分配和收集。这里的on_attack仅关于角色状态吗?

您还可以重新访问状态,并问自己是否确实需要在角色上拥有某些状态。“ successful_attack”听起来也可以在其他类上进行跟踪。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.