更新-半夜寝た後、前の答えがとても良いとは本当に感じなかったので、答えをやり直しました。
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
を直接呼び出しているかどうかは少し疑わしいです。実際には、キャラクターに攻撃をある種のバトルエンジンに「送信」させたいだけですが、その部分は残しておくのが最善だと思います。読者への運動。Character
TakeDamage
さて、うまくいけば、あなたはこれがなぜであるかを理解します:
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
ダメージの計算方法を変更したい場合は、どこを見ればよいかは明らかです。
これが原理をよりよく説明するようになることを願っています。