単一の責任を持つ大規模なクラス


13

2500行のCharacterクラスがあります。

  • ゲーム内のキャラクターの内部状態を追跡します。
  • その状態をロードして永続化します。
  • 〜30個の着信コマンドを処理します(通常=に転送しますGameが、一部の読み取り専用コマンドはすぐに応答します)。
  • Game実行中のアクションと他のユーザーの関連アクションに関する〜80の呼び出しを受け取ります。

私にCharacterは、単一の責任があるように思われます。キャラクターの状態を管理し、受信コマンドとゲームを仲介することです。

既に分割されている他のいくつかの責任があります。

  • CharacterOutgoingクライアントアプリケーションの発信更新を生成するために呼び出すがあります。
  • 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://…
Piotr Gwiazda

コメントと回答をありがとう。私はただ物事を十分に分解しておらず、大きな曖昧なクラスでやりすぎにしがみついていたと思います。これまでのところ、コマンドパターンの使用は大きな助けになっています。また、さまざまな抽象化レベル(ソケット、ゲームメッセージ、ゲームコマンドなど)で動作するレイヤーに分けています。私は進歩しています!
user35358

1
これに対処する別の方法は、別として、他に「CharacterInputHandler」、「CharacterPersistance」...、クラスとして「CharacterState」を持つことである
T.サール-復活モニカ

回答:


14

SRPを問題に適用しようとする試みで、クラスごとに単一の責任に固執する良い方法は、責任をほのめかすクラス名を選択することです。そのクラスでは本当に「所属」しています。

さらに、私のような、単純な名詞を感じるCharacter(またはEmployeePersonCarAnimal、など)は、彼らが実際に記述するので、多くの場合、非常に悪いのクラス名を作成するエンティティ、アプリケーションで(データを)、およびクラスとして扱われたときに、それはあまりにも簡単になってしまうことがしばしばあります非常に肥大化したもの。

「良い」クラス名は、プログラムの振る舞いのいくつかの側面を有意に伝えるラベルである傾向があることがわかります。つまり、別のプログラマーがクラスの名前を見ると、そのクラスの振る舞い/機能に関する基本的な考えをすでに得ています。

経験則として、私はエンティティをデータモデルとして、クラスを動作の代表として考える傾向があります。(もちろん、ほとんどのプログラミング言語classは両方にキーワードを使用しますが、「プレーンな」エンティティをアプリケーションの動作から分離するという考え方は、言語に依存しません)

あなたがあなたのキャラクタークラスについて言及した様々な責任の内訳を考えると、私は名前が満たすという要件に基づいている名前を持つクラスに傾倒し始めるでしょう。例えば:

  • CharacterModel振る舞いがなく、単にキャラクターの状態を維持する(データを保持する)エンティティーを考えます。
  • persistence / IOについては、CharacterReaderand CharacterWriter (またはCharacterRepository/ CharacterSerialiser/ etc)などの名前を検討してください。
  • コマンド間にどのようなパターンが存在するかを考えてください。30個のコマンドがある場合、30個の個別の責任を負う可能性があります。いくつかは重複する可能性がありますが、分離の良い候補のようです。
  • 同じリファクタリングをアクションにも適用できるかどうかを検討してください-繰り返しになりますが、80のアクションが80の別個の責任を示唆する場合があります。
  • コマンドとアクションの分離は、それらのコマンド/アクションの実行/起動を担当する別のクラスにもつながる可能性があります。おそらく、ある種の、CommandBrokerまたはActionBrokerアプリケーションの「ミドルウェア」のように動作し、異なるオブジェクト間でコマンドやアクションを送信/受信/実行する

また、動作に関連するすべてが必ずしもクラスの一部として存在する必要があるわけではないことを忘れないでください。たとえば、多数のステートレスな単一メソッドクラスを記述するのではなく、関数ポインタ/デリゲート/クロージャのマップ/辞書を使用してアクション/コマンドをカプセル化することを検討できます。

シグネチャ/インターフェイスを共有する静的メソッドを使用して構築されたクラスを作成せずに「コマンドパターン」ソリューションを表示することは非常に一般的です。

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

最後に、単一の責任を達成するためにどこまで行かなければならないかについての厳格なルールはありません。複雑さのための複雑さは良いことではありませんが、巨石クラスはそれ自体かなり複雑になる傾向があります。SRPおよび実際の他のSOLID原則の主要な目標は、構造、一貫性を提供し、コードをより保守しやすくすることです。


この答えは私の問題の核心を解決したと思います、ありがとう。私はそれをアプリケーションのリファクタリングに使用してきましたが、今のところ物事はずっときれいに見えます。
user35358

1
あなたが注意する必要があり貧血モデルキャラクタモデルのような振る舞いを持っているため、それは完全に許容可能であるWalkAttackDuck。大丈夫ではないのはSaveLoad(永続性)を持つことです。SRPでは、クラスには1つの責任のみが必要であると述べていますが、キャラクターの責任はデータコンテナーではなくキャラクターであることです。
クリスウォーラート

1
@ChrisWohlertそれが、ビジネスロジック層からデータ層の懸念を切り離すためのデータコンテナーになるCharacterModel責任あるという名前の理由です。動作Characterクラスがどこかに存在することは確かに望ましいかもしれませんが、80のアクションと30のコマンドを使用して、さらに分類することに傾倒します。ほとんどの場合、エンティティー名詞はクラス名の「赤いニシン」であることがわかります。エンティティー名詞から責任を推測することは難しく、スイス軍の一種になりやすいからです。
ベンコットレル

10

「責任」のより抽象的な定義をいつでも使用できます。少なくとも多くの経験を積むまで、これらの状況を判断するのはあまり良い方法ではありません。4つの箇条書きを簡単に作成したことに注意してください。これをクラスの粒度のより良い開始点と呼びます。本当にSRPをフォローしている場合、そのような箇条書きを作成することは困難です。

別の方法は、クラスメンバを見て、実際にそれらを使用するメソッドに基づいて分割することです。たとえば、実際に使用するすべてのメソッドから1つのクラスを作成し、実際に使用するself.timerすべてのメソッドself.outgoingから別のクラスを作成し、残りから別のクラスを作成します。db参照を引数として取るメソッドから別のクラスを作成します。クラスが大きすぎる場合、多くの場合、このようなグループがあります。

実験として合理的だと思うよりも小さく分割することを恐れないでください。それがバージョン管理の目的です。適切なバランスポイントは、あまりにも遠くまで見た後に見やすくなります。


3

「責任」の定義は漠然と曖昧ですが、「変化する理由」と考えると、やや曖昧になります。まだあいまいですが、もう少し直接分析できるものです。変更の理由はドメインとソフトウェアの使用方法に依存しますが、ゲームはそれについて合理的な仮定を立てることができるため、良い例です。あなたのコードでは、最初の5行で5つの異なる責任をカウントしています。

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

ゲームの要件が次のいずれかの方法で変更されると、実装が変更されます。

  1. 「ゲーム」を構成するものの概念が変わります。これは最も可能性が低いかもしれません。
  2. ヘルスポイントの変化を測定および追跡する方法
  3. 攻撃システムの変更
  4. ポイントシステムの変更
  5. タイミングシステムの変更

データベースからの読み込み、攻撃の解決、ゲームとのリンク、タイミングの調整。私には、責任のリストはすでに非常に長いようで、あなたのCharacterクラスのほんの一部しか見ていません。したがって、質問の一部に対する答えは「いいえ」です。クラスはほぼ確実にSRPに従っていません。

ただし、SRPでは2,500行以上のクラスを持つことが許容される場合があります。以下に例を示します。

  • 非常に複雑だが明確に定義された数学計算で、明確に定義された入力を受け取り、明確に定義された出力を返します。これは、数千行を必要とする高度に最適化されたコードである可能性があります。明確に定義された計算のための実証済みの数学的手法には、変更する多くの理由がありません。
  • データストアとして機能するクラス。yield return <N>最初の10,000個の素数、または最も一般的な上位10,000個の英語の単語だけを持つクラスなど。この実装がデータストアまたはテキストファイルからのプルよりも優先される理由として考えられるものがあります。これらのクラスに変更する理由はほとんどありません(たとえば、10,000以上必要な場合)。

2

他のエンティティに対して作業するときはいつでも、代わりに処理を行う3番目のオブジェクトを導入できます。

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」は、他のクラスでも潜在的に追跡できるもののように聞こえます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.