ウィザードとウォリアーのルールを回避する


9

ブログ記事のこのシリーズ、エリックリッペルトは、ウィザードとの例として、戦士を使用して、オブジェクト指向設計における問題点を説明します。

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();

状態はCommands によって、sに従って変更されRuleます。

... 2つのゲーム状態オブジェクト(a とa)を取るCommandと呼ばれるオブジェクトを作成します。ユーザーがシステムにコマンドを発行すると、「このウィザードはその剣を振るう必要があります」と、一連のsを生成するsのセットのコンテキストでそのコマンドが評価されます。私たちは、1持っているプレイヤーの試みは、武器を振るうする際、効果は既存の武器は、存在する場合、廃棄され、新たな武器がプレイヤーの武器になることであると述べています。最初のルールを強化する別のルールがあります。つまり、ウィザードが剣を振るうとき、最初のルールの効果は適用されません。WieldPlayerWeaponRuleEffectRule

私はこのアイデアを原則として気に入っていますが、実際にどのように使用されるかについて懸念があります。

何も回避からの現像を防ぐように思わないCommandsRule簡単に設定することで、秒WeaponPlayerWeaponプロパティがアクセスできる必要がありWield、それを行うことができないので、コマンドprivate set

それで、開発者がこれを行うのを妨げているもの何ですか?彼らは覚えていないだけでいいですか?


2
この質問は言語固有(C#)ではないと思います。これは、実際にはOOP設計に関する質問です。C#タグの削除を検討してください。
Maybe_Factor 2017年

1
@maybe_factor c#タグは、投稿されたコードがc#であるため問題ありません。
CodingYoshi 2017年

@EricLippertに直接尋ねてみませんか?彼は時々このサイトにここに現れるようです。
Doc Brown

@Maybe_Factor-私はC#タグを揺らしましたが、言語固有のソリューションがある場合に備えてそれを保持することにしました。
Ben L

1
@DocBrown-私は彼のブログにこの質問を投稿しました(たった数日前に認められました。私は応答をそれほど長く待っていませんでした)。私の質問をここで彼の注意を引く方法はありますか?
Ben L

回答:


9

一連のブログ投稿につながる議論全体は、パート5にあります。

C#の型システムがダンジョンズ&ドラゴンズのルールをエンコードするのに十分な一般性を持つように設計されていると信じる理由はありません。

「システムのルールを表すコードはどこにあるのか」という問題を解決しました。ゲームの状態を表すオブジェクトではなく、システムのルールを表すオブジェクトに入ります。状態オブジェクトの懸念は、状態を一定に保つことであり、ゲームのルールを評価することではありません。

武器、キャラクター、モンスター、その他のゲームオブジェクトは、何ができるか、何ができないかを確認する責任はありません。ルールシステムがその責任を負います。Commandオブジェクトは、いずれかのゲームオブジェクトと何もしていません。それは彼らと何かをしようする試みを表すだけです。次に、ルールシステムは、コマンドが可能かどうかをチェックし、可能であれば、ゲームオブジェクトの適切なメソッドを呼び出してコマンドを実行します。

開発者が、最初のルールシステムでは許可されていない文字や武器を使用して処理を行う2番目のルールシステムを作成する場合、C#では(厄介なリフレクションハックなしでは)メソッド呼び出しがどこにあるのかを見つけることができないため、これを行うことができます。から。

この問題を回避するかもしれないで働くいくつかの状況では、ルール・エンジンとアセンブリいずれかでゲームオブジェクト(またはそれらのインターフェース)を入れてのような任意のミューテータ・メソッドをマークしていますinternal。ゲームオブジェクトへの読み取り専用アクセスを必要とするシステムは、別のアセンブリにありpublicます。つまり、メソッドにしかアクセスできません。これでも、お互いの内部メソッドを呼び出すゲームオブジェクトの抜け穴は残ります。しかし、ゲームオブジェクトクラスが無意味な状態保持者であることになっていることに同意したので、それを行うことは明らかなコードのにおいでしょう。


4

元のコードの明らかな問題は、オブジェクトモデリングではなくデータモデリングを実行していることです。リンク先の記事には、実際のビジネス要件についての記述は一切ありません。

まず、実際の機能要件を取得することから始めます。例:「どのプレイヤーも他のプレイヤーを攻撃できます...」。ここに:

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);
}

など...にないのWield()はなぜPlayerですか?プレイヤーが武器を振るうことができる必要はなかったからです。

私は、「誰Playerもが何かを振るうことを試みる可能性がある」という要件があると想像できWeaponます。ただし、これはまったく別のものです。私はそれをおそらくこのようにモデル化します:

interface Player {
    void Attack(Player enemy);
    void TryWielding(Weapon weapon); // Throws UnwieldableException
}

概要:要件をモデル化し、要件のみをモデル化します。データモデリングは行わないでください。これは、OOモデリングではありません。


1
シリーズを読みましたか?そのシリーズの作者にデータではなく要件をモデル化するように伝えたいと思うかもしれません。あなたの答えにある要件は、C#コンパイラーをビルドするときに著者が持っていた要件ではなく、作成された要件です。
CodingYoshi 2017年

2
Eric Lippertがそのシリーズの技術的な問題を詳しく述べていますが、それは問題ありません。この質問は、C#の機能ではなく、実際の問題に関するものです。私の要点は、実際のプロジェクトでは、ビジネス要件(私が作成した例を挙げたものです)に従うべきであり、関係やプロパティを想定していないということです。このようにして、適合するモデルを取得します。それが問題でした。
RobertBräutigam2017年

それがそのシリーズを読んだときに最初に思ったのです。著者はいくつかの抽象化を思いついただけで、それらをさらに評価することはせず、ただそれに固執しました。問題を機械的に何度も解決しようとしています。有用なドメインと抽象化について考えるのではなく、明らかに最初にそれを行うべきです。私の賛成票。
Vadim Samokhin 2017年

これが正解です。記事は矛盾する要件を表現し(1つの要件はプレイヤーが[任意の]武器を振るうことができることを示し、他の要件はそうではないことを示しています。)、システムが競合を正しく表現することがどれほど難しいかを詳しく説明します。唯一の正解は、競合を取り除くことです。この場合、それはプレイヤーが武器を振るうことができるという要件を取り除くことを意味します。
ダニエルT.

2

1つの方法は、Wieldコマンドをに渡すことPlayerです。次に、プレーヤーはWieldコマンドを実行し、適切なルールをチェックしてを返しますWeapon。これによりPlayer、独自の武器フィールドが設定されます。このように、Weaponフィールドはプライベートセッターを持つことができWield、プレイヤーにコマンドを渡すことによってのみ設定できます。


実際には、これは問題を解決しません。コマンドオブジェクトを作成している開発者は、任意の武器を渡すことができ、プレイヤーはそれを設定します。問題はあなたが思っているよりも難しいので、シリーズを読んでください。実際、彼はRoslyn C#コンパイラの開発中にこの設計の問題に遭遇したため、このシリーズを作成しました。
CodingYoshi 2017年

2

開発者がそうすることを妨げるものは何もありません。実際、エリック・リッパートはさまざまなテクニックを試しましたが、すべてに弱点がありました。それがそのシリーズの要点であり、開発者がそうするのを止めることは容易ではなく、彼が試みたすべてに欠点がありました。最後にCommand、ルール付きのオブジェクトを使用する方法を選択することにしました。

ルールを使用すると、aのWeaponプロパティをa Wizardに設定できますが、武器(剣)を装備して攻撃Swordするように要求しWizardても、効果はなく、状態は変化しません。彼が以下に言うように:

最初のルールを強化する別のルールがあります。つまり、ウィザードが剣を振るうとき、最初のルールの効果は適用されません。その状況の影響は、「悲しいトロンボーンサウンドを作り、ユーザーはこのターンのアクションを失います。ゲームの状態は変化しません。

言い換えれば、type彼が多くの異なる方法を試みたが、それを好きではなかった、または機能しなかった関係によって、そのようなルールを強制することはできません。したがって、彼が実行できると彼が言った唯一のことは、実行時にそれについて何かをすることです。彼はそれを例外とは考えていないので、例外を投げることは良くありませんでした。

彼は最終的に上記の解決策を選択しました。このソリューションでは、基本的に任意の武器を設定できると述べていますが、それを放棄した場合、適切な武器ではないとしても、それは本質的に役に立たないでしょう。ただし、例外はスローされません。

それは良い解決策だと思います。いくつかのケースでは私もトライセットパターンで行きます。


This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.私はそのシリーズでそれを見つけることができませんでした。このソリューションが提案されている場所を教えていただけますか?
Vadim Samokhin 2017年

@zapadlo彼はそれを間接的に言っています。私は私の回答にその部分をコピーして引用しました。ここで再びです。引用で彼は言う:魔法使いが剣を振るうとき。剣がセットされていない場合、ウィザードはどのように剣を振るうことができますか?設定されている必要があります。次に、ウィザードが剣を振るうと、その状況の影響は、「悲しいトロンボーンの音を出すと、ユーザーはこのターンのアクションを失います
CodingYoshi

うーん、剣を振るうということは、基本的にはセットする必要があるということですよね?その段落を読んでいると、最初のルールの効果はであると解釈しthat the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weaponます。一方、that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.武器が剣かどうかをチェックするルールがあると思いますので、ウィザードで振り回せないので設定されていません。代わりに悲しいトロンボーンが鳴ります。
Vadim Samokhin 2017年

私の考えでは、いくつかのコマンドルールに違反するのはおかしいでしょう。それは問題をぼかすようなものです。ルールに違反した後でこの問題を処理し、ウィザードを無効な状態に設定するのはなぜですか?ウィザードは剣を持つことはできませんが、それは持っています!それを実現させてみませんか?
Vadim Samokhin 2017年

Wieldここでの解釈方法については、@ Zapadloに同意します。コマンドを少し誤解を招く名前だと思います。のようなものChangeWeaponがより正確になります。武器を設定できる別のモデルを使用することもできますが、それを放棄した場合、適切な武器ではないとしても、それは本質的に役に立たないでしょう。それは面白そうに聞こえますが、私はそれがエリック・リッパートが述べていることだとは思いません。
Ben L

2

著者の最初の破棄された解決策は、型システムでルールを表現することでした。型システムはコンパイル時に評価されます。ルールを型システムから切り離すと、ルールはコンパイラーによってチェックされなくなるため、開発者自身が間違いを犯さないようにするものはありません。

しかし、この問題は、コンパイラーによってチェックされないすべてのロジック/モデリングが直面しており、これに対する一般的な答えは(ユニット)テストです。したがって、著者の提案するソリューションには、開発者によるエラーを回避するための強力なテストハーネスが必要です。実行時にのみ検出されるエラーに対して強力なテストハーネスが必要であるというこのポイントを強調するには、動的言語での強力なテストのために強力な型付けを交換する必要があると主張するBruce Eckelによるこの記事を参照してください。

結論として、開発者が間違いを犯さないようにすることができる唯一のことは、すべてのルールが遵守されていることをチェックする一連の(ユニット)テストを行うことです。


あなたはAPIを使用することになっていて、APIはあなたがユニットテストを行うかその仮定をすることを保証することになっていますか?全体の課題はモデリングに関するものであるため、モデルを使用する開発者が不注意であってもモデルが壊れることはありません。
CodingYoshi 2017年

1
私がしようとしていた点は、開発者が間違いを犯すのを妨げるもの何もないということでした。提案されたソリューションでは、ルールがデータから切り離されているため、独自のチェックを作成しない場合でも、ルールを適用せずにデータオブジェクトを使用することを妨げるものはありません。
larsbe 2017年

1

ここで微妙なところを見逃したかもしれませんが、問題が型システムにあるかどうかはわかりません。多分それはC#の慣習にあります。

たとえば、Weaponセッターをで保護することにより、これを完全にタイプセーフにすることができますPlayer。次に、保護されたセッターを呼び出すsetSword(Sword)andとsetStaff(Staff)to WarriorWizardそれぞれ追加します。

このようにして、Player/ Weapon関係は静的にチェックされ、気にしないコードはを使用してPlayerを取得できますWeapon


Eric Lippertは例外を投げたくありませんでした。シリーズを読みましたか?ソリューションは要件を満たす必要があり、この要件はシリーズで明確に述べられています。
CodingYoshi 2017年

@CodingYoshiなぜこれが例外をスローするのでしょうか?タイプセーフです。つまり、コンパイル時にチェックできます。
Alex

申し訳ありませんが、あなたが例外を投げていないことに気付いたら、私のコメントを変更できませんでした。しかし、あなたはそうすることによって継承を壊しました。作成者が解決しようとしていた問題は、型を多態的に処理できないため、メソッドを追加しただけでは解決できないという問題を参照してください。
CodingYoshi 2017年

@CodingYoshi多態的な要件は、プレイヤーが武器を持っていることです。そしてこのスキームでは、プレイヤーは実際に武器を持っています。継承は破壊されません。このソリューションは、ルールが正しい場合にのみコンパイルされます。
Alex

あなたが追加しようとすると、実行時のチェックなどを必要とする書き込みコードできないわけではありません@CodingYoshiさて、WeaponPlayer。しかし、コンパイル時にそれらの具象型に作用できるコンパイル時に具象型を知らない型システムはありません。定義により。このスキームは、実行時に処理する必要があるのはそのケースのみであることを意味します。そのため、実際にはエリックのどのスキームよりも優れています。
アレックス

0

それで、開発者がこれを行うのを妨げているものは何ですか?彼らは覚えていないだけでいいですか?

この質問は、「検証をどこに置くか」と呼ばれるかなり聖戦的なトピックと事実上同じです(おそらくdddにも注意してください)。

したがって、この質問に答える前に、自問する必要があります。従う必要があるルールの性質は何ですか?彼らは石に彫られて、実体を定義していますか?これらの規則に違反すると、エンティティはそれが何であるかを停止しますか?はいの場合は、これらのルールをコマンドの検証で維持するとともに、それらもエンティティに配置します。したがって、開発者がコマンドの検証を忘れた場合、エンティティは無効な状態にはなりません。

そうでない場合、まあ、それは本質的に、このルールがコマンド固有であり、ドメインエンティティに存在すべきではないことを意味します。そのため、このルールに違反すると、許可されていないはずのアクションが発生しますが、モデルの状態は無効ではありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.