LSPに違反しても大丈夫ですか?


10

私はこの質問についてフォローアップしていますが、コードから原則に焦点を移しています。

Liskov置換原理(LSP)についての私の理解から、私の基本クラスにあるメソッドは何でも、それらは私のサブクラスに実装する必要があります。このページによれば、基本クラスのメソッドをオーバーライドし、何も実行しないか、例外、あなたは原則に違反しています。

さて、私の問題は次のようにまとめることができます:私は抽象Weapon classと2つのクラス、Swordとを持っていReloadableます。と呼ばれるReloadable特定のが含まれている場合、それにアクセスするにはダウンキャストする必要があります。理想的には、それを回避する必要があります。methodReload()method

次に、を使用することを考えましたStrategy Pattern。このように、各武器は実行可能なアクションのみを認識していたため、たとえば、Reloadable武器は明らかにリロードSwordできますが、リロードすることはできませんReload class/method。Stack Overflowの投稿で述べたように、ダウンキャストする必要はなく、List<Weapon>コレクションを維持できます。

別のフォーラム、最初の答えができるようにする提案Swordを認識するReloadだけで何もしません、。これと同じ答えが、上でリンクしたスタックオーバーフローのページにもありました。

理由はよくわかりません。なぜ原則に違反し、Swordにを認識さReloadせ、空白のままにするのですか?Stack Overflowの投稿で述べたように、SPで問題がほぼ解決しました。

なぜそれが実行可能な解決策ではないのですか?

public final Weapon{

    private final String name;
    private final int damage;
    private final List<AttackStrategy> validactions;
    private final List<Actions> standardActions;

    private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
    {
        this.name = name;
        this.damage = damage;
        standardActions = new ArrayList<Actions>(standardActions);
        validAttacks = new ArrayList<AttackStrategy>(validActions);
    }

    public void standardAction(String action){} // -- Can call reload or aim here.  

    public int attack(String action){} // - Call any actions that are attacks. 

    public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
        return new Weapon(name, damage,standardActions, attacks) ;
    }

}

攻撃インターフェースと実装:

public interface AttackStrategy{
    void attack(Enemy enemy);
}

public class Shoot implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to shoot
    }
}

public class Strike implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to strike
    }
}

2
できますclass Weapon { bool supportsReload(); void reload(); }。クライアントは、リロードする前にサポートされているかどうかをテストします。reloadiffをスローするように契約で定義されてい!supportsReload()ます。これは、駆動されたクラスが先ほど説明したプロトコルに準拠していれば、LSPに準拠しています。
usr

3
あなたは残しているかどうかreload()、空白かどうかstandardActionsリロードアクションが含まれていないことは、単に別のメカニズムです。基本的な違いはありません。両方を行うことができます。=>あなたの解決策実行可能です(これはあなたの質問でした)。Weaponに空白のデフォルト実装が含まれている場合、Swordはリロードについて知る必要はありません。
usr

27
この問題を解決するためのさまざまな手法を使用して、さまざまな問題を調査する一連の記事を書きました。結論:言語の型システムでゲームのルールを捉えようとしないでくださいタイプシステムのレベルではなく、ゲームロジックのレベルでルールを表し、適用するオブジェクトでゲームのルールをキャプチャします。使用している型システムがゲームロジックを表現するのに十分に洗練されていると信じる理由はありません。ericlippert.com/2015/04/27/wizards-and-warriors-part-one
Eric Lippert

2
@EricLippert-あなたのリンクをありがとう。私は何度もこのブログに出くわしましたが、いくつかのポイントがよくわかりませんが、それはあなたの責任ではありません。私は自分でOOPを学んでいて、SOLIDのプリンシパルに出会いました。初めてあなたのブログに出会ったときは、まったく理解できませんでしたが、もう少し学び、もう一度ブログを読んで、ゆっくりと話されている部分の一部を理解し始めました。ある日、私はそのシリーズのすべてを完全に理解します。:D

6
@SR「それが何もしないか、例外をスローする場合、違反です」-その記事のメッセージを誤って読んだと思います。問題は、setAltitudeが直接何もしないことではなく、「設定された高度で鳥が描かれる」という事後条件を満たさないことでした。「リロード」の事後条件を「十分な弾薬が利用可能であった場合、武器は再び攻撃できる」と定義した場合、何もしないことは、弾薬を使用しない武器の完全に有効な実装です。
Sebastian Redl 2017年

回答:


16

LSPはサブタイピングと多態性を懸念しています。すべてのコードが実際にこれらの機能を使用するわけではありません。その場合、LSPは無関係です。サブタイピングのケースではない継承言語構造​​の2つの一般的な使用例は次のとおりです。

  • 継承は、基本クラスの実装を継承するために使用されていましたが、そのインターフェースは継承していませんでした。ほとんどすべての場合、構成が優先されます。Javaなどの言語では、実装とインターフェースの継承を分離できませんが、C ++にはprivate継承があります。

  • 合計タイプ/ユニオンをモデル化するために使用される継承。たとえば、a BaseCaseAまたはCaseBです。基本タイプは、関連するインターフェースを宣言しません。そのインスタンスを使用するには、それらを正しい具象タイプにキャストする必要があります。キャストは安全に行うことができ、問題ではありません。残念ながら、多くのOOP言語では、基本クラスのサブタイプを目的のサブタイプのみに制限することができません。外部コードがを作成できる場合CaseC、a Baseはa のみであるCaseAか、またはCaseB正しくないことを想定したコード。Scalaはそのcase classコンセプトでこれを安全に行うことができます。Javaでは、これがBaseプライベートコンストラクターを持つ抽象クラスで、ネストされた静的クラスがベースから継承する場合にモデル化できます。

実世界のオブジェクトの概念階層のようないくつかの概念は、オブジェクト指向モデルに非常にうまくマッピングされません。「銃は武器であり、剣は武器なので、Weapon基本クラスを用意GunしてSword継承する」のような考えは誤解を招きます。実際の単語のis-aの関係は、モデルではそのような関係を意味するものではありません。関連する問題の1つは、オブジェクトが複数の概念階層に属しているか、実行時に階層の所属を変更する可能性があることです。継承は通常、オブジェクトごとではなくクラスごとであり、実行時ではなく設計時に定義されるため、ほとんどの言語ではモデル化できません。

OOPモデルを設計するときは、階層や、あるクラスが別のクラスを「拡張」する方法について考えるべきではありません。基本クラスは、複数のクラスの共通部分を除外する場所ではありません。代わりに、オブジェクトがどのように使用されるか、つまり、これらのオブジェクトのユーザーがどのような動作をする必要があるかを考えます。

ここでは、ユーザーはattack()武器を必要とするかもしれませんreload()。タイプ階層を作成する場合、これらのメソッドは両方ともベースタイプである必要がありますが、リロードできない武器はそのメソッドを無視し、呼び出されても何もしない場合があります。したがって、基本クラスには共通部分は含まれていませんが、すべてのサブクラスの結合されたインターフェースが含まれています。サブクラスのインターフェースは異なりませんが、このインターフェースの実装のみが異なります。

階層を作成する必要はありません。二つのタイプGunとはSword全く関係のないかもしれません。一方、Gunfire()とだけかもしれません。これらのオブジェクトを多態的に管理する必要がある場合は、アダプタパターンを使用して関連する側面をキャプチャできます。Java 8では、これは機能的なインターフェースとラムダ/メソッド参照を使用するとかなり便利に実現できます。たとえば、やを提供する戦略があるとします。reload()Swordstrike()AttackmyGun::fire() -> mySword.strike()

最後に、サブクラスをまったく避けて、すべてのオブジェクトを単一の型でモデル化することが賢明な場合があります。多くのゲームオブジェクトはどの階層にもうまく適合せず、さまざまな機能を備えている可能性があるため、これはゲームに特に関連しています。たとえば、ロールプレイングゲームには、クエストアイテム、装備時にステータスを+2の強さで強化するアイテム、20%の確率で受けたダメージを無視し、近接攻撃を与えるアイテムがある場合があります。それとも*魔法*なので、リロード可能な剣かもしれません。誰が物語が必要とするかを知っています。

その混乱のクラス階層を理解しようとするのではなく、さまざまな機能にスロットを提供するクラスを用意することをお勧めします。これらのスロットは実行時に変更できます。各スロットは、OnDamageReceivedまたはのような戦略/コールバックAttackです。あなたの武器で、我々は持っていることMeleeAttackRangedAttackおよびReloadスロット。これらのスロットは空である場合があります。その場合、オブジェクトはこの機能を提供しません。次に、スロットは条件付きで呼び出されますif (item.attack != null) item.attack.perform()


ある意味SPのようなものです。なぜスロットを空にする必要があるのですか?辞書にアクションが含まれていない場合は、何もしないでください

@SRスロットが空であるか存在しないかは実際には重要ではなく、これらのスロットの実装に使用されるメカニズムに依存します。私は、スロットがインスタンスフィールドであり、常に存在するかなり静的な言語(つまり、Javaの通常のクラス設計)を想定してこの回答を書きました。スロットがディクショナリのエントリであるより動的なモデルを選択する場合(JavaでのHashMapや通常のPythonオブジェクトの使用など)、スロットは存在する必要はありません。より動的なアプローチは、多くのタイプセーフを放棄することに注意してください。これは通常は望ましくありません。
amon

現実世界のオブジェクトはうまくモデル化しないことに同意します。私があなたの投稿を理解した場合、あなたは私が戦略パターンを使用できると言っていますか?

2
@SRはい、何らかの形での戦略パターンは、おそらく賢明なアプローチです。関連するタイプオブジェクトパターンも比較してください:gameprogrammingpatterns.com/type-object.html
amon

3

のための戦略を持つことはattackあなたのニーズに十分ではないので。確かに、アイテムが実行できるアクションを抽象化できますが、武器の射程を知る必要がある場合はどうなりますか?または弾薬容量?それともどのような弾薬が必要ですか?あなたはそれを得るためにダウンキャストに戻っています。また、そのレベルの柔軟性があると、UIの実装が少し難しくなります。これは、すべての機能を処理するために同様の戦略パターンが必要になるためです。

とはいえ、他の質問への回答には特に同意しません。有するsword継承するweapon恐ろしい、常にノー・オペレーションの方法またはコードについて散らばっ型チェックを導くナイーブOOです。

しかし、問題の根源には、どちらの解決策も間違っていませ。両方のソリューションを使用して、楽しくて機能するゲームを作成できます。どのソリューションにも同じように、それぞれに独自のトレードオフのセットが付属しています。


これは完璧だと思います。私はSPを使用できますが、それらはトレードオフであり、それらに注意する必要があります。私が考えていることについては、私の編集を参照してください。

1
Fwiw:剣には無限の弾薬があります。永遠に読まなくても使い続けることができます。最初は無限に使用できるため、reloadは何もしません。1 /近接の範囲:近接武器です。近接と遠隔の両方で機能する方法ですべての統計/アクションについて考えることは不可能ではありません。それでも、年をとるにつれて、インターフェース、競争、そしてWeapon剣と銃のインスタンスを持つ単一のクラスを使用するための名前が何であれ、継承を少しずつ使用しています。
97年

Destiny 2の剣のFwiwは何らかの理由で弾薬を使用します!

@ CAD97-これは、私がこの問題に関して見たタイプの考え方です。無限の弾薬を持つ剣を持っているので、リロードはありません。これは単に問題を回避したり、隠したりするだけです。手榴弾を導入するとどうなりますか?手榴弾は弾薬や射撃を持っていないので、そのような方法を知らないはずです。

1
私はこれについてCAD97を使っています。そしてWeaponBuilder、戦略の武器を構成することによって、剣と銃を構築できるを作成します。
Chris Wohlert、2017

3

もちろん、それは実行可能な解決策です。それは非常に悪い考えです。

問題は、基本クラスにリロードを配置するこの単一のインスタンスがある場合ではありません。問題は、「スイング」、「シュート」、「パリー」、「ノック」、「ポリッシュ」、「逆アセンブル」、「研ぎ」、「クラブの先のとがった端の釘の交換」も置く必要があることです。基本クラスのメソッド。

LSPのポイントは、トップレベルのアルゴリズムが機能し、意味をなす必要があるということです。したがって、次のようなコードがある場合:

if (isEquipped(weapon)) {
   reload();
}

実装されていない例外がスローされてプログラムがクラッシュする場合は、非常に悪い考えです。

コードが次のようになっている場合、

if (canReload(weapon)) {
   reload();
}
else if (canSharpen(weapon)) {
  sharpen();
}
else if (canPollish(weapon)) {
  polish();
}

その場合、コードは、抽象的な「武器」のアイデアとは何の関係もない非常に特定のプロパティで乱雑になる可能性があります。

ただし、一人称シューティングゲームを実装していて、1つのナイフを除いてすべての武器がシュート/リロードできる場合は、(特定のコンテキストでは)ナイフのリロードで何もしないことは非常に理にかなっています。基本クラスが特定のプロパティで雑然とすることはほとんどありません。

更新: 抽象的なケース/用語について考えてみてください。たとえば、すべての武器に、銃のリロードと剣の鞘なしの「準備」アクションがあるかもしれません。


武器のアクションを保持する内部武器ディクショナリがあるとします。ユーザーが「リロード」を渡すと、ディクショナリをチェックします。たとえば、weaponActions.containsKey(action)がそうであれば、それに関連付けられているオブジェクトを取得して実行します。それ。複数のifステートメントを含む武器クラスではなく

上記の編集を参照してください。これは、SPを使用するときに心に留めていたものです

0

基本クラスのインスタンスを代用する目的でサブクラスを作成せず、機能の便利なリポジトリとして基本クラスを使用してサブクラスを作成する場合は、明らかにOKです。

これが良いアイデアかどうかは非常に議論の余地がありますが、ベースクラスをサブクラスに置き換えない場合は、それが機能しないことは問題ありません。問題があるかもしれませんが、LSPはこの場合問題ではありません。


0

LSPは、呼び出し元のコードがクラスの動作を気にする必要がないため、優れています。

例えば。私は、BattleMechにマウントされているすべての武器でWeapon.Attack()を呼び出すことができ、それらの一部が例外をスローしてゲームがクラッシュすることを心配する必要はありません。

ここで、あなたはあなたのベースタイプを新しい機能で拡張したいと思います。Gun()クラスはその弾薬を追跡し、不足すると発砲を停止できるため、Attack()は問題ありません。しかし、Reload()は新しいものであり、武器であることの一部ではありません。

簡単な解決策は、ダウンキャストすることです。パフォーマンスを過度に心配する必要はないと思います。フレームごとに行う必要はありません。

あるいは、アーキテクチャを再評価して、抽象的にはすべての武器がリロード可能であり、一部の武器はリロードする必要がないことを考慮することができます。

その後、銃のクラスを拡張したり、LSPに違反したりすることはありません。

しかし、Gun.SafteyOn()、Sword.WipeOffBlood()などの特別なケースを考えなければならないため、長期的には問題があり、それらすべてをWeaponに配置すると、非常に複雑な一般化基本クラスが維持されます変更する必要があります。

編集:戦略パターンがBad(tm)である理由

そうではありませんが、セットアップ、パフォーマンス、および全体的なコードを考慮してください。

銃をリロードできることを示す設定をどこかに持っている必要があります。武器をインスタンス化するときは、その構成を読み取ってすべてのメソッドを動的に追加し、重複する名前がないことを確認する必要があります。

メソッドを呼び出すときは、そのアクションのリストをループ処理し、文字列の照合を実行して、呼び出すものを確認する必要があります。

コードをコンパイルして、「attack」の代わりにWeapon.Do( "atack")を呼び出すと、コンパイル時にエラーが発生しません。

これは、ランダムな方法のさまざまな組み合わせを備えた数百の武器を持っているが、オブジェクト指向と強力な型付けの多くの利点を失うなど、いくつかの問題の適切な解決策になる可能性があります。それは実際にダウンキャストに何も保存しません


私はSPは(上記の編集を参照)、銃を持っているであろうと、すべてを扱うことができると思いますSafteyOn()し、Sword必要がありますwipeOffBlood()。各武器は他の方法を認識していません(そして、それらは認識してはいけません)

SPは問題ありませんが、タイプセーフなしのダウンキャストと同等です。私はちょっと別の質問に答えていたと思います、更新させてください
Ewan

2
戦略パターン自体は、リストまたはディクショナリ内の戦略の動的検索を意味しません。weapon.do("attack")つまり、両方とタイプセーフweapon.attack.perform()が戦略パターンの例である可能性があります。名前でストラテジーを検索する必要があるのは、configファイルからオブジェクトを構成する場合のみですが、リフレクションを使用してもタイプセーフです。
amon

あなたには、いくつかのユーザー入力にバインドする必要がある2つの別々のアクションが攻撃し、リロードされているように、このような状況ではその習慣仕事、
ユアン・
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.