状態パターンは、リスコフ代替原理に違反していますか?


17

この画像は、ドメイン駆動のデザインとパターンの適用:C#および.NETの例を使用したものです。

ここに画像の説明を入力してください

これは、a がその存続期間中に異なる状態を持つことができる状態パターンのクラス図ですSalesOrder。異なる状態間では特定の遷移のみが許可されます。

これでOrderStateクラスはabstractクラスになり、そのすべてのメソッドはそのサブクラスに継承されます。Cancelled他の状態への遷移を許可しない最終状態であるサブクラスを考慮する場合override、このクラスのすべてのメソッドで例外をスローする必要があります。

サブクラスは親の振る舞いを変えてはいけないので、今ではそれはリスコフの代用原則に違反していないのですか?抽象クラスをインターフェイスに変更すると、これが修正されますか?
これはどのように修正できますか?


OrderStateを、デフォルトですべてのメソッドで「NotSupported」例外をスローする具象クラスに変更しますか?
ジェームズ

私はこの本を読んでいませんが、OrderStateにCancel()が含まれており、その後に異なるサブタイプであるCanceled状態があるのは奇妙に思えます。
陶酔

@Euphoricなぜそれが変なのですか?を呼び出すとcancel()、状態がキャンセルされます。
松o

どうやって?SalesOrderの状態のインスタンスが変更されることになっているときに、OrderStateでCancelを呼び出すにはどうすればよいですか。モデル全体が間違っていると思います。私は完全な実装を見たいです。
陶酔

回答:


3

この特定の実装、はい。状態を抽象実装者ではなく具象クラスにすると、これから逃れられます。

ただし、実際にステートマシンの設計である状態パターンを参照すると、一般的に、これまで見た方法とは異なります。これらの状態管理パターンは、システムの他の多くの部分の現在の状態を知るための中央リポジトリであるため、これを単一責任原則の違反として非難する十分な根拠があると思います。集中化されているこの状態管理部分は、多くの場合、システムのさまざまな部分に関連するビジネスルールを合理的に調整する必要があります。

状態を気にするシステムのすべての部分が異なるサービス、異なるマシン上の異なるプロセス、それらの各場所のステータスを詳細に示す中央ステータスマネージャーがこの分散システム全体を効果的にボトルネックにしていると想像してください。ボトルネックはサインだと思いますSRP違反のほか、一般的にデザインが悪い。

対照的に、MVCパターンのModelオブジェクトのようにオブジェクトをよりインテリジェントにすることをお勧めします。MVCパターンでは、モデルはそれ自体の処理方法を知っており、内部の動作やその理由を管理するために外部オーケストレーターを必要としません。

このような状態パターンをオブジェクトの内部に配置して、それ自体を管理するだけでも、そのオブジェクトを大きくしすぎているように感じます。ワークフローは、他のオブジェクトのフロー、または内部のインテリジェンスのフローを管理する単一の組織化された状態ではなく、さまざまな自己責任オブジェクトの構成を通じて行われるべきです。

しかし、その時点では、それはエンジニアリングよりも芸術であるため、それらのいくつかに対するあなたのアプローチは間違いなく主観的です。原則は良いガイドであり、はい、あなたがリストする実装 LSP違反ですが、修正されない可能性があると述べました。この性質のパターンを使用するときは、SRPに非常に注意してください。安全である可能性があります。


基本的に、大きな問題は、オブジェクトの方向は新しいクラスを追加するのには適していますが、新しいメソッドを追加するのが難しくなることです。そして、状態の場合、あなたが言ったように、新しい状態でコードを非常に頻繁に拡張する必要はないでしょう。
hugomg

3
@Jimmy Hoffa申し訳ありませんが、「抽象実装者ではなく状態を具体的なクラスにすると、これから逃れることができます」とはどういう意味ですか。?
松o

@Jimmy:各コンポーネントに独自の状態のみを認識させることで、状態パターンなしで状態を実装しようとしました。その結果、実装と保守が大幅に複雑になり、フローに大きな変更が加えられました。そうは言っても、状態設計パターンではなく、状態マシン(理想的には他の誰かのライブラリを使用してブラックボックスとして扱うこと)を使用すると思います(したがって、状態は完全なクラスではなく列挙型内の要素にすぎず、遷移は単なるバカです)エッジ)は、状態パターンの利点の多くを提供する一方で、開発者による試みを悪用しながら悪用します。
ブライアン

8

sublcassは親の振る舞いを変えてはいけませんか?

これはLSPの一般的な誤解です。サブクラスは、親タイプに忠実である限り、親の動作を変更できます。

Wikipediaには、LSPに違反することを示唆する長い説明があります。

...サブタイプが満たさなければならない多くの行動条件があります。これらは、契約ごとの方法論に似た用語で詳しく説明されており、契約が継承と相互作用する方法にいくつかの制限があります。

  • サブタイプでは前提条件を強化できません。
  • サブタイプでは事後条件を弱めることはできません。
  • スーパータイプの不変式はサブタイプに保存する必要があります。
  • 履歴制約(「履歴ルール」)。オブジェクトは、そのメソッド(カプセル化)によってのみ変更可能と見なされます。サブタイプはスーパータイプには存在しないメソッドを導入する可能性があるため、これらのメソッドの導入により、スーパータイプでは許可されないサブタイプの状態変更が可能になる場合があります。これは履歴制約により禁止されています。それは、リスコフとウィングによって導入された新しい要素でした。この制約の違反は、MutablePointをImmutablePointのサブタイプとして定義することで実証できます。不変ポイントの履歴では、作成後の状態は常に同じであるため、これは履歴制約の違反です。したがって、一般にMutablePointの履歴を含めることはできません。ただし、サブタイプに追加されたフィールドは、スーパータイプメソッドを介して観察できないため、安全に変更できます。

個人的には、単純にこれを覚えている方が簡単だと思います。タイプAのメソッドのパラメーターを見ている場合、サブタイプBを渡すと、驚きが生じますか?その場合、LSPの違反があります。

例外を投げることは驚きですか?あんまり。OrderStateでShipメソッドを呼び出しているか、GrantedまたはShippedであるかを問わず、いつでも発生する可能性があります。だから私はそれを説明しなければなりません、それは本当にLSPの違反ではありません。

そうは言っても、この状況に対処するより良い方法があると思います。C#でこれを書いている場合、インターフェイスを使用し、メソッドを呼び出す前にインターフェイスの実装を確認します。たとえば、現在のOrderStateがIShippableを実装していない場合、Shipメソッドを呼び出さないでください。

しかし、この特定の状況ではStateパターンを使用しません。Stateパターンは、このようなドメインオブジェクトの状態よりもアプリケーションの状態にはるかに適しています。

簡単に言えば、これは状態パターンの不自然な例であり、注文の状態を処理するための特に良い方法ではありません。しかし、それは間違いなくLSPを侵害しません。そして、Stateパターンは、それ自体、確かにそうではありません。


LSPとサプライズ/驚きの最小原理を混同しているように思えますが、LSPはこれに違反している明確な境界を持っていると思いますが、主観的な期待に基づくより主観的なPOLAを適用しています; それは各人が次とは異なる何かを期待しているということです。LSPは、消費者が推測する主観的に評価された期待ではなく、プロバイダーが提供する契約上の明確な保証に基づいています。
ジミー・ホッファ

@JimmyHoffa:そのとおりです。だからこそ、LSPを覚える簡単な方法を述べる前に、正確な意味を説明しました。ただし、「サプライズ」とは何かという疑問が頭に浮かぶ場合は、戻ってLSPの正確なルールを確認します(実際に思いのままに思い出せますか?)。上記の特定の条項に違反しないため、機能を置き換える例外(またはその逆)は難しいものです。これはおそらく、まったく異なる方法で処理する必要があるという手がかりです。
pdr

1
例外は、私の心の契約で定義された予想される条件であり、NetworkSocketを考えてください。開いていない場合は送信時に予想される例外があります。実装がLSPに違反している閉じたソケットに対して例外をスローしない場合、契約が反対を述べ、サブタイプが例外をスローする場合、LSPに違反しています。LSPで例外を分類する方法は多かれ少なかれです。ルールを思い出すことに関しては、これについて間違っている可能性がありますので、お気軽に教えてください。ただし、LSPとPOLAについての私の理解は、上記のコメントの最後の文で定義されています。
ジミー・ホッファ

1
@JimmyHoffa:POLAとLSPがリモート接続されているとは考えていませんでした(UIの用語でPOLAを考えています)しかし、彼らが対立しているのか、それとも一方が他方より主観的であるのかはわかりません。LSPはソフトウェアエンジニアリング用語でのPOLAの初期の説明ではありませんか?Ctrl-Cがコピー(POLA)でないときにイライラするのと同じように、ImmutablePointが実際にMutablePointサブクラス(LSP)であるために変更可能である場合にもイライラします。
pdr

1
CircleWithFixedCenterButMutableRadiusの消費者契約ImmutablePointがifの場合X.equals(Y)、消費者がXをYに自由に置換でき、その逆も同様であるため、そのような置換が問題を引き起こすような方法でクラスを使用することを控えなければならない場合、私はLSP違反の可能性が高いと見なします。このタイプはequals、すべてのインスタンスが別個のものとして比較されるように合法的に定義できる場合がありますが、そのような動作はその有用性を制限する可能性があります。
supercat

2

(これはC#の観点から書かれているため、チェック例外はありません。)

LSPに関するウィキペディアの記事によると、LSPの条件の1つは次のとおりです。

これらの例外自体がスーパータイプのメソッドによってスローされる例外のサブタイプである場合を除き、サブタイプのメソッドによって新しい例外をスローすることはできません。

どのようにそれを理解すべきですか?スーパータイプのメソッドが抽象的である場合、「スーパータイプのメソッドによってスローされる例外」とは正確には何ですか?これらは、スーパータイプのメソッドによってスローされる例外として文書化されている例外だと思います。

これが意味することは、OrderState.Ship()InvalidOperationExceptionこの操作が現在の状態でサポートされていない場合にスローする」などのように文書化されている場合、この設計はLSPを壊さないと思います。一方、スーパータイプメソッドがこの方法で文書化されていない場合、LSPに違反します。

しかし、これはこれが優れた設計であることを意味するものではありません。通常の制御フローには例外を使用しないでください。また、実際のアプリケーションでは、たとえばUIの[Ship]ボタンを無効にするなど、操作を実行する前に操作を実行できるかどうかを知りたいと思うでしょう。


実際、これがまさに私に考えさせられたポイントです。サブクラスが親で定義されていない例外をスローする場合、LSPの違反である必要があります。
松o

例外がチェックされない言語では、例外をスローすることはLSPの違反ではないと思います。それは言語そのものの概念であり、いつでもどこでも例外をスローできます。そのため、クライアントコードはその準備ができている必要があります。
ザパドロ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.