web-dev-qa-db-ja.com

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

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)
13
user35358

問題にSRPを適用する私の試みでは、一般に、クラスごとに単一の責任を守るための良い方法は、自分の責任をほのめかすクラス名を選択することです。本当にそのクラスに属しています。

さらに、Character(またはEmployeePersonCarAnimalなど)のような単純な名詞は、しばしばアプリケーションでエンティティ(データ)を実際に記述しているため、クラス名は非常に貧弱であり、クラスとして扱うと、非常に肥大化したものになってしまうことがよくあります。

「良い」クラス名は、プログラムの動作のいくつかの側面を意味のある形で伝えるラベルになりがちです。つまり、別のプログラマーがクラスの名前を見ると、彼らはすでにそのクラスの動作/機能に関する基本的な考え方を身につけています。

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

あなたがあなたのキャラクタークラスについて述べた様々な責任の内訳を考えると、私は名前が彼らが果たす要件に基づいているクラスに傾くようになります。例えば:

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

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

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

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

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

14
Ben Cottrell

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

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

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

10
Karl Bielefeldt

「責任」の定義はあいまいなことで有名ですが、「変更する理由」と考えると少し曖昧になります。まだあいまいですが、もう少し直接分析できるものがあります。変更の理由はドメインとソフトウェアの使用方法によって異なりますが、ゲームはそれについて妥当な仮定を行うことができるため、素晴らしい例です。あなたのコードでは、最初の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行以上のクラスが許容される場合があると思います。いくつかの例は次のとおりです。

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

他のエンティティに対して作業するときはいつでも、代わりに処理を行う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」は、他のクラスでも追跡できるように聞こえます。

2
Joppe