web-dev-qa-db-ja.com

フォールバックを伴う特殊なケースは、リスコフ代替原則に違反しますか?

次のシグネチャを持つインターフェイスFooInterfaceがあるとします。

_interface FooInterface {
    public function doSomething(SomethingInterface something);
}
_

そして、そのインターフェースを実装する具象クラスConcreteFoo

_class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}
_

特殊な型のSomethingInterfaceオブジェクト(SpecialSomethingと呼ばれているとしましょう)が渡された場合は、ConcreteFoo::doSomething()で何かユニークなことをしたいと思います。

メソッドの前提条件を強化したり、新しい例外をスローしたりする場合、これは間違いなくLSP違反ですが、ジェネリックSpecialSomethingオブジェクトにフォールバックを提供しながら、SomethingInterfaceオブジェクトを特殊ケース化した場合でも、LSP違反になりますか?何かのようなもの:

_class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}
_
20
Evan

mightは、doSomethingメソッドのコントラクトに他に何があるかによって、LSPの違反になります。しかし、LSPに違反していなくても、ほぼ間違いなくコードのにおいです。

たとえば、doSomethingのコントラクトの一部が、戻る前に少なくとも1回はsomething.commitUpdates()を呼び出すことであり、特別な場合は代わりにcommitSpecialUpdates()を呼び出すことです。そのis LSPの違反。 SpecialSomethingcommitSpecialUpdates()メソッドがcommitUpdates()と同じことをすべて意識的に行うように設計されていたとしても、LSP違反を予防的にハッキングするだけであり、まさにハッカーは、LSPに一貫して従っていれば、する必要はありません。このようなことがあなたのケースに当てはまるかどうかは、そのメソッドの契約(明示的または暗黙的)を確認することによって理解する必要があります。

これがコードのにおいである理由は、引数の1つの具体的な型をチェックすることは、そもそもそのインターフェイス/抽象型を定義するポイントを逃すためであり、原則として、メソッドが機能することさえ保証できないためです(想像してください)誰かがSpecialSomethingのサブクラスを書き込んだ場合、commitUpdates()が呼び出されることを前提としています)。まず、これらの特別な更新が既存のSomethingInterface内で機能するようにします。それが最良の結果です。それができないと本当に確信している場合は、インターフェースを更新する必要があります。インターフェースを制御しない場合は、必要なことを行う独自のインターフェースを作成することを検討する必要があります。それらすべてで機能するインターフェースを思い付くことができない場合は、インターフェースを完全に廃棄し、さまざまな具体的な型をとる複数のメソッドを用意するか、さらに大きなリファクターを使用する必要があります。これらのうちどれが適切であるかを通知するには、コメントアウトした魔法についてもっと知る必要があります。

19
Ixrec

いいえ、与えられた引数はインターフェイスAだけでなくA2も提供するという事実を利用して、LSPに違反しません。

特別なパスに強い前提条件(それを取ることを決定する際にテストされたものを除いて)も、弱い事後条件もないことを確認してください。

C++テンプレートは、たとえばInputIteratorsを要求するなどのより良いパフォーマンスを提供するためにそうしますが、RandomAccessIteratorsで呼び出された場合は追加の保証を提供します。

代わりに実行時に決定する必要がある場合(たとえば、動的キャストを使用する場合)は、すべての潜在的利益またはそれ以上を消費するための経路を決定することに注意してください。

特殊なケースを利用することは、コードを複製する必要がある場合があるため、DRY(自分を繰り返さないでください)に反することが多く、KISS(シンプル)、より複雑なので。

1
Deduplicator

LSPの違反ではありません。ただし、それでも「型チェックを行わない」ルールに違反します。スペシャルが自然に発生するようにコードを設計する方が良いでしょう。多分SomethingInterfaceはこれを達成できる別のメンバーを必要とするか、多分どこかに抽象ファクトリを注入する必要があります。

ただし、これは難しくて速い規則ではないので、トレードオフに価値があるかどうかを判断する必要があります。現在、コードのにおいがあり、将来の機能拡張の障害となる可能性があります。それを取り除くことは、かなり複雑なアーキテクチャを意味するかもしれません。より多くの情報がなければ、どちらが優れているかわかりません。

1
Sebastian Redl

「型チェックを行わない」の原則と「インターフェースを分離する」の原則の間にはトレードオフがあります。多くのクラスがいくつかのタスクを実行するための実行可能だが非効率的な手段を提供し、それらのいくつかはより良い手段を提供することができ、タスクを実行できるアイテムのより広いカテゴリのいずれかを受け入れることができるコードが必要である場合(おそらく非効率的)しかし、可能な限り効率的にタスクを実行するには、すべてのオブジェクトに、より効率的なメソッドがサポートされているかどうかを示すメンバーを含むインターフェースを実装するか、サポートされている場合はそれを使用する別のオブジェクトを実装する必要があります。オブジェクトを受け取るコードで、拡張インターフェースをサポートしているかどうかを確認し、サポートしている場合はキャストします。

個人的には、私は前者のアプローチを支持しますが、.NETのようなオブジェクト指向のフレームワークでインターフェイスがデフォルトのメソッドを指定できるようにしたいと思います(大きなインターフェイスを操作するのに苦痛が少なくなります)。共通インターフェースにオプションのメソッドが含まれている場合、単一のラッパークラスは、元のラップされたオブジェクトに存在する機能のみを消費者に約束しながら、機能のさまざまな組み合わせを持つオブジェクトを処理できます。多くの関数が異なるインターフェイスに分割されている場合、ラップされたオブジェクトがサポートする必要のあるインターフェイスのさまざまな組み合わせごとに、異なるラッパーオブジェクトが必要になります。

0
supercat

Liskov置換原則は、スーパータイプの契約に従って動作するサブタイプに関するものです。したがって、Ixrecが書いたように、それがLSP違反であるかどうかを答えるのに十分な情報がありません。

ここで違反しているのは、オープンクローズの原則です。新たな要件-SpecialSomethingの魔法-があり、既存のコードを変更する必要がある場合、あなたは 間違いなくOCPに違反しています です。

0
Zapadlo