Circle
extend Ellipse
を指定すると、 Liskov Substition Principle が無効になります。これは、事後条件を変更するためです。つまり、XとYを個別に設定できます。楕円を描画しますが、円の場合、Xは常にYと等しくなければなりません。
しかし、ここでの問題は、Circleが楕円のサブタイプであることによって引き起こされているのではないでしょうか。関係を逆転できませんか?
したがって、Circleはスーパータイプです。単一のメソッドsetRadius
があります。
次に、EllipseはsetX
とsetY
を追加してCircleを拡張します。 EllipseでsetRadius
を呼び出すと、XとYの両方が設定されます。つまり、setRadiusの事後条件は維持されますが、拡張インターフェースを介してXとYを個別に設定できるようになりました。
しかし、ここでの問題は、Circleが楕円のサブタイプであることによって引き起こされているのではないでしょうか。関係を逆転できませんか?
この問題(および四角/四角形の問題)は、あるドメイン(ジオメトリ)の関係が別のドメイン(動作)でも保持されると誤って想定している
幾何学理論のプリズムを通してそれらを見ているならば、円と楕円は関連しています。しかし、あなたが見ることができるのはそれだけではありません。
オブジェクト指向設計は動作。を扱います
オブジェクトを定義する特性は、オブジェクトが担当する動作です。そして、振る舞いの領域では、円と楕円は非常に異なる振る舞いをしており、それらをまったく関連があると考えない方が良いでしょう。このドメインでは、楕円と円には重要な関係はありません。
ここでの教訓は、OODに最も意味のあるドメインを選択することです。別のドメインに存在するという理由だけで関係を確立しようとするのではありません。
この間違いの最も一般的な実例は、オブジェクトが関連している(または同じクラスである)と想定することです。これは、オブジェクトの動作が大きく異なっていても、dataが似ているためです。これは、データの行き先を定義することによって「データを最初に」オブジェクトの構築を開始する場合の一般的な問題です。完全に異なる振る舞いを持つデータを介して関連するクラスになる可能性があります。たとえば、給与明細と従業員オブジェクトの両方に「総給与」属性があるかもしれませんが、従業員は給与明細のタイプではなく、給与明細は従業員のタイプではありません。
円は楕円の特殊なケースです。つまり、楕円の両方の軸が同じです。楕円が一種の円であるかもしれないと述べることは、問題領域(幾何学)で根本的に偽です。この欠陥のあるモデルを使用すると、たとえば「円上のすべての点の中心までの距離が同じである」など、円の多くの保証に違反します。それもリスコフの代替原則違反です。楕円の半径は1つですか? (setRadius()
ではなく、より重要なのはgetRadius()
)
楕円のサブタイプとしての円のモデリングは基本的に間違っているわけではありませんが、このモデルを壊すのは可変性の導入です。 setX()
およびsetY()
メソッドがなければ、LSP違反はありません。異なる次元のオブジェクトが必要な場合は、新しいインスタンスを作成することをお勧めします。
class Ellipse {
final double x;
final double y;
...
Ellipse withX(double newX) {
return new Ellipse(x: newX, y: y);
}
}
コーマックには本当に素晴らしい答えがありますが、私はそもそも混乱の理由について少し詳しく説明したいと思います。
OOの継承は、「リンゴとオレンジはどちらも果物のサブクラスです」のように、実際のメタファーを使って教えられることが多いです。残念ながら、これはOOは、プログラムとは無関係に存在するいくつかの分類階層に従ってモデル化する必要があります。
ただし、ソフトウェア設計では、アプリケーションの要件に従って型をモデル化する必要があります。他のドメインの分類は通常、無関係です。 「アップル」オブジェクトと「オレンジ」オブジェクトのある実際のアプリケーションでは(スーパーマーケットの在庫管理システムなど)、おそらくまったく別のクラスではなく、「フルーツ」などのカテゴリはスーパータイプではなく属性になります。
円楕円問題は赤いニシンです。幾何学では、円は楕円の特殊化ですが、この例のクラスは幾何学図形ではありません。重要なことに、幾何学図形は変更できません。 変換にすることもできますが、円変換を楕円に変換できます。したがって、円は半径を変更できるが楕円に変更できないモデルは、ジオメトリに対応していません。このようなモデルは、特定のアプリケーション(描画ツールなど)で意味をなす場合がありますが、幾何学的分類は、クラス階層の設計方法には関係ありません。
では、CircleはEllipseのサブクラスにする必要がありますか?これらのオブジェクトを使用する特定のアプリケーションの要件に完全に依存します。描画アプリケーションでは、円と楕円の扱い方にさまざまな選択肢があります。
円と楕円を、UIが異なる形状の異なるタイプとして扱います(例:楕円には2つのサイズ変更ハンドル、円には1つのハンドル)。これは、アプリケーションの観点からは、幾何学的には円であるが、円ではない楕円を持つことができることを意味します。
円を含むすべての楕円を同じように扱いますが、xとyを同じ値に「ロック」するオプションがあります。
楕円は、スケーリング変換が適用された単なる円です。
可能な各設計は、異なるオブジェクトモデルにつながります-
最初のケースでは、円と楕円は兄弟クラスになります。
2つ目は、まったく別のCircleクラスはありません。
3番目のものでは、明確なEllipseクラスはありません。したがって、いわゆる円と楕円の問題は、これらのいずれにも当てはまりません。
だから提起された質問に答えるには:円は楕円を延長する必要がありますか?答えは:それはあなたがそれで何をしたいかによります。しかし、おそらくそうではありません。
「Ellipse」クラスと「Circle」クラスがあり、一方が他方のサブクラスであると主張することは、最初から間違いです。 2つの現実的な選択肢があります。1つは、個別のクラスを持つことです。それらは、色、オブジェクトが塗りつぶされているかどうか、描画の線幅などの共通のスーパークラスを持つ場合があります。
もう1つは、「Ellipse」という名前の1つのクラスのみを持つことです。そのクラスがあれば、それを使用して円を表すのは簡単です(実装の詳細によってはトラップが存在する場合があります。楕円には角度があり、その角度の計算で円形状の楕円が問題になることはありません)。円形の楕円に特化したメソッドを使用することもできますが、これらの「円形の楕円」は完全な「楕円」オブジェクトのままです。
LSPポイントに続いて、この問題に対する「適切な」解決策の1つは、@ HorusKolと@Ixrecが出現したことです-Shapeから両方のタイプを派生させます。しかし、それは作業しているモデルに依存するため、常にそのモデルに戻る必要があります。
私が教えられたのは:
サブタイプがスーパータイプと同じ動作を実行できない場合、関係はIS-A前提で保持されません-変更する必要があります。
英語で:
(例:
これが分類のしくみ(つまり、動物の世界)であり、原則としてOOです。
これを継承とポリモーフィズム(常に一緒に記述される)の定義として使用すると、この原則が破られた場合は、モデル化しようとしている型を再考する必要があります。
@HorusKulと@Ixrecで述べたように、数学では型を明確に定義しています。しかし、数学では、円は楕円のサブセットなので、円は楕円です。しかし、OOPでは、これは継承の仕組みではありません。クラスは、それが既存のクラスのSUPERSET(拡張)である場合にのみ継承する必要があります。つまり、すべてのコンテキストで基本クラスをISのままにします。
これに基づいて、私はソリューションを少し言い換える必要があると思います。
Shapeベースタイプ、次にRoundedShape(実質的には円ですが、ここでは意図的に別の名前を使用しています...)
...次に楕円。
そのように:
(これは現在、言語の人々にとって理にかなっています。私たちは頭の中で「サークル」という明確に定義された概念をすでに持っています。