長方形クラスが以下のようになっているとします。
class Rectangle{
float w,h;
public:
void setWidth(float w){this->w = w;}
void setHeight(float h){this->h = h;}
float getWidth(){return w;}
float getHeight(){return h;}
float getArea(){return w * h;}
};
横と縦の長さをそれぞれ指定、取得できる。また、面積を取得できる。
(横辺の長さを縦辺の長さと無関係に変更できる)
次に、正方形クラスの仕様を以下のようにしようと思います。
class Square{
float len;
public:
void setLen(float len){this->len = len;}
float getLen(){return len;}
float getArea(){return len * len;}
};
辺の長さを指定、取得できる。面積も取得できる。
(辺の長さはすべて同時にしか変更できない)
では、ここで正方形クラスを長方形クラスの子供として書き直してみましょう。
正方形は長方形なのだから、正方形クラスは長方形クラスと置き換え可能であるはずです。
class Rectangle{
float w,h;
public:
void setWidth(float w){this->w = w;}
void setHeight(float h){this->h = h;}
float getWidth(){return w;}
float getHeight(){return h;}
float getArea(){return w * h;}
};
class Square : public Rectangle{
pubic:
void setLen(float len){w = h = len;}
float getLen(){return w;}
};
どうでしょう。これで良いでしょうか。
ではSquareクラスを使ってみましょう。
int main()
{
Rectangle rect;
rect.setWidth(5.f);
rect.setHeight(6.f);
printf("RECT w=%f, h = %f, area=%f\n", rect.getWidth(), rect.getHeight(), rect.getArea());
Square sq;
sq.setWidth(5.f);
sq.setHeight(6.f);
printf("SQUARE w = %f, h = %f, area = %f\n", sq.getWidth(), sq.getHeight(), sq.getArea());
}
=> RECT w = 5.00000, h = 6.00000, area = 30.000000
=> SQUARE w = 5.00000, h = 6.00000, area = 30.000000
おや、おかしい。正方形なのに、縦と横の長さが違っている。
長方形クラスから継承したまま、setWidth()やsetHeight()を適切に書き換えていなかったからでしょうか。
もう一度書き直してみましょう。
class Rectangle{
float w,h;
public:
virtual void setWidth(float w){this->w = w;}
virtual void setHeight(float h){this->h = h;}
float getWidth(){return w;}
float getHeight(){return h;}
float getArea(){return w * h;}
};
class Square : public Rectangle{
pubic:
void setWidth(float w){this->w = w; this->h = w;}
void setHeight(float h){this->w = h; this->h = h;}
void setLen(float len){w = h = len;}
float getLen(){return w;}
};
これでどうでしょう。正方形の縦と横の長さが違うということは起こらなくなります。
ではまた使用してみましょう。
=> RECT w = 5.00000, h = 6.00000, area = 30.000000
=> SQUARE w = 6.00000, h = 6.00000, area = 36.000000
今度は確かに正方形になっています。
でもやっぱりなんだかおかしい。
正方形なのに、setWidth()とかsetHeight()とか使っている。
もちろん、正方形クラスとしてはsetLen()を使ってもらいたくて、メソッドを宣言している訳ですが、それでもsetWidth()やsetHeight()は使えてしまう。いや、長方形クラスを継承しているのだから、setWidth(),setHeight()も使えなければならないはずです。
ですが、setWidth()という名前の関数は、縦幅を変えずに横幅だけ変えられるかのように見えます。でも正方形なので、縦幅を変えれば横幅も一緒に変わります。
つまり、関数の名前と機能が合っていません。これは良くない。
正方形クラスでsetWidth()やsetHeight()が使えてしまうことが問題であるのなら、親子関係を逆転させてしまってはどうでしょうか?
正方形より長方形クラスのほうが、縦と横を別々に指定できるなど機能が多いのですから、そのほうがしっくりくるような気もします。
やってみましょう。
class Square{
float len;
public:
virtual void setLen(float len){this->len = len;}
float getLen(){return len;}
virtual float getArea(){return len * len;}
};
class Rectangle : public Square{
float w, h;
public:
void setLen(float len){this->w = this->h = this->len = len;}
void setWidth(float w){this->w = w;}
void setHeight(float h){this->h = h;}
float getWidth(){return w;}
float getHeight(){return h;}
float getArea(){return w * h;}
};
どうでしょう? これで良いでしょうか。
良くないですね。
長方形クラスにおいて、setLen()とかgetLen()って何なんでしょうか。
とりあえずsetLen()については、正方形クラスの名残ということで、縦と横の両方を同じ長さに指定するようにしてみました。では、getLen()って何を返したら良いんでしょうか。len? でもsetLen()のあとにsetWidth(),setHeight()が呼び出され、wもhもlenと違う値になっていたら、lenという値には何の意味も無くなってしまいます。
そもそも、len,w,hという3つの変数が存在していることが冗長だし不自然です。
え〜、じゃあどうしたらいいの!?
親子にする必要ないんじゃないの!!
・・・それが正解です。
プログラミングにおいては、正方形と長方形は親子ではありません。
クラスにおいての継承関係は、親の持つメソッドが子にも使えなければなりません。
さて、長方形クラスには、setWidth(), setHeight()という関数があります。これらは、(縦幅とは無関係に)横幅を変える関数、(横幅とは無関係に)縦幅を変える関数です。
正方形クラスには、setLen()という関数があります。これは、縦幅と横幅を同時に変えるクラスです。
正方形クラスは、setWidth()やsetHeight()という関数は使えません。そういう機能は正方形にはありません。
長方形クラスは、setLen()という関数は使えません。使えないこともないですが、それは長方形の機能としてはちょっとおかしい。
ということは、両方のクラスは包含関係(is-a関係)になっておらず、どちらも相手の子クラスにはなれません。
プログラミング的な長方形クラスと正方形クラスの定義を書いてみましょうか。
長方形クラス
縦と横の辺の長さをそれぞれ指定できる
縦と横の辺の長さをそれぞれ取得できる
正方形クラス
辺の長さを指定できる
辺の長さを取得できる
ちょっと表現を変えてみましょう
長方形クラス
2つの縦の辺の長さが同じである
縦の辺の長さを(横の長さと無関係に)変更できる
2つの横の辺の長さが同じである
横の辺の長さを(縦の長さと無関係に)変更できる
正方形クラス
4つの辺の長さが同じである
辺の長さを変更できる
最初に書いた数学的定義と比べると、辺の長さを「変更できる」というところが余分についている、というところがポイント。
数学的定義においては、「そこにすでに存在している図形が、長方形・正方形であるかどうかを判定するためのもの」が定義です。
対してプログラミング的には、「図形を自ら指定して作成し、それが長方形・正方形としての定義を満たす必要がある」のです。
つまり、数学的定義でだけ考えるなら、「すでに作られている図形」がそこにあって、その辺の長さだけを見て「これは長方形だ」とか「正方形だ」とか言えれば良いんです。
でもプログラミング的には、まず「自分で長方形・正方形を作る」ということができないといけない。長方形・正方形としての定義を満たす形で図形を作ることができるということが必要になる。
ここが数学との違いであり、正方形が長方形になれない理由です。「そこにある図形が正方形なら、それは長方形でもある」というのは成り立ちますが、「そこにある図形を正方形として作る」ということが「長方形を作る」のと同じ方法では作れないのです。
「そこにある図形が長方形であることを判定する方法」は、そこにある図形が正方形のときでもなりたちますが、「とある図形を長方形として作る方法」は、正方形を作るときには使えないのです。(なぜなら、長方形を作るにはwidthとheightを別々に指定できる必要があり、対して正方形を作るときにはwidthとheightは同じ値を指定することしかできないからです)
まとめると、「長方形かどうか判定する」と「正方形かどうか判定する」は包含関係になっていますが、「長方形を作る」と「正方形を作る」が包含関係になっていないことが問題なのです。
じゃあ、「長方形かどうか判定する」と「正方形かどうか判定する」だけを行うクラスであれば、継承関係にできるんでしょうか。
できるはずです。
setter関数が無く、getter関数だけのクラスであれば、理屈上継承しても問題ないはずです。
class Rectangle{
public:
float getWidth();
float getHeight();
float getArea();
};
class Square : public Rectangle{
public:
float getLen();
};
SquareクラスでgetWidth()やgetHeight()が使用できますが、これは特に問題ありません。getLen()と合わせ、3つとも常に同じ値が返ってくるようになっていれば正しい動作と言えます。
現実解としてありそうなのは、以下のような形だと思います。
class IQuadrangle{
public:
virtual float getArea() = 0;
};
class Rectangle : public IQuadrangle{
float w, h;
public:
void setWidth(float w){this->w = w;}
void setHeight(float h){this->h = h;}
float getWidth(){return w;}
float getHeight(){return h;}
float getArea(){return w * h;}
};
class Square : public IQuadrangle{
float len;
public:
void setLen(float len){this->len = len;}
float getLen(){return len;}
float getArea(){return len * len;}
};
基底クラスとしてIQuadrangle(四角形クラス)があり、面積を求めるといった、すべての四角形に共通して仕様できる機能だけを持っている。(ただし実装は無い)
で、RectangleとSquareはいずれもIQuadrangleの子クラスであり、それぞれがそれぞれに辺の長さや面積の求め方を自定義している。
間違いなく共通している機能(この例で言うと、面積が求められること)だけを取り出して規定クラスに宣言し、残りの部分はそれぞれのクラスで実装する。
長方形と正方形は、共通する部分ももちろんありますが、包含はできなかったんです。
共通する部分だけ取り出して別クラスにし、両方の親になってもらいます。長方形と正方形は、親子ではなく兄弟だったのです。
PR