楕円と真円
楕円の短径と長径が等しい場合、真円となる。すなわち楕円の特殊型が真円であることから、型階層として楕円のサブタイプに真円を考えたくなる。
Wikipediaにみられる実装案
https://en.wikipedia.org/wiki/Circle–ellipse_problem
A. 成功または失敗の値を返す
code:java
class Ellipse {
private double x;
private double y;
public double area() {
return x * y * Math.PI;
}
public boolean stretchX(double dx) {
this.x += dx;
return true
}
}
class Circle extends Ellipse {
@Override
public boolean stretchX(double dx) {
return false
}
}
B. 変更後の新しい値を返す
円の場合は、stretchXによって引き伸ばしはされないので、元の値が返る。円を引き伸ばすときは、stretchメソッドの方を使う。
code:java
class Ellipse {
private double x;
private double y;
public double area() {
return x * y * Math.PI;
}
public double stretchX(double dx) {
this.x += dx;
return this.x;
}
}
class Circle extends Ellipse {
@Override
public double stretchX(double dx) {
return this.x;
}
public void stretch(double dr) {
this.x += dr;
this.y += dr;
}
}
C. 楕円の制約をゆるめる
円の場合はstretchXメソッドにより、y軸方向にも引き伸ばしを行う。
code:java
class Ellipse {
private double x;
private double y;
public double area() {
return x * y * Math.PI;
}
public void stretchX(int dx) {
this.x += dx;
}
}
class Circle extends Ellipse {
@Override
public void stretchX(int dx) {
this.x += dx;
this.y += dx;
}
}
D. 円を引き伸ばすと楕円型に変わる
オブジェクトの型を動的に変更できる言語であれば、CircleのstretchXを呼んだ時点で、Ellipse型に変わる
code:javascript
class Ellipse {
constructor(x, y) {
this.x = x;
this.y = y;
}
area() {
return x * y * Math.PI;
}
stretchX(dx) {
this.x += dx;
}
}
class Circle extends Ellipse {
constructor(r) {
super(r,r);
}
stretchX(dx) {
this.__proto__ = Ellipse.prototype;
this.x += dx;
}
}
E. 新しいインスタンスを返す
オブジェクトの型が動的に変わらなくても、Circleに対してstretchXを呼んだら新しくEllipseが返されるようにすれば、似たことは実現できる。
code:java
class Ellipse {
private double x;
private double y;
public Ellipse(double x, double y) {
this.x = x;
this.y = y;
}
public double area() {
return x * y * Math.PI;
}
public Ellipse stretchX(double dx) {
return new Ellipse(this.x + dx, this.y);
}
}
class Circle extends Ellipse {
@Override
public Ellipse stretchX(int dx) {
return new Ellipse(this.x + dx, this.y);
}
}
F. ミュータブルな操作を別クラスにする
引き伸ばしなどオブジェクトのプロパティを書き換える操作は、MutableEllipseを新たに作りそちらで実装する。
code:java
class Ellipse {
private double x;
private double y;
public double area() {
return x * y * Math.PI;
}
}
class MutableEllipse extends Ellipse {
public void stretchX(int dx) {
this.x += dx;
}
}
class Circle extends Ellipse {
}
G. 変更操作のインタフェースを定義し、それを実装することがstretchXを呼べる事前条件とする
code:java
interface Stretchable {
void stretchX(double dx);
}
abstract class AbstractEllipse {
private double x;
private double y;
public double area() {
return x * y * Math.PI;
}
}
class Ellipse extends AbstractEllipse implements Stretchable {
@Override
public void stretchX(double dx) {
this.x += dx;
}
}
class Circle extends AbstractEllipse {
}
H. 共通の処理を抽象クラスにもたせ、差分を実装クラスで実装する
code:java
abstract class EllipseOrCircle {
private double x;
private double y;
public double area() {
return x * y * Math.PI;
}
}
class Ellipse extends EllipseOrCircle {
public void stretchX(double dx) {
this.x += dx;
}
}
class Circle extends EllipseOrCircle {
}
「円は楕円である」というのはもはや成り立たない。
Gの方が明示的なインタフェースが与えられるので、あえてこれを選択する理由は?
I. 継承関係をなくす
継承しないようにする。
code:java
class Ellipse {
public void stretchX(int dx) {
this.x += dx;
}
}
class Circle {
}
ポリモーフィズムが不要ならこれでも…
J. 円に対してもEllipseクラスを使う
K.
https://www.researchgate.net/publication/323457799_Ellipse-Circle_Dilemma_and_Inverse_Inheritance
code:java
interface EllipseQuery {
}
class MutableEllipse extends Ellipse {
public void stretchX(int dx) {
this.x += dx;
}
}
class Circle extends Ellipse {
}
L. リスコフ論文案
GやHのように型ファミリーを構成する。
型とは何か?
リスコフ置換原則の論文(Behavioral Subtyping Using Invariants and Constraints)によると、以下のものから構成されるものと定義されている。
名前
属性値の定義
不変条件
メソッド
名前
シグネチャ
引数
戻り値
例外
事前条件/事後条件とそれを満たすふるまい
不変条件(invariants)とは単一の状態において満たさなくてはならない性質
制約(constraints)とは2つの状態において満たさなくてはならない性質(メソッドの事前条件/事後条件に現れる)
サブタイプの分類
サブタイプを作る場合、以下のバリエーションが存在しうる
属性が増える
属性が減る
属性の定義域が狭くなる
日時 → 請求日時
属性の定義域が広くなる
ふるまいが増える
ユーザ → プレミアムユーザ
ふるまいが減る
楕円→真円 : 引き伸ばす
ふるまいの詳細化
このうち「属性の定義域が広くなる」はサブタイプがスーパータイプの不変条件を満たせないので、型階層の作り方として不適であるので考えない。「属性が減る」に関しても、すぐさま型階層が壊れるわけではないが、そうまでして型階層を構成する意義が認めにくいので、考慮の対象外とする。
「ふるまいの詳細化」に関しても、サブタイプがスーパータイプの不変条件や事前条件/事後条件を満たすように構成されれば、その内容は問わない。
LSPの論文中には、サブタイプの構成の仕方には、以下の2通りがあるとされている。
Extension Subtypes
スーパータイプに追加のメソッドや状態を付け加えたサブタイプ。
なので上記でいう、
属性が増える
ふるまいが増える
はこのパターンである。このケースではLSPに違反することなく自然な形で型階層を作ることができる。
Constrained Subtypes
スーパータイプの不変条件を狭くしたサブタイプ。Number -> Integer -> Positive Integerのような型階層は、この例となる。これは注意深く型階層を作らないと、ふるまいの事前条件/事後条件が満たされなくなる。
楕円-真円の関係性は、LSP論文中のConstrained Subtypesのパターンである。定義域が狭まる、楕円の不変条件に加えて、「長径aと短径bが等しい」という不変条件が追加されたものが真円である。このとき、「長径を引き伸ばす」というメソッドを楕円が持っていると、楕円のサブタイプである真円の不変条件が壊れてしまう。なので、このままではLSPに反して「ふるまいが減る」サブタイプになる。
この解決策としてLSPの論文中では、楕円からサブタイプ真円を作るのではなく、楕円と真円のスーパータイプを作るようにしよう、と言っている。
https://gyazo.com/4c118add4230b4c7107887fdb48c0877