2500行のCharacter
クラスがあります。
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)
問題にSRPを適用する私の試みでは、一般に、クラスごとに単一の責任を守るための良い方法は、自分の責任をほのめかすクラス名を選択することです。本当にそのクラスに属しています。
さらに、Character
(またはEmployee
、Person
、Car
、Animal
など)のような単純な名詞は、しばしばアプリケーションでエンティティ(データ)を実際に記述しているため、クラス名は非常に貧弱であり、クラスとして扱うと、非常に肥大化したものになってしまうことがよくあります。
「良い」クラス名は、プログラムの動作のいくつかの側面を意味のある形で伝えるラベルになりがちです。つまり、別のプログラマーがクラスの名前を見ると、彼らはすでにそのクラスの動作/機能に関する基本的な考え方を身につけています。
経験則として、私はEntitiesをデータモデルとして、Classesを行動の代表として考える傾向があります。 (もちろん、ほとんどのプログラミング言語は両方にclass
キーワードを使用しますが、「プレーンな」エンティティをアプリケーションの動作から分離するという考えは言語に依存しません)
あなたがあなたのキャラクタークラスについて述べた様々な責任の内訳を考えると、私は名前が彼らが果たす要件に基づいているクラスに傾くようになります。例えば:
CharacterModel
エンティティを考えてみます。CharacterReader
やCharacterWriter
(またはCharacterRepository
/CharacterSerialiser
/etc)などの名前を検討してください。CommandBroker
またはActionBroker
は、アプリケーションの「ミドルウェア」が、異なるオブジェクト間でこれらのコマンドとアクションを送信/受信/実行するように動作しますまた、動作に関連するすべてが必ずしもクラスの一部として存在する必要があるわけではないことも覚えておいてください。たとえば、数十個のステートレスな単一メソッドクラスを作成するのではなく、関数ポインタ/デリゲート/クロージャのマップ/辞書を使用してアクション/コマンドをカプセル化することを検討できます。
シグネチャ/インターフェイスを共有する静的メソッドを使用して構築されたクラスを作成せずに「コマンドパターン」ソリューションを確認することはかなり一般的です。
void AttackAction(CharacterModel) { ... }
void ReloadAction(CharacterModel) { ... }
void RunAction(CharacterModel) { ... }
void DuckAction(CharacterModel) { ... }
// etc.
最後に、単一の責任を達成するためにどこまで行けばよいかについて、厳格なルールはありません。複雑さのための複雑さは良いことではありませんが、巨石クラスはそれ自体がかなり複雑になる傾向があります。 SRPとその他のSOLID=原則の主要な目標は、構造、一貫性を提供し、コードをより保守しやすくすることです。これにより、多くの場合、より単純な結果になります。
「責任」のより抽象的な定義はいつでも使用できます。これは、少なくともあなたが多くの経験を積むまでは、これらの状況を判断するにはあまり良い方法ではありません。 4つの箇条書きを簡単に作成したことに注意してください。これをクラスの粒度の良い出発点と呼びます。あなたが本当にSRPをフォローしているなら、そのような箇条書きをすることは困難です。
別の方法は、クラスメンバーを調べ、実際にそれらを使用するメソッドに基づいて分割することです。たとえば、実際にself.timer
を使用するすべてのメソッドから1つのクラスを作成し、実際にself.outgoing
を使用するすべてのメソッドから別のクラスを作成し、残りのクラスから別のクラスを作成します。引数としてdb参照を取るメソッドから別のクラスを作成します。クラスが大きすぎる場合、このようなグループがよくあります。
実験として妥当だと思うよりも小さく分割することを恐れないでください。それがバージョン管理の目的です。適切なバランスポイントは、遠くに置くとはるかに見やすくなります。
「責任」の定義はあいまいなことで有名ですが、「変更する理由」と考えると少し曖昧になります。まだあいまいですが、もう少し直接分析できるものがあります。変更の理由はドメインとソフトウェアの使用方法によって異なりますが、ゲームはそれについて妥当な仮定を行うことができるため、素晴らしい例です。あなたのコードでは、最初の5行で5つの異なる責任をカウントします。
self.game = None
self.health = 1000
self.successful_attacks = 0
self.points = 0
self.timer = Timer()
ゲームの要件が次のいずれかの方法で変化すると、実装が変化します。
データベースからの読み込み、攻撃の解決、ゲームとのリンク、タイミングの調整を行っています。責任のリストはすでに非常に長く、私はあなたのCharacter
クラスのほんの一部しか見ていないようです。したがって、質問の一部に対する答えは「いいえ」です。クラスはほぼ確実にSRPに準拠していません。
ただし、SRPでは2,500行以上のクラスが許容される場合があると思います。いくつかの例は次のとおりです。
yield return <N>
を含むクラスや、最も一般的な英語の上位10,000個の単語など、データストアとして機能するクラス。この実装がデータストアまたはテキストファイルからのプルよりも優先される理由はいくつか考えられます。これらのクラスには、変更する理由がほとんどありません(たとえば、10,000を超える数が必要であることがわかります)。他のエンティティに対して作業するときはいつでも、代わりに処理を行う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」は、他のクラスでも追跡できるように聞こえます。