web-dev-qa-db-ja.com

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

私は この質問 についてフォローしていますが、焦点をコードから原則に切り替えています。

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

これで、私の問題は次のように要約できます。抽象的なWeaponclassと、2つのクラスSwordReloadableがあります。 ReloadableReload()と呼ばれる特定のmethodが含まれている場合、そのmethodにアクセスするにはダウンキャストする必要があります。理想的には、それを避けなさい。

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

別のフォーラム では、SwordReloadを認識できるように提案された最初の回答ですが、何もしないでください。これと同じ答えが、上にリンクしたStack Overflowのページにもありました。

理由はよくわかりません。なぜ原則に違反し、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
    }
}
10
user286462

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

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

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

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

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

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

階層を作成する必要はありません。 GunSwordの2つのタイプは、まったく関係がない場合があります。一方、Gunfire()およびreload()を使用できますが、Swordstrike()のみを許可します。これらのオブジェクトを多態的に管理する必要がある場合は、アダプターパターンを使用して関連する側面をキャプチャできます。 Java 8では、これは機能的なインターフェースとラムダ/メソッド参照を使用するとかなり簡単に可能です。例えば。 _myGun::fire_または_() -> mySword.strike()_を提供するAttack戦略がある場合があります。

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

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

16
amon

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

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

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

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

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

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

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

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

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

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

3
Batavia

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

とはいえ、他の質問への回答には特に同意しません。 swordからweaponを継承するのは恐ろしいことです。素朴なOOは、常にコードにまき散らされたno-opメソッドや型チェックにつながります。

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

3
Telastyn

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

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

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

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

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

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

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

編集:戦略パターンが悪い(TM)である理由

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

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

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

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

ランダムな方法のさまざまな組み合わせを持つ何百もの武器を持っているが、OOと強力なタイピングの多くの利点を失う)と言うことは、いくつかの問題の適切な解決策になる可能性があります。ダウンキャストで何でも保存します

0
Ewan

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

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

0
gnasher729