SetWidthメソッドとSetHeightメソッドをオーバーライドすると、Rectangleから継承するSquareに問題があるのはなぜですか?


105

SquareがRectangleのタイプである場合、なぜSquareはRectangleを継承できないのですか?それとも、なぜそれが悪いデザインですか?

私は人々が言うのを聞いたことがあります:

SquareをRectangleから派生させた場合、Squareは予想される任意の場所で使用できるはずです。

ここで問題は何ですか?そして、なぜSquareはあなたが長方形を期待するどこでも使えるのでしょうか?Squareオブジェクトを作成し、SquareのSetWidthメソッドとSetHeightメソッドをオーバーライドする場合にのみ使用できるのは、なぜ問題があるのでしょうか?

Rectangle基本クラスにSetWidthメソッドとSetHeightメソッドがあり、Rectangle参照がSquareを指している場合、SetWidthとSetHeightは、一方を設定するともう一方がそれに合わせて変更されるため、意味がありません。この場合、SquareはRectangleによるLiskov Substitution Testに失敗し、RectangleからSquareを継承させるという抽象化は悪いものです。

誰かが上記の議論を説明できますか?繰り返しますが、SquareでSetWidthおよびSetHeightメソッドをオーバーライドすると、この問題は解決しませんか?

私も聞いた/読んだ:

実際の問題は、長方形をモデリングするのではなく、「再整形可能な長方形」、つまり作成後に幅または高さを変更できる長方形をモデリングすることです(それでも同じオブジェクトと見なします)。このように長方形クラスを見ると、正方形は「再成形可能な長方形」ではないことが明らかです。なぜなら、正方形は形を変えられず、依然として正方形であるためです(一般的に)。数学的には、数学的文脈では可変性は意味をなさないため、問題は見られません。

ここでは、「サイズ変更可能」が正しい用語だと思います。長方形は「サイズ変更可能」であり、正方形も同様です。上記の議論に何か欠けていますか?正方形は、任意の長方形と同様にサイズ変更できます。


15
この質問は非常に抽象的なようです。クラスと継承を使用する無数の方法があります。あるクラスからいくつかのクラスを継承させることは、通常、それらのクラスをどのように使用したいかによって決まります。実用的なケースがなければ、この質問がどのように関連する答えを得ることができるかわかりません。
aaaaaaaaaaaa

2
いくつかの常識を使用すると、正方形長方形であることが思い出されるので、長方形が必要な場所で正方形クラスのオブジェクトを使用できない場合は、おそらくアプリケーション設計上の何らかの欠陥です。
クトゥルフ

7
より良い質問はWhy do we even need Square?2本のペンを持つようなものです。1本の青いペンと1本の赤い青、黄色、または緑のペン。青色のペンは冗長です-正方形の場合はコスト面でメリットがないため、さらに大きくなります。
ガスドール

2
@eBusinessその抽象性は、それを良い学習問題にします。特定のユースケースとは無関係に、サブタイピングのどの使用が悪いのかを認識できることが重要です。
ドーバル

5
@Cthulhuそうでもない。サブタイピングはすべて動作に関するものであり、可変正方形は可変長方形のようには動作しません。これが、「である」という比phorが悪い理由です。
ドーバル

回答:


189

基本的に私たちは物事が賢明に振る舞うことを望みます。

次の問題を考慮してください。

長方形のグループが与えられ、その面積を10%増やしたいです。だから私は、長方形の長さを以前の1.1倍に設定しました。

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

この場合、すべての長方形の長さが10%増加し、面積が10%増加します。残念ながら、実際に誰かが私に正方形と長方形の混合物を渡し、長方形の長さが変更されると幅も変更されました。

すべての単体テストを作成して四角形のコレクションを使用するため、単体テストに合格します。私は今、アプリケーションにわずかなバグを導入しました。

さらに悪いことに、会計のジムは私の方法を見て、彼が私の方法に正方形を渡すと、彼が非常に素晴らしい21%のサイズを得るという事実を使用する他のコードを書きます。ジムは幸せで、誰も賢くありません。

ジムは、優れた仕事のために別の部門に昇進します。アルフレッドはジュニアとして入社しました。AdvertisingのJillは最初のバグレポートで、このメソッドに正方形を渡すと21%増加し、バグを修正することを望んでいると報告しています。アルフレッドは、正方形と長方形がコードのあらゆる場所で使用されていることを認識し、継承チェーンを破ることは不可能であることを認識しています。彼はまた、経理のソースコードにアクセスできません。したがって、Alfredはこのバグを次のように修正します。

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

アルフレッドは彼の非常にハッキングのスキルに満足しており、ジルはバグが修正されたことを承認します。

来月は、会計がIncreaseRectangleSizeByTenPercentメソッドに正方形を渡すことができ、面積が21%増加することに依存していたため、誰も支払いを受けません。会社全体が「優先度1バグ修正」モードに入り、問題の原因を突き止めます。彼らはアルフレッドの修正に問題をトレースします。彼らは、会計と広告の両方を満足させなければならないことを知っています。そこで、次のようなメソッド呼び出しでユーザーを識別することで問題を修正します。

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

などなど。

この逸話は、プログラマーが毎日直面する現実の状況に基づいています。リスコフの置換原則の違反は違反を固定し、その時点で彼らだけが書かれている数年後に拾わ取得は非常に微妙なバグを導入することができ、物事の束を破るとなります固定ではない、それはあなたの最大のクライアントを怒らます。

この問題を修正するには、2つの現実的な方法があります。

最初の方法は、Rectangleを不変にすることです。RectangleのユーザーがLengthおよびWidthプロパティを変更できない場合、この問題はなくなります。異なる長さと幅の長方形が必要な場合は、新しい長方形を作成します。正方形は、長方形からうまく継承できます。

2番目の方法は、正方形と長方形の間の継承チェーンを分割することです。正方形が単一のSideLengthプロパティを持ち、長方形がLengthand Widthプロパティを持ち、継承がないと定義されている場合、長方形を期待して正方形を取得することで誤って物事を壊すことは不可能です。C#の用語ではseal、四角形クラスを使用して、取得するすべての四角形を実際に四角形にすることができます。

この場合、問題を修正する「不変オブジェクト」の方法が好きです。長方形の正体は、その長さと幅です。オブジェクトのIDを変更する場合、本当に必要なのは新しいオブジェクトであることは理にかなっています。古い顧客を失って新しい顧客を獲得した場合、Customer.Idフィールドを古い顧客から新しい顧客に変更せずに、新しいを作成しますCustomer

Liskov Substitutionの原則に対する違反は現実の世界では一般的です。それは主に、多くのコードが無能である/時間のプレッシャーにさらされている/気にしない/間違える人によって書かれているためです。それはいくつかの非常に厄介な問題を引き起こす可能性があります。ほとんどの場合、代わりに継承よりも合成優先します。


7
Liskovは1つのことであり、ストレージは別の問題です。ほとんどの実装では、Rectangleから継承するSquareインスタンスは、1つだけが必要な場合でも、2つの次元を格納するためのスペースを必要とします。
el.pescado

29
ポイントを説明するための物語の素晴らしい使用
ロリーハンター

29
いい話ですが、私は同意しません。ユースケースは次のとおりです。長方形の面積を変更します。修正は、オーバーライド可能なメソッド「ChangeArea」をSquareに特化した長方形に追加する必要があります。これは、継承チェーンを破壊せず、ユーザーが何をしたいのかを明確にし、言及された修正によって導入されたバグを引き起こしませんでした(適切なステージング領域でキャッチされます)。
ロイT.

33
@RoyT .:なぜ四角形はその領域の設定方法を知っている必要があるのですか?これは、長さと幅から完全に派生したプロパティです。さらに重要なのは、長さ、幅、またはその両方を変更する必要がある寸法ですか?
cHao

32
@Roy T.問題を別の方法で解決したと言うのは非常に素晴らしいことですが、事実は、これは開発者がレガシー製品を保守するときに日常的に直面する現実世界の状況の例であるということです-単純化されていますが。そして、そのメソッドを実装したとしても、継承者がLSPに違反し、これに似たバグを導入することを止めません。これが、.NETフレームワークのほとんどすべてのクラスが封印されている理由です。
スティーブン

30

すべてのオブジェクトが不変である場合、問題はありません。すべての正方形も長方形です。Rectangleのすべてのプロパティは、Squareのプロパティでもあります。

問題は、オブジェクトを変更する機能を追加したときに始まります。または実際に-プロパティのゲッターを読み取るだけでなく、オブジェクトに引数を渡し始めるとき。

Rectangleクラスのすべての不変式を維持するRectangleに対して実行できる変更がありますが、すべてのSquare不変式ではありません-幅や高さの変更など。突然、Rectangleの動作はそのプロパティだけでなく、可能な変更でもあります。それはあなたが長方形から得るものだけでなく、あなたが入れることができるものでもあります

RectangleにsetWidth幅を変更し、高さを変更しないと記載されているメソッドがある場合、Squareには互換性のあるメソッドはありません。高さではなく幅を変更すると、結果は有効なスクエアではなくなります。を使用するときにSquareの幅と高さの両方を変更することを選択した場合setWidth、Rectangleの仕様を実装していませんsetWidth。勝てないだけです。

RectangleとSquareに「入れる」ことができるもの、それらに送信できるメッセージを見ると、おそらく、Squareに有効に送信できる任意のメッセージ、Rectangleに送信することもできます。

それは共分散対反分散の問題です。

適切なサブクラスのメソッド(スーパークラスが期待されるすべての場合にインスタンスを使用できるサブクラス)では、各メソッドが以下を行う必要があります。

  • スーパークラスが返す値のみを返します。つまり、戻り値の型は、スーパークラスメソッドの戻り値型のサブタイプでなければなりません。戻り値は共変です。
  • スーパータイプが受け入れるすべての値を受け入れます。つまり、引数タイプはスーパークラスメソッドの引数タイプのスーパータイプでなければなりません。引数は反変です。

したがって、Rectangle and Squareに戻ります。SquareをRectangleのサブクラスにできるかどうかは、Rectangleが持つメソッドに完全に依存します。

Rectangleに幅と高さの個別のセッターがある場合、Squareは適切なサブクラスを作成しません。

同様に、compareTo(Rectangle)RectangleとcompareTo(Square)Squareでのように、引数で共変なメソッドを作成すると、RectangleとしてSquareを使用する際に問題が発生します。

SquareとRectangleを互換性があるように設計する場合、おそらく動作しますが、一緒に開発する必要があります。そうしないと動作しないでしょう。


「すべてのオブジェクトが不変であれば、問題はありません」これは明示的に幅と高さのためのセッターに言及し、この質問の文脈で明らかに無関係な文です-
ブヨ

11
私はそれが「明らかに無関係」だ場合でも、これは面白い発見
Jesvinホセ

14
@gnat質問の本当の価値は、2つの型の間に有効なサブタイピング関係があることを認識しているからです。これは、スーパータイプが宣言する操作に依存するため、ミューテーターメソッドがなくなると、問題がなくなることを指摘する価値があります。
ドーバル

1
@gnatまた、セッターは、ミューテータは、そうLRNは、本質的に、言っている「ことをしないでください、それは問題ではありません。」私はたまたま単純な型の不変性に同意しますが、良い点を指摘します。複雑なオブジェクトの場合、問題はそれほど単純ではありません。
パトリックM

1
このように考えて、「Rectangle」クラスによって保証される動作は何ですか?幅と高さを互いに独立して変更できること。(つまり、setWidthおよびsetHeight)メソッド。SquareがRectangleから派生した場合、Squareはこの動作を保証する必要があります。squareはこの振る舞いを保証できないので、継承が悪いです。ただし、setWidth / setHeightメソッドがRectangleクラスから削除された場合、そのような動作はないため、RectangleからSquareクラスを派生できます。
ニティンビハイ

17

ここにはたくさんの良い答えがあります。特にスティーブンの答えは、代替原則の違反がチーム間の現実世界の対立につながる理由を説明するのに適しています。

LSPの他の違反のメタファーとして使用するのではなく、長方形と正方形の特定の問題について簡単に話すかもしれないと思いました。

square-is-a-special-kind-of-rectangleには、めったに言及されない追加の問題があります。それは、なぜ正方形と長方形で停止するのですか?正方形が特別な種類の長方形であると喜んで言うなら、きっと私たちも喜んで言うべきです:

  • 正方形は、特別な種類の菱形です-正方形の角を持つ菱形です。
  • 菱形は特別な種類の平行四辺形です-それは等しい辺を持つ平行四辺形です。
  • 長方形は特別な種類の平行四辺形です-角が直角の平行四辺形です
  • 長方形、正方形、平行四辺形はすべて特別な種類の台形です-それらは2組の平行辺を持つ台形です
  • 上記はすべて特別な種類の四辺形です
  • 上記はすべて、特殊な種類の平面形状です。
  • 等々; ここでしばらく続けることができました。

すべての関係がここにあるべきですか?C#やJavaなどのクラス継承ベースの言語は、これらの種類の複雑な関係を複数の異なる種類の制約で表すようには設計されていません。これらのすべてをサブタイプ関係を持つクラスとして表そうとしないことで、単に質問を完全に回避するのが最善です。


3
シェイプオブジェクトが不変の場合IShape、境界ボックスを含むタイプがあり、描画、スケーリング、シリアル化が可能です。IPolygonまた、頂点の数を報告するメソッドとを返すメソッドを持つサブタイプがありIEnumerable<Point>ます。一つは、その後かもしれないIQuadrilateralから派生したサブタイプをIPolygonIRhombusそしてIRectangle、そこから派生し、ISquareから派生IRhombusしてIRectangle。可変性はすべてを窓から追い出し、多重継承はクラスでは機能しませんが、不変のインターフェースでは問題ないと思います。
supercat 14年

私はここでエリックに実質的に同意しません(-1では十分ではありません!)。@supercatが言及しているように、これらの関係はすべて(おそらく)関連しています。これは単なるYAGNIの問題です。必要になるまで実装しないでください。
マークハード

非常に良い答えです!ずっと高いはずです。
-andrew.fox

1
@MarkHurd-これはYAGNIの問題ではありません。提案された継承ベースの階層は、説明した分類法のような形をしていますが、それを定義する関係を保証するために記述することはできません。定義されたから返されたIRhombusすべてが等しい長さのエッジに対応することをどのように保証しますか?インターフェースの実装だけでは、具体的なオブジェクトが菱形であることを保証しないため、継承は答えになりません。PointEnumerable<Point>IPolygonIRhombus
A.レイガー

14

数学的な観点から、正方形は長方形です。数学者が正方形を修正して、正方形の契約を守らなくなると、長方形に変わります。

しかし、オブジェクト指向設計では、これは問題です。オブジェクトはそれが何であるかであり、これには動作だけでなく状態も含まれます。私が正方形のオブジェクトを保持しているが、他の誰かがそれを長方形に変更すると、それは私自身の過失なしに正方形の契約に違反します。これにより、あらゆる種類の悪いことが起こります。

ここで重要な要素は可変性です。構築された後、形状を変更できますか?

  • 可変:一度構築された形状を変更できる場合、正方形は長方形とis-a関係を持つことはできません。長方形のコントラクトには、反対側の長さが等しくなければならないという制約が含まれますが、隣接する側はそうである必要はありません。正方形には4つの等しい辺が必要です。四角形のインターフェイスで四角形を変更すると、四角形の規約に違反する可能性があります。

  • 不変:一度構築した図形を変更できない場合、正方形のオブジェクトも常に長方形のコントラクトを満たす必要があります。正方形は、長方形とis-aの関係を持つことができます。

どちらの場合も、1つ以上の変更を加えた状態に基づいて、正方形に新しい形状を作成するように依頼することができます。たとえば、「この正方形に基づいて新しい長方形を作成します。ただし、反対側のAとCは2倍の長さです」と言うことができます。新しいオブジェクトが構築されているため、元の正方形はその契約を守り続けます。


1
This is one of those cases where the real world is not able to be modeled in a computer 100%。なんでそうなの?正方形と長方形の機能モデルを引き続き使用できます。唯一の結果は、これらの2つのオブジェクトを抽象化するためのより単純な構造を探す必要があることです。
サイモンベルゴ

6
長方形と正方形の間には、それよりも多くの共通点があります。問題は、長方形の正体と正方形の正体が辺の長さ(および各交点の角度)であるということです。ここでの最善の解決策は、四角形を四角形から継承することですが、両方を不変にすることです。
スティーブン

3
@Stephen Agreeed。実際、それらを不変にすることは、サブタイプの問題に関係なく行うべき賢明なことです。それらを可変にする理由はありません-新しい正方形や長方形を構築することは、それらを突然変異させることほど難しくありません。これで、エイリアス/副作用を心配する必要がなくなり、必要に応じてそれらをマップ/辞書のキーとして使用できます。ホットスポットがシェイプコードにあることを実際に測定して証明するまで、「パフォーマンス」と言う人もいますが、これに対して「時期尚早な最適化」と言います。
ドーバル

申し訳ありませんが、遅くなり、答えを書いたときはとても疲れました。私はそれを書き直して、私が本当に意味することを言いました。その核心は可変性です。

13

そして、なぜSquareはあなたが長方形を期待するどこでも使えるのでしょうか?

それは、サブタイプであることの意味の一部であるためです(「リスコフ置換原理」も参照)。あなたはこれを行うことができる必要があります:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

OOPを使用するときは、実際にこれを常に(時にはさらに暗黙的に)行います。

そして、SquareのSetWidthメソッドとSetHeightメソッドをオーバーライドすると、なぜ問題が発生するのでしょうか?

のを賢明にオーバーライドできないためですSquare。正方形「長方形のようにサイズ変更できない」ためです。長方形の高さが変わっても、幅は変わりません。しかし、正方形の高さが変わると、それに応じて幅も変わる必要があります。問題は、単にサイズを変更できるだけでなく、両方の次元で独立してサイズを変更できることです。


多くの言語では、このRect r = s;行は必要ありません。できdoSomethingWith(s)ます。ランタイムは、すべての呼び出しを使用sして仮想Squareメソッドを解決します。
パトリックM

1
@PatrickMサブタイプのある健全な言語では必要ありません。明示するために、説明のためにその行を含めました。

したがって、オーバーライドsetWidthsetHeightて、幅と高さの両方を変更します。
ApproachingDarknessFish

@ValekHalfHeartこれはまさに私が検討しているオプションです。

7
@ValekHalfHeart:それはまさにリスコフ置換原則の違反であり、2年後にコードがどのように機能するかを忘れてしまったときに、あなたを悩ませ、奇妙なバグを見つけようと何度も眠れぬ夜を過ごすことになります。
Jan Hudec

9

あなたが説明していることは、リスコフ代替原理と呼ばれるものに反します。LSPの基本的な考え方は、特定のクラスのインスタンスを使用するときはいつでも、バグ導入することなく、常にそのクラスのサブクラスのインスタンスをスワップできるようにすることです。

Rectangle-Square問題はLiskovを導入するのにあまり良い方法ではありません。実際に非常に微妙な例を使用して広範な原理を説明しようとし、すべての数学で最も一般的な直感的な定義の1つに反しています。その理由から、楕円円問題と呼ばれる人もいますが、これに関しては少しだけ良いです。より良いアプローチは、私が「Parallelogram-Rectangle」問題と呼ぶものを使用して、少し後退することです。これにより、物事がはるかに理解しやすくなります。

平行四辺形は、2組の平行辺を持つ四辺形です。また、2組の一致角度があります。これらの線に沿って平行四辺形のオブジェクトを想像するのは難しくありません:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

長方形を考える一般的な方法の1つは、直角の平行四辺形です。一見すると、これはRectangleをParallelogramから継承するための良い候補にしているように見えるかもしれません。そのため、すべてのおいしいコードを再利用できます。しかしながら:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

これらの2つの関数がRectangleにバグを導入するのはなぜですか?問題は、長方形の角度を変更できないことです。それらは常に90度であると定義されているため、このインターフェイスは、平行四辺形を継承するRectangleに対して実際には機能しません。RectangleをParallelogramを予期するコードに交換し、そのコードが角度を変更しようとすると、ほぼ確実にバグが発生します。サブクラスで書き込み可能なものを取得し、読み取り専用にしましたが、これはリスコフ違反です。

さて、これを正方形と長方形にどのように適用しますか?

値を設定できると言うとき、一般に、単に値を書き込むことができるよりも少し強い何かを意味します。ある程度の排他性を意味します。値を設定すると、異常な状況がなければ、再度設定するまでその値のままになります。書き込むことができるが、設定されたままにならない値には多くの用途がありますが、一度設定した値にとどまる値に依存する場合も多くあります。そして、そこから別の問題が発生します。

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

SquareクラスはRectangleからバグを継承しましたが、新しいバグがいくつかあります。setSideAとsetSideBの問題は、これらのどちらももう真に設定できないことです。どちらか一方に値を書き込むことはできますが、もう一方が書き込まれると、下から変更されます。互いに独立してサイドを設定できることに依存しているコードで、これを平行四辺形に交換すると、驚くことになります。

それが問題であり、それがLiskovの紹介としてRectangle-Squareを使用することに問題がある理由です。Rectangle-Squareは、何かに書き込むことができるかどうかの違いに依存します。それは、何かを設定できるのと読み取り専用にするよりもはるかに微妙な違いです。Rectangle-Squareは、注意が必要なかなり一般的な落とし穴を文書化するため、例として価値がありますが、入門的な例として使用すべきではありません。学習者が最初の基礎でいくつかの接地を取得しましょう、そしてその後、彼らに難しい何かを投げます。


8

サブタイピングは動作に関するものです。

typeがtypeのBサブタイプAであるためにはA、同じセマンティクスでtypeがサポートするすべての操作をサポートする必要があります(「振る舞い」の凝った話)。すべてのBがAであるという理論的根拠を使用しても動作しませ -動作の互換性には最終決定権があります。ほとんどの場合、「Bは一種のA」であり、「BはAのように動作する」と重複しますが、常にではありません

例:

実数のセットを検討してください。任意の言語では、我々は彼らが操作をサポートすることを期待することができ+-*、と/。次に、正の整数のセット({1、2、3、...})を考えます。明らかに、すべての正の整数も実数です。しかし、正の整数型は実数型のサブタイプですか?4つの演算を見て、正の整数が実数と同じように動作するかどうかを確認しましょう。

  • +:問題なく正の整数を追加できます。
  • -:正の整数のすべての減算が正の整数になるわけではありません。例3 - 5
  • *:問題なく正の整数を乗算できます。
  • /:常に正の整数を除算して正の整数を取得できるわけではありません。例5 / 3

そのため、正の整数は実数のサブセットですが、サブタイプではありません。有限サイズの整数についても同様の引数を作成できます。明らかにすべての32ビット整数も64ビット整数ですが、32_BIT_MAX + 1タイプごとに異なる結果が得られます。したがって、プログラムを提供し、すべての32ビット整数変数のタイプを64ビット整数に変更した場合、プログラムの動作が異なる可能性が高くなります(ほとんどの場合は間違っています)。

もちろん、+結果が64ビット整数になるように32ビット整数を定義できますが、2つの32ビット数値を追加するたびに64ビットのスペースを予約する必要があります。それはあなたのメモリのニーズに応じてあなたに受け入れられるかもしれないし、そうでないかもしれません。

なぜこれが重要なのですか?

プログラムが正しいことは重要です。これはおそらく、プログラムが持つ最も重要なプロパティです。プログラムが特定のタイプに対して正しい場合、プログラムがA特定のサブタイプに対して正しいことを保証する唯一の方法Bは、あらゆる方法でB同様Aに動作する場合です。

したがって、タイプはRectanglesであり、その仕様は、その側面を独立して変更できると述べています。Rectangles実装が仕様に準拠していると想定して使用するプログラムをいくつか作成しました。次にSquare、サイドのサイズを個別に変更できないというサブタイプを導入しました。その結果、長方形のサイズを変更するほとんどのプログラムは間違っています。


6

SquareがRectangleのタイプである場合、SquareがRectangleを継承できないのはなぜですか?または、なぜそれが悪いデザインなのですか?

まず最初に、正方形が長方形であると思う理由を自問してください

もちろん、ほとんどの人は小学校でそれを学びました。長方形は90度の角度を持つ4辺の形状であり、正方形はこれらすべての特性を満たします。それで、正方形は長方形ではありませんか?

ただし、それはすべて、オブジェクトをグループ化するための最初の基準は何か、これらのオブジェクトをどのコンテキストで見ているかによって異なります。ジオメトリでは、形状はそれらのポイント、ライン、エンジェルのプロパティに基づいて分類されます。

「正方形は長方形の一種です」と言う前に、まず自分に問いかける必要があります。これは、私が気にする基準に基づいていますか

大多数の場合、それはあなたがまったく気にかけていることではありません。GUI、グラフィックス、ビデオゲームなどの形状をモデル化するシステムの大部分は、オブジェクトの幾何学的なグループ化を主に考慮していませんが、動作です。正方形が幾何学的な意味で長方形の一種であることが重要なシステムに取り組んだことがありますか。それは4つの辺と90度の角度を持っていることを知って、あなたに何を与えますか?

静的なシステムをモデリングするのではなく、物事が発生する動的なシステムをモデリングします(形状の作成、破壊、変更、描画など)。このコンテキストでは、オブジェクト間の共有動作に関心があります。これは、シェイプで何ができるか、一貫したシステムを維持するためにどのルールを維持する必要があるかが主な関心事だからです。

このコンテキストでは、正方形を変更する方法を管理するルールは長方形と同じではないため、正方形は間違いなく長方形ではありません。したがって、それらは同じタイプのものではありません。

その場合、それらをそのようにモデル化しないでください。なんで?それは不必要な制限以外には何も得られません。

Squareオブジェクトを作成し、SquareのSetWidthメソッドとSetHeightメソッドをオーバーライドする場合にのみ使用できるのは、なぜ問題があるのでしょうか?

あなたが実際にコードでそれらが同じものではないことを述べているのにそうするならば。あなたのコードは、正方形はこのように振る舞い、長方形はそのように振る舞いますが、それらは同じままです。

2つの異なる動作を定義しただけなので、気にするコンテキストでは明らかに同じではありません。それで、あなたが気にしない文脈でそれらが似ているだけなら、なぜ彼らは同じふりをするのでしょうか?

これは、開発者がモデル化したいドメインに来たときに重大な問題を浮き彫りにします。ドメイン内のオブジェクトについて考え始める前に、興味のあるコンテキストを明確にすることが非常に重要です。あなたは何に興味がありますか。数千年前、ギリシア人は形の線と天使の共有特性を気にし、これらに基づいてそれらをグループ化しました。それはあなたがそれがあなたが気にしないものである場合、あなたがそのグループ化を続けることを強制されることを意味しません(ソフトウェアの時間の99%であなたは気にしないでしょう)。

この質問に対する多くの回答は、行動をグループ化することに関するサブタイピングに焦点を当てています

しかし、ルールに従うためだけにこれを行うのではないことを理解することは非常に重要です。あなたがこれをしているのは、ほとんどの場合、これもあなたが実際に気にしていることだからです。正方形と長方形が同じ内部天使を共有するかどうかは気にしません。あなたは、彼らがまだ正方形と長方形である間に何ができるかを気にします。オブジェクトの動作のルールに基づいてシステムを変更することに焦点を合わせているシステムをモデリングしているため、オブジェクトの動作に関心があります。


型の変数がRectangleを表すためだけに使用される場合、クラスがそのコントラクトを継承し、そのコントラクトを完全に順守する可能性があります。残念ながら、多くの言語では、値をカプセル化する変数とエンティティを識別する変数を区別しません。SquareRectangle
supercat

おそらく、しかし、そもそもなぜわざわざ。長方形/正方形の問題のポイントは、「正方形は長方形」の関係を機能させる方法を試してみるのではなく、オブジェクトを使用しているコンテキストに関係が実際に存在しないことを理解することです(動作)、および無関係な関係をドメインに押し付けないことに関する警告として。
コーマックマルホール

または別の言い方をすると、スプーンを曲げようとしないでください。それ無理。代わりに、スプーンがないという真実だけを実現しようとします。:-)
コーマックマルホール

1
不変のSquare型から継承する不変の型を持つことは、Rectnagle正方形に対してのみ実行可能な操作の種類がある場合に役立ちます。概念の現実的な例として、ReadableMatrix型[基本型、まばらに含むさまざまな方法で格納される可能性のある長方形配列]、およびComputeDeterminantメソッドを考えます。それは持っている感覚になるかもしれないComputeDeterminantだけで仕事をReadableSquareMatrix由来のType ReadableMatrixIはの例であると考えるだろうSquareから導出しますRectangle
supercat

5

SquareがRectangleのタイプである場合、SquareがRectangleを継承できないのはなぜですか?

問題は、物事が実際に何らかの方法で関連している場合、モデリング後にまったく同じ方法で関連しなければならないという考えにあります。

モデリングで最も重要なことは、共通の属性と共通の動作を識別し、それらを基本クラスで定義し、子クラスに追加の属性を追加することです。

あなたの例の問題は、それが完全に抽象的なことです。そのクラスを何に使用するのかを誰も知らない限り、どのデザインを作成すべきかを推測することは困難です。しかし、本当に高さ、幅、サイズ変更だけが必要な場合は、次のように論理的です。

  • Squareを基本クラスとして定義し、widthパラメーターを指定resize(double factor)して、指定された係数で幅をサイズ変更します
  • RectangleクラスとSquareのサブクラスを定義します。これは、別の属性を追加しheight、そのresize関数をオーバーライドsuper.resizeし、指定された係数で呼び出して高さを変更するためです

プログラミングの観点から見ると、SquareにはRectangleにはないものは何もありません。SquareをRectangleのサブクラスとして作成する意味はありません。


+1正方形が数学の特別な種類の長方形であるというだけで、オブジェクト指向で同じであるという意味ではありません。
ロヴィス

1
正方形は正方形であり、長方形は長方形です。それらの間の関係は、モデリングでも同様に保持する必要があります。そうでない場合、かなり貧弱なモデルになります。実際の問題は次のとおりです。1)それらを変更可能にすると、正方形や長方形をモデリングしなくなります。2)2種類のオブジェクト間で「ある」関係が成り立っているという理由だけで、一方を他方に無差別に置き換えることができると仮定します。
ドーバル

4

LSPによって、2つの間の継承関係を作成してオーバーライドしsetWidthsetHeight正方形が確実に同じになるようにするために、混乱した直感的な動作が導入されるためです。コードがあるとしましょう:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

ただし、メソッドがcreateRectangle返された場合は、SquareからSquare継承することで可能になりRectangeます。それから期待は壊れます。ここで、このコードでは、幅または高さを設定すると、それぞれ幅または高さのみが変更されると予想されます。OOPのポイントは、スーパークラスで作業する場合、その下のサブクラスについてまったく知識がないことです。また、サブクラスが動作を変更して、スーパークラスに関する期待に反する場合、バグが発生する可能性が高くなります。そして、これらの種類のバグはデバッグも修正も困難です。

OOPの主要なアイデアの1つは、継承されたデータではなく動作であるということです(これもIMOの主要な誤解の1つです)。また、正方形と長方形を見ると、継承関係で関連付けることができる動作自体はありません。


2

LSPが言うことは、継承するものはすべてでRectangleなければならないということRectangleです。つまり、a Rectangleがすることは何でもするべきです。

のドキュメントRectangleは、Rectangle名前付きの動作はr次のように書かれていると思われます。

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Squareに同じ動作がない場合、のように動作しませんRectangle。そのため、LSPはから継承してはならないと述べていRectangleます。メソッドのオーバーライドで何か間違ったことをするのを止めることができないため、言語はこの規則を実施できませんが、それは「言語がメソッドをオーバーライドできるので大丈夫」という意味ではありません!

これで、上記のコードが10を出力することを意味しないような方法でドキュメントを書くことが可能になりRectangleます。「これはXを実行します。さらに、このクラスの実装はYを実行します」などのドキュメントが表示される場合があります。その場合、クラスからインターフェイスを抽出し、インターフェイスが保証するものと、それ以外にクラスが保証するものとを区別するための良いケースがあります。しかし、人々が「可変正方形は可変長方形ではなく、不変正方形は不変長方形である」と言うとき、彼らは基本的に上記が可変長方形の合理的な定義の一部であると仮定しています。SquareRectangle


これはで説明するだけでポイントを繰り返しているようだ5時間前に投稿答え
ブヨ

@gnat:他の回答をほぼこの程度まで編集することを希望しますか?;-)他の回答者がおそらく質問に答えるのに必要であると感じ、そうではないと感じるポイントを削除せずにできるとは思わない。
スティーブジェソップ


1

サブタイプと、拡張により、オブジェクト指向プログラミングは、AがBを必要とする場合、A <= Bの場合、タイプAの任意の値を使用できるというLiskov Substitution Principleに依存することがよくあります。すべてのサブクラスにこのプロパティがあると想定されます(そうでない場合、サブタイプはバグがあり、修正する必要があります)。

ただし、この原則は、ほとんどのコードの非現実的/非代表的なものであるか、実際に満たすことは不可能であることがわかります(非自明な場合)!四角問題または円楕円問題(http://en.wikipedia.org/wiki/Circle-ellipse_problem)として知られるこの問題は、達成するのがいかに難しいかという有名な例です。

観測的に同等の正方形と四角形を実装できることに注意してください。ただし、区別が役に立たなくなるまで、より多くの機能を破棄するだけです。

例として、http://okmij.org/ftp/Computation/Subtyping/を参照してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.