この一連のブログ投稿 で、Eric Lippertは、ウィザードとウォリアーを例として使用したオブジェクト指向設計の問題について説明しています。
abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player
{
public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }
次に、いくつかのルールを追加します。
次に、C#型システムを使用してこれらのルールを適用しようとすると発生する問題を実証し続けます(例:Wizard
クラスに、ウィザードがスタッフのみを使用できるようにする責任があるようにする)。 Liskov置換原則に違反したり、ランタイム例外のリスクを冒したり、拡張が困難なコードを作成したりする。
彼が思いついた解決策は、Playerクラスによって検証が行われないことです。状態の追跡にのみ使用されます。次に、プレイヤーに武器を与える代わりに:
player.Weapon = new Sword();
状態はCommand
sによって、およびRule
sに従って変更されます。
... 2つのゲーム状態オブジェクト、
Command
とWield
を取るPlayer
というWeapon
オブジェクトを作成します。ユーザーがシステムにコマンドを発行すると、「このウィザードはその剣を振るう必要があります」と、そのコマンドは一連のRule
sのコンテキストで評価され、Effect
sのシーケンスが生成されます。Rule
が1つあります。これは、プレイヤーが武器を振るうと、既存の武器があればそれがドロップされ、新しい武器がプレイヤーの武器になるという効果です。最初のルールを強化する別のルールがあります。つまり、ウィザードが剣を振るうとき、最初のルールの効果は適用されません。
私は原則としてこのアイデアが好きですが、実際にどのように使用されるかについて懸念があります。
開発者がCommands
にRule
を設定するだけでWeapon
とPlayer
sを回避できないようにするものは何もないようです。 Weapon
プロパティはWield
コマンドでアクセスできる必要があるため、作成できませんprivate set
。
それで、何が開発者がこれを行うのを妨げますか?彼らは覚えていないだけでいいですか?
一連のブログ投稿が導くすべての議論は Part Five にあります:
C#の型システムがDungeons&Dragonsのルールをエンコードするのに十分な一般性を持つように設計されていると信じる理由はないので、なぜ私たちは試みているのでしょうか?
「システムのルールを表すコードはどこにあるのか」という問題を解決しました。ゲームの状態を表すオブジェクトではなく、システムのルールを表すオブジェクトに入ります。状態オブジェクトの懸念は、状態を一定に保つことであり、ゲームのルールを評価することではありません。
武器、キャラクター、モンスター、その他のゲームオブジェクトは、何ができるか、何ができないかを確認する責任はありません。ルールシステムがその責任を負います。 Command
オブジェクトもゲームオブジェクトで何も実行していません。それは、彼らと何かをしようとする試みを表すだけです。次に、ルールシステムは、コマンドが可能かどうかをチェックし、可能であれば、ゲームオブジェクトの適切なメソッドを呼び出してコマンドを実行します。
開発者がsecondルールシステムを作成する場合、最初のルールシステムでは許可されない文字や武器を使用して、ルールシステムはそれを行うことができます。 C#では(厄介なリフレクションハックなしでは)メソッド呼び出しがどこからきたのかを見つけることができません。
mightがsomeの状況で機能する回避策は、ゲームオブジェクト(またはそれらのインターフェイス)ルールエンジンを備えた1つのアセンブリで、ミューテーターメソッドをinternal
としてマークします。ゲームオブジェクトへの読み取り専用アクセスを必要とするシステムは、別のアセンブリにあります。つまり、public
メソッドにのみアクセスできます。これでも、互いの内部メソッドを呼び出すゲームオブジェクトの抜け穴は残ります。しかし、ゲームオブジェクトクラスが無意味な状態保持者であることになっていることに同意したので、それを行うことは明らかなコードのにおいでしょう。
元のコードの明らかな問題は、Object ModelingではなくData Modelingを実行していることです。リンク先の記事には、実際のビジネス要件についての記述は一切ありません。
まず、実際の機能要件を取得することから始めます。例:「どのプレイヤーも他のプレイヤーを攻撃できます...」。ここに:
_interface Player {
void Attack(Player enemy);
}
_
「プレイヤーは攻撃に使用される武器を振るうことができ、ウィザードはスタッフを振るうことができ、ウォリアーズは剣を振るうことができる」:
_public class Wizard: Player {
...
public void Wield(Staff weapon) { ... }
...
}
public class Warrior: Player {
...
public void Wield(Sword sword) { ... }
...
}
_
「各武器は攻撃された敵にダメージを与える」。さて、今私たちは武器のための共通のインターフェースを持っている必要があります:
_interface Weapon {
void dealDamageTo(Player enemy);
}
_
など... Player
にWield()
がないのはなぜですか?プレイヤーが武器を振るうことができる必要はなかったからです。
「すべてのPlayer
がtryを使用してWeapon
を振るう」という要件があることを想像できます。ただし、これはまったく別のものです。私はそれをおそらくこのようにモデル化します:
_interface Player {
void Attack(Player enemy);
void TryWielding(Weapon weapon); // Throws UnwieldableException
}
_
概要:要件をモデル化し、要件のみをモデル化します。データモデリングは行わないでください。これは、OOモデリングではありません。
開発者がそうすることを妨げるものは何もありません。実際、エリック・リッパートはさまざまなテクニックを試しましたが、すべてに弱点がありました。それがそのシリーズの要点であり、開発者がそうするのを止めることは容易ではなく、彼が試みたすべてに不利な点がありました。最後に、ルールでCommand
オブジェクトを使用する方法を選択することにしました。
ルールを使用すると、Weapon
のWizard
プロパティをSword
に設定できますが、Wizard
に武器(剣)を装備して攻撃するように要求すると、効果がないため、状態は変化しません。彼が以下に言うように:
最初のルールを強化する別のルールがあります。つまり、ウィザードが剣を振るうとき、最初のルールの効果は適用されません。その状況の影響は、「悲しいトロンボーンの音を出し、ユーザーはこのターンのアクションを失い、ゲームの状態は変化しません。
言い換えると、彼がtype
関係を介してそのようなルールを適用することはできません。彼は多くの異なる方法を試みましたが、好きではなかったか、機能しませんでした。したがって、彼が実行できると彼が言った唯一のことは、実行時にそれについて何かをすることです。彼はそれを例外とは考えていないので、例外を投げることは良くありませんでした。
彼は最終的に上記の解決策を選択しました。このソリューションでは、基本的には任意の武器を設定できると述べていますが、それを放棄した場合、適切な武器ではないとしても、それは本質的に役に立たないでしょう。ただし、例外はスローされません。
それは良い解決策だと思います。いくつかのケースでは私もトライセットパターンで行くでしょう。
1つの方法は、Wield
コマンドをPlayer
に渡すことです。次に、プレーヤーはWield
コマンドを実行します。これにより、適切なルールがチェックされ、Weapon
が返されます。Player
は、独自の武器フィールドを設定します。このように、Weaponフィールドはプライベートセッターを持つことができ、Wield
コマンドをプレーヤーに渡すことによってのみ設定できます。
著者の最初の破棄された解決策は、型システムでルールを表すことでした。型システムはコンパイル時に評価されます。ルールを型システムから切り離すと、ルールはコンパイラーによってチェックされなくなるため、開発者がそれ自体で間違いを犯すのを防ぐものはありません。
しかし、この問題は、コンパイラーによってチェックされないすべてのロジック/モデリングが直面しており、これに対する一般的な答えは(ユニット)テストです。したがって、著者が提案するソリューションには、開発者によるエラーを回避するための強力なテストハーネスが必要です。実行時にのみ検出されるエラーに対して強力なテストハーネスが必要であるというこのポイントを強調するには、Bruce Eckelによる this の記事を参照してください。言語。
結論として、開発者が間違いを犯さないようにすることができる唯一のことは、すべてのルールが遵守されていることを確認する一連の(ユニット)テストを行うことです。
ここで微妙なところを見逃したかもしれませんが、問題は型システムにあるかどうかはわかりません。多分それはC#の慣習にあります。
たとえば、Weapon
でPlayer
セッターを保護することで、これを完全にタイプセーフにすることができます。次に、保護されたセッターを呼び出すWarrior
およびWizard
にそれぞれsetSword(Sword)
およびsetStaff(Staff)
を追加します。
そうすれば、Player
/Weapon
関係が静的にチェックされ、気にしないコードはPlayer
を使用してWeapon
を取得できます。
それで、開発者がこれを行うのを妨げているものは何ですか?彼らは覚えていないだけでいいですか?
この質問は、「 検証を配置する場所 」と呼ばれるかなり聖戦的なトピックと事実上同じです(おそらくdddにも注意してください)。
したがって、この質問に答える前に、自問する必要があります。従う必要があるルールの性質は何ですか?彼らは石に彫られて、実体を定義していますか?これらの規則に違反すると、エンティティはそれが何であるかを停止しますか?はいの場合、これらのルールを command validation に保持するとともに、それらもエンティティに配置します。したがって、開発者がコマンドの検証を忘れても、エンティティは無効な状態にはなりません。
そうでない場合、まあ、それは本質的に、このルールがコマンド固有であり、ドメインエンティティに常駐してはならないことを意味します。したがって、このルールに違反すると、許可されていないはずのアクションが発生しますが、モデルの状態は無効ではありません。