オブジェクト指向プログラミングの「継承」、思っているほど簡単な概念でしょうかという話。
クラス継承を書くときに親と子を間違えるなんてそんな間抜けなことするわけないって本当に言えますかという話です。
実例を見ながら
クラス継承の親子関係を議論するのに簡単な例として、実数を表すReal型と複素数を表すComplex型を作ってみます。
class Real {
double r;
}
class Complex {
double r;
double i;
}
これら二つのクラスがまったく無関係なクラスではない、継承関係が見て取れるのは異論ないかと思います。どちらが親でどちらが子でしょう。
まず、フィールド構成だけ注目してクラス定義に継承をつけてみます。
class Real {
double r;
}
class Complex extends Real {
double i;
}
フィールド構成は、ComplexがRealのものをまるっと包含しています。だからこれだけ見てクラスを書くと上記の通り、Real型が親、Complex is-a Realの形になりそうです。
でも、それでいいんでしたっけ!?
そう、実数と複素数の意味を思い出してもらえば、実数 is-a 複素数のはずです。Complex型が親でないといけません。実数を求めている関数に複素数を渡したら怒られますよね。では、こう?
class Complex {
double r;
double i;
}
class Real extends Complex {
public Complex(double r) {
this.r = r;
this.i = 0;
// iが常に0であるようにコードで保つ
}
}
親子関係は正しくなったはずですが、プログラムとして全然ダメです。Real型は無駄なフィールドiを持っていますし、しかもそれをプログラマの注意力だけで0に保たないといけません。これが正しいことになるんだったらクラスベースOOPなんて無価値です。
インターフェースなんですよComplexは
プロパティを使えるモダンな言語環境に慣れ親しんでいる方はもうお気付きかもしれません。メモリ領域確保を直に表現するフィールドという道具を使っているからおかしくなるんじゃないかと。
プロパティ使えばこんな感じになるわけなのです。
class Complex {
double r { get; set; }
double i { get; set; }
}
class Real: Complex {
double i {
get { return 0; }
set { }
// そもそもミュータブルにするかってのはあるがとりあえず虚部の代入は無視。
// 例外飛ばす方がより厳密ですよね。
}
}
すると見えてきました。iは多態である必要があったのです。
多態ということはインターフェース。うん、複素数というのはまずインターフェースであると考えれば綺麗にまとまったようです。
interface IComplex {
double getR();
double getI();
// せっかくだから今回はイミュータブルにしてみましたよ!
}
class Real implements IComplex{
double r;
double getR() { return r; }
double getI() { return 0; }
}
class Complex implements IComplex {
double r;
double i;
double getR() { return r; }
double getI() { return i; }
// いつも冗長でうんざりさせられるgetter/setterもこういう状況では役に立つというもの。
}
結論として、「やはりComplexが親。ただしインターフェースとして」となりました。こんな簡単なクラスでも意外に一ひねりが必要でしたね。