13

私はただ楽しみのために、そしてSOLIDの原則についてもっと学ぶためにロールプレイングゲームを作っています。私が最初に注目していることの1つは、SRPです。ゲーム内のキャラクターを表す「キャラクター」クラスがあります。名前、体力、マナ、能力スコアなどがあります。

さて、通常は、Characterクラスにもメソッドを配置するので、次のようになります...

   public class Character
   {
      public string Name { get; set; }
      public int Health { get; set; }
      public int Mana { get; set; }
      public Dictionary<AbilityScoreEnum, int>AbilityScores { get; set; }

      // base attack bonus depends on character level, attribute bonuses, etc
      public static void GetBaseAttackBonus();  
      public static int RollDamage();
      public static TakeDamage(int amount);
   }

しかし、SRPのために、すべてのメソッドを別のクラスに移動することにしました。そのクラスに「CharacterActions」という名前を付けたところ、メソッドのシグネチャは次のようになりました...

public class CharacterActions
{
    public static void GetBaseAttackBonus(Character character);
    public static int RollDamage(Character character);
    public static TakeDamage(Character character, int amount);
}

ここで、使用しているCharacterオブジェクトをすべてのCharacterActionsメソッドに含める必要があることに注意してください。これはSRPを活用するための正しい方法ですか?これは、カプセル化というOOPの概念に完全に反しているようです。

それとも私はここで完全に間違ったことをしていますか?

これについて私が気に入っていることの1つは、私のCharacterクラスがその機能を非常に明確にしていることです。これは、単にCharacterオブジェクトを表しているだけです。

4

5 に答える 5

22

更新-半夜寝た後、前の答えがとても良いとは本当に感じなかったので、答えをやり直しました。

SRPの動作例を確認するために、非常に単純なキャラクターについて考えてみましょう。

public abstract class Character
{
    public virtual void Attack(Character target)
    {
        int damage = Random.Next(1, 20);
        target.TakeDamage(damage);
    }

    public virtual void TakeDamage(int damage)
    {
        HP -= damage;
        if (HP <= 0)
            Die();
    }

    protected virtual void Die()
    {
        // Doesn't matter what this method does right now
    }

    public int HP { get; private set; }
    public int MP { get; private set; }
    protected Random Random { get; private set; }
}

さて、これはかなり退屈なRPGになります。しかし、このクラスは理にかなっています。ここにあるものはすべて、に直接関係していCharacterます。すべてのメソッドは、によって実行されるアクション、またはに対して実行されるアクションのいずれかCharacterです。ねえ、ゲームは簡単です!

その部分に焦点を当てて、Attackこれを中途半端に面白くしてみましょう。

public abstract class Character
{
    public const int BaseHitChance = 30;

    public virtual void Attack(Character target)
    {
        int chanceToHit = Dexterity + BaseHitChance;
        int hitTest = Random.Next(100);
        if (hitTest < chanceToHit)
        {
            int damage = Strength * 2 + Weapon.DamageRating;
            target.TakeDamage(damage);
        }
    }

    public int Strength { get; private set; }
    public int Dexterity { get; private set; }
    public Weapon Weapon { get; set; }
}

今、私たちはどこかに到達しています。キャラクターがミスすることがあり、ダメージ/ヒットはレベルとともに上昇します(STRも増加すると仮定)。ジョリー良いですが、ターゲットについて何も考慮されていないため、これはまだかなり鈍いです。それを修正できるかどうか見てみましょう:

public void Attack(Character target)
{
    int chanceToHit = CalculateHitChance(target);
    int hitTest = Random.Next(100);
    if (hitTest < chanceToHit)
    {
        int damage = CalculateDamage(target);
        target.TakeDamage(damage);
    }
}

protected int CalculateHitChance(Character target)
{
    return Dexterity + BaseHitChance - target.Evade;
}

protected int CalculateDamage(Character target)
{
    return Strength * 2 + Weapon.DamageRating - target.Armor.ArmorRating -
        (target.Toughness / 2);
}

この時点で、すでに頭の中に疑問が生じているはず です。ターゲットに対する自身のダメージを計算する責任があるのはなぜですか。Characterなぜそれもその能力を持っているのですか? このクラスが何をしているのかについては無形に奇妙なことがありますが、現時点ではまだあいまいです。Characterクラスから数行のコードを移動するだけでリファクタリングする価値は本当にありますか?おそらくそうではありません。

しかし、機能を追加し始めたときに何が起こるかを見てみましょう。たとえば、典型的な1990年代のRPGからです。

protected int CalculateDamage(Character target)
{
    int baseDamage = Strength * 2 + Weapon.DamageRating;
    int armorReduction = target.Armor.ArmorRating;
    int physicalDamage = baseDamage - Math.Min(armorReduction, baseDamage);
    int pierceDamage = (int)(Weapon.PierceDamage / target.Armor.PierceResistance);
    int elementDamage = (int)(Weapon.ElementDamage /
        target.Armor.ElementResistance[Weapon.Element]);
    int netDamage = physicalDamage + pierceDamage + elementDamage;
    if (HP < (MaxHP * 0.1))
        netDamage *= DesperationMultiplier;
    if (Status.Berserk)
        netDamage *= BerserkMultiplier;
    if (Status.Weakened)
        netDamage *= WeakenedMultiplier;
    int randomDamage = Random.Next(netDamage / 2);
    return netDamage + randomDamage;
}

これはすべてうまくてダンディですが、Characterクラスでこの数の計算をすべて行うのは少しばかげていますか?そして、これはかなり短い方法です。実際のRPGでは、この方法は、セーヴィングスローやその他すべての方法で、数百行に及ぶ可能性があります。想像してみてください。あなたが新しいプログラマーを連れてきて、彼らはこう言います。通常のダメージが何であれ、2倍になるはずのデュアルヒット武器のリクエストがありました。どこで変更する必要がありますか? そして、あなたは彼に言います、クラスをチェックしてください。Character は??

さらに悪いことに、ゲームは、バックスタブボーナス、または他のタイプの環境ボーナスのような新しいしわを追加するかもしれません。さて、あなたはクラスでそれをどのように理解することになっていますCharacterか?おそらく、次のようなシングルトンを呼び出すことになります。

protected int CalculateDamage(Character target)
{
    // ...
    int backstabBonus = Environment.Current.Battle.IsFlanking(this, target);
    // ...
}

うん。これはひどいです。これのテストとデバッグは悪夢になります。どうしようか?クラスからそれを取り出しCharacterます。Characterクラスは、論理的に実行方法を知っていることだけCharacterを知っている必要があり、ターゲットに対する正確なダメージを計算することは、実際にはそれらの1つではありません。そのためのクラスを作成します。

public class DamageCalculator
{
    public DamageCalculator()
    {
        this.Battle = new DefaultBattle();
        // Better: use an IoC container to figure this out.
    }

    public DamageCalculator(Battle battle)
    {
        this.Battle = battle;
    }

    public int GetDamage(Character source, Character target)
    {
        // ...
    }

    protected Battle Battle { get; private set; }
}

ずっといい。このクラスは正確に1つのことを行います。それはそれが缶に言うことをします。シングルトン依存関係を取り除いたので、このクラスは実際にテストできるようになりました。もっと正しいと感じますね。そして今、私たちは行動Characterに集中することができます:Character

public abstract class Character
{
    public virtual void Attack(Character target)
    {
        HitTest ht = new HitTest();
        if (ht.CanHit(this, target))
        {
            DamageCalculator dc = new DamageCalculator();
            int damage = dc.GetDamage(this, target);
            target.TakeDamage(damage);
        }
    }
}

今でも、ある人が別の方法Characterを直接呼び出しているかどうかは少し疑わしいです。実際には、キャラクターに攻撃をある種のバトルエンジンに「送信」させたいだけですが、その部分は残しておくのが最善だと思います。読者への運動。CharacterTakeDamage


さて、うまくいけば、あなたはこれがなぜであるかを理解します:

public class CharacterActions
{
    public static void GetBaseAttackBonus(Character character);
    public static int RollDamage(Character character);
    public static TakeDamage(Character character, int amount);
}

...基本的に役に立たない。どうしたの?

  • 明確な目的はありません。一般的な「アクション」は単一の責任ではありません。
  • Characterそれは、それ自体ではまだできないことを何も達成できません。
  • それは完全にに依存し、他には何も依存しCharacterません。
  • Characterおそらく、本当にプライベート/保護したいクラスの部分を公開する必要があります。

クラスはカプセル化をCharacterActions破り、それ自体にほとんど何も追加しません。Character一方、このクラスは新しいカプセル化を提供し、不要な依存関係や無関係な機能をすべて排除することDamageCalculatorで、元のクラスのまとまりを回復するのに役立ちます。Characterダメージの計算方法を変更したい場合は、どこを見ればよいかは明らかです。

これが原理をよりよく説明するようになることを願っています。

于 2010-01-27T06:24:17.017 に答える
7

SRPは、クラスにメソッドが含まれていてはならないという意味ではありません。 あなたが行ったことは、ポリモーフィックオブジェクトではなくデータ構造を作成することです。そうすることには利点がありますが、この場合はおそらく意図されていないか、必要ではありません。

オブジェクトがSRPに違反しているかどうかを判断する方法の1つは、オブジェクト内のメソッドが使用するインスタンス変数を調べることです。特定のインスタンス変数を使用するが他のインスタンス変数を使用しないメソッドのグループがある場合、それは通常、オブジェクトがインスタンス変数のグループに従って分割できることを示しています。

また、メソッドを静的にしたくない場合もあります。ポリモーフィズム(メソッドが呼び出されたインスタンスのタイプに基づいてメソッド内で異なることを実行する機能)を活用することをお勧めします。

たとえば、とがあった場合、メソッドを変更する必要がありますElfCharacterWizardCharacter?メソッドが絶対に変更されず、完全に自己完結型である場合は、おそらく静的で問題ありません...しかし、それでもテストははるかに困難になります。

于 2010-01-27T06:24:34.223 に答える
2

そもそもそのタイプのクラスをSRPと呼んでいるのかわかりません。「fooを扱うすべて」は通常、SRPに準拠していないことを示しています(これは問題ありませんが、すべてのタイプの設計に適しているわけではありません)。

SRP境界を確認する良い方法は、「クラスの大部分をそのままにしておくために、クラスについて変更できることはありますか?」です。もしそうなら、それらを分離します。または、言い換えると、クラス内の1つのメソッドに触れる場合は、おそらくすべてのメソッドに触れる必要があります。SRPの利点の1つは、変更を加えたときに触れている範囲が最小限に抑えられることです。別のファイルが変更されていない場合は、バグを追加していないことがわかります。

特にキャラクタークラスは、RPGで神のクラスになる危険性が高いです。これを回避するための可能な方法は、別の方法からこれにアプローチすることです。UIから始めて、各ステップで、現在作成しているクラスの観点から、必要なインターフェイスがすでに存在していることを表明します。また、制御の反転の原則と、IoC(必ずしもIoCコンテナである必要はありません)を使用すると設計がどのように変化するかを調べます。

于 2010-01-27T07:18:16.030 に答える
2

キャラクターの振る舞いが変わるかどうかによると思います。たとえば、(RPGで発生している他の何かに基づいて)使用可能なアクションを変更したい場合は、次のようにすることができます。

public interface ICharacter
{
    //...
    IEnumerable<IAction> Actions { get; }
}

public interface IAction
{
    ICharacter Character { get; }
    void Execute();
}

public class BaseAttackBonus : IAction
{
    public BaseAttackBonus(ICharacter character)
    {
        Character = character;
    }

    public ICharacter Character { get; private set; }   

    public void Execute()
    {
        // Get base attack bonus for character...
    }
}

これにより、キャラクターは必要な数のアクションを持つことができ(つまり、キャラクタークラスを変更せずにアクションを追加/削除できます)、各アクションはそれ自体(ただしキャラクターについては知っています)と、より複雑な要件を持つアクションに対してのみ責任があります。 IActionから継承して、プロパティなどを追加します。Executeには別の戻り値が必要な場合があり、アクションのキューが必要な場合もありますが、ドリフトが発生します。

キャラクターは異なるプロパティと動作(ウォーロック、ウィザードなど)を持っている可能性があるため、キャラクターの代わりにICharacterを使用することに注意してください。ただし、おそらくすべてにアクションがあります。

アクションを分離することで、テストもはるかに簡単になります。これは、キャラクター全体を接続しなくても各アクションをテストできるようになり、ICharacterを使用すると、独自の(モック)キャラクターをより簡単に作成できるためです。

于 2010-01-27T06:31:45.800 に答える
0

クラスを設計するときに私が行っている方法であり、オブジェクト指向が基づいているのは、オブジェクトが実世界のオブジェクトをモデル化することです。

Characterを見てみましょう...Characterクラスの設計は非常に興味深い場合があります。コントラクトの可能性がありますICharacterこれは、キャラクターになりたいものはすべて、Walk()、Talk()、Attack()を実行でき、Health、Manaなどのプロパティを持っている必要があることを意味します。

次に、ウィザード、特別なプロパティを持つウィザードを作成できます。ウィザードの攻撃方法は、たとえばファイターとは異なります。

私は設計原則にあまり強制されない傾向がありますが、現実世界のオブジェクトのモデリングについても考えます。

于 2010-01-27T06:26:17.577 に答える