この画像は Applying Domain-Driven Design and Patterns:With Examples in C#and .NET
これはState Patternのクラス図であり、SalesOrder
は異なるその生涯の間に状態。異なる状態間で許可されるのは、特定の遷移のみです。
これでOrderState
クラスはabstract
クラスになり、そのすべてのメソッドはそのサブクラスに継承されます。他の状態への遷移を許可しない最終状態であるサブクラスCancelled
を考慮する場合、例外をスローするには、このクラスのすべてのメソッドをoverride
する必要があります。
サブカルカスは親の動作を変更してはならないので、これはリスコフの置換原理に違反していませんか?抽象クラスをインターフェイスに変更すると、これは修正されますか?
これはどのように修正できますか?
この特定の実装です。状態を抽象インプリメンターではなく具象クラスにすると、これを回避できます。
ただし、実際にステートマシンの設計である、参照している状態パターンは、一般に、それが成長するのを見てきた方法とは私が同意しないものです。これらの状態管理パターンは、システムの他の多くの部分の現在の状態を知るための中央リポジトリになるため、これを単一責任の原則の違反として非難するのに十分な根拠があると思います。集中化されたこの状態管理の部分は、システムの多くの異なる部分に関連するビジネスルールを合理的に調整するために必要となることがよくあります。
状態を気にするシステムのすべての部分が異なるサービス、異なるマシンの異なるプロセス、これらの各場所のステータスを詳細に示す中央ステータスマネージャが、この分散システム全体を効果的にボトルネックにしていたと想像してください。ボトルネックは兆候だと思いますSRP違反および一般に悪い設計の。
対照的に、モデルがそれ自体を処理する方法を知っているMVCパターンのモデルオブジェクトのように、オブジェクトをよりインテリジェントにすることをお勧めします。内部の動作やその理由を管理するために外部のオーケストレーターは必要ありません。
このような状態パターンをオブジェクト内に配置して、それ自体を管理するだけでも、そのオブジェクトを大きくしすぎているように感じます。ワークフローは、他のオブジェクトのフローや内部のインテリジェンスのフローを管理する単一のオーケストレーションされた状態ではなく、私が言うさまざまな自己責任オブジェクトの構成を通じて実行する必要があります。
しかし、その時点では、それはエンジニアリングよりも芸術的であり、したがって、それらのいくつかへのアプローチは間違いなく主観的であり、原則は優れたガイドであり、はい、あなたがリストする実装はis LSP違反ですが、修正されない可能性があります。このようなパターンを使用する場合は、SRPに十分注意してください。安全になる可能性があります。
sublcassは親の動作を変更すべきではありませんか?
これは、LSPのよくある誤解です。サブクラスは、親の型に忠実である限り、親の動作を変更できます。
Wikipedia には長い説明があり、LSPに違反することを示唆しています。
...サブタイプが満たす必要のある行動条件は多数あります。これらは、契約方法による設計に似た用語で詳しく説明されており、契約が継承と相互作用する方法にいくつかの制限が生じます。
- サブタイプでは前提条件を強化できません。
- サブタイプでは事後条件を弱めることはできません。
- スーパータイプの不変条件は、サブタイプに保存する必要があります。
- 履歴制約(「履歴ルール」)。オブジェクトは、そのメソッド(カプセル化)を通じてのみ変更可能であると見なされます。サブタイプはスーパータイプに存在しないメソッドを導入する可能性があるため、これらのメソッドの導入により、スーパータイプでは許可されないサブタイプの状態変更が可能になる場合があります。履歴制約はこれを禁止します。それは、リスコフとウィングによって導入された新しい要素でした。この制約の違反は、MutablePointをImmutablePointのサブタイプとして定義することで例示できます。これは、ヒストリー制約の違反です。イミュータブルポイントのヒストリーでは、作成後の状態は常に同じであるため、一般にMutablePointのヒストリーを含めることはできません。ただし、サブタイプに追加されたフィールドは、スーパータイプメソッドでは監視できないため、安全に変更できます。 LSPに違反せずに、ImmutablePointからCircleWithFixedCenterButMutableRadiusを派生させることができます。
個人的には、これを覚えるだけの方が簡単だと思います。タイプAのメソッドのパラメーターを調べている場合、サブタイプBを渡す誰かが私を驚かせますか?彼らがそうするならば、LSPの違反があります。
例外を投げることは驚きですか?あんまり。 OrderStateでShipメソッドを呼び出している場合でも、GrantedまたはShippedで呼び出している場合でも、いつでも発生する可能性があります。だから私はそれを説明しなければなりません、そしてそれは実際にはLSPの違反ではありません。
とはいえ、私はこの状況を処理するより良い方法があると思います。これをC#で記述している場合は、インターフェイスを使用して、メソッドを呼び出す前にインターフェイスの実装を確認します。たとえば、現在のOrderStateがIShippableを実装していない場合は、それに対してShipメソッドを呼び出さないでください。
ただし、この特定の状況ではStateパターンを使用しません。 Stateパターンは、このようなドメインオブジェクトの状態よりもアプリケーションの状態にはるかに適しています。
つまり、簡単に言えば、これは不十分に考案された状態パターンの例であり、注文の状態を処理するための特に良い方法ではありません。しかし、それは間違いなくLSPに違反していません。そして、状態パターン自体は、確かににはありません。
(これはC#の観点から書かれているため、チェックされた例外はありません。)
LSPに関するWikipediaの記事 によると、LSPの条件の1つは次のとおりです。
それらの例外自体がスーパータイプのメソッドによってスローされる例外のサブタイプである場合を除いて、サブタイプのメソッドによって新しい例外がスローされることはありません。
それをどのように理解すべきですか?スーパータイプのメソッドが抽象的である場合、「スーパータイプのメソッドによってスローされる例外」とは正確には何ですか?それらは、スーパータイプのメソッドによってスローされる可能性がある例外としてdocumentedである例外だと思います。
これが意味することは、OrderState.Ship()
が「現在の状態でこの操作がサポートされていない場合にInvalidOperationException
をスローする」のようにドキュメント化されている場合、この設計はLSPを壊さないと思います。一方、スーパータイプメソッドがこの方法でドキュメント化されていない場合、LSPに違反します。
しかし、これはこれが優れた設計であることを意味するものではなく、通常の制御フローには例外を使用しないでください。これは非常に近いようです。また、実際のアプリケーションでは、おそらく操作を実行できるかどうかを知りたいと思いますbeforeたとえば、UIの[Ship]ボタンを無効にするなど、その操作を試みます。