Inheritance and Composition
2024/11/30 C++ 活動報告
Inheritance and Composition
応用が利くコードを書こう!
プログラムを書くとき、何を意識していますか? まずは「何をしたいか」ですよね。どんなアプリを作りたいのか、どんなデータ処理を行いたいのか。それを念頭に置きながらコーディングに励むかと思います。
しかしながら、そればかりに注力していると応用が利かないコードになってしまう事があります。例えば以下のコードを見てください。
#include <string>
#include <iostream>
class Patient {
protected:
int id_;
std::string name_;
public:
Patient(int id, std::string name) : id_(id), name_(name) {}
void Introduce() {
std::cout << "私は" << name_ << "です。" << std::endl;
std::cout << "患者IDは" << id_ << "です。" << std::endl;
}
};
int main(void) {
Patient tanaka(0, "田中");
Patient olivia(1, "Olivia");
tanaka.Introduce();
olivia.Introduce();
}
実行結果
私は田中です。
患者IDは0です。
私はOliviaです。
患者IDは1です。
C++なので複雑に見えるかもしれませんが、患者情報(idと名前)を保存するPatient
というクラスが定義されていて、Introduce()
を呼び出すことで自己紹介してくれるプログラムとなっています。
このプログラム、一見何も問題が無いように思えますが、重大な問題点が一つ存在します。そうです、日本語話者の事しか想定していないのです……! Oliviaちゃんが日本語を話せなかったら困ります!
では英語で話す人用にPatientSpeaksEnglish
というクラスを新しく作りましょうか? 短期的にはそれでもOKですね。しかし、後になって「患者情報に生年月日を加えよう」と思った時、Patient
もPatientSpeaksEnglish
も変更しないといけなくなります。このような変更を繰り返していると、いずれ致命的なバグを産むかもしれません。
このような理由から「応用が利くプログラム 」が必要になってくるのです!
継承(Inheritance)
応用的なコードを書く方法その1、それは継承(Inheritance) を使う事です。次のコードを見てください。
#include <string>
#include <iostream>
class Patient {
protected:
int id_;
std::string name_;
public:
Patient(int id, std::string name) : id_(id), name_(name) {}
virtual void Introduce() = 0;
};
class PatientSpeaksJapanese : public Patient {
public:
using Patient::Patient;
void Introduce() override {
std::cout << "私は" << name_ << "です。" << std::endl;
std::cout << "患者IDは" << id_ << "です。" << std::endl;
}
};
class PatientSpeaksEnglish : public Patient {
public:
using Patient::Patient;
void Introduce() override {
std::cout << "I am " << name_ << "." << std::endl;
std::cout << "My id is " << id_ << "." << std::endl;
}
};
int main(void) {
PatientSpeaksJapanese tanaka(0, "田中");
PatientSpeaksEnglish olivia(1, "Olivia");
tanaka.Introduce();
olivia.Introduce();
}
実行結果
私は田中です。
患者IDは0です。
I am Olivia.
My id is 1.
このコードでは患者情報を保存するPatient
クラス内ではIntroduce()
を定義していません(=0と書くと未定義のままになります)。その後、これを継承したクラスであるPatientSpeaksJapanese
やPatientSpeaksEnglish
らの中で、それぞれ関数の動作が定義されています。
要するに継承(Inheritance) とは「テンプレートだけ作って、詳細な動作は各々定義する」事を指します。
合成(Composition)
さて、このように患者情報管理プログラム(?)を作っていると、基礎研究をしている友人から声がかかりました。
「私の作っている『DNA情報解析プログラム』を組み込まない?」
この誘いに乗ったあなたは、以下のDNA情報解析プログラムを患者情報に組み込もうと考えました。
#include <string>
#include <iostream>
enum SpeciesId {UNDEFINED, Mouse, Rat, Human};
class Creature {
protected:
int species_id_;
std::string dna_;
public:
Creature(int species_id = UNDEFINED, std::string dna = "")
: species_id_(species_id), dna_(dna) {}
void Analyze() {
std::cout << "** Result **" << std::endl;
std::cout << "Species Id:" << species_id_ << std::endl;
// 解析結果が表示されるコード。ここでは省略。
}
};
int main(void) {
Creature mouse(Mouse, "atgcattggt...ggc");
Creature rat(Rat, "gctagctagt...att");
mouse.Analyze();
rat.Analyze();
}
実行結果
** Result **
Species Id:1
** Result **
Species Id:2
Creature
クラスは種族ID(整数値)と塩基配列(文字列)を保存するクラスです。そしてAnalyze()
関数で解析結果を表示します。
これを組み込む方法として、先ほど使った継承を使う事も出来ます。Patient
クラスにCreature
クラスを継承させたら、一応動く事には動きます。
しかしながら、ここで大きな問題が一つ。Patient(0, "Tanaka")
と書いたときに「第一引数の0は患者IDなの、それとも種族IDなの?」となってしまいます。第二引数も「これは塩基配列、それとも名前?」となってしまいます。
このように機能の追加に継承を使っていると、コンストラクタや関数名が競合したときに予期しない動作が起きたりします。
そこで使うのが合成(Composition)です。これは物凄くシンプルで、情報をデータの一部として持っておくという意味です。以下が何そのサンプルコードです。
// 前略
class Patient {
protected:
int id_;
std::string name_;
Creature creature_; // 継承するのではなく、データの一部として持っておく!
public:
Patient(int id, std::string name) : id_(id), name_(name) {}
virtual void Introduce() = 0;
void SetDNA(std::string dna) {
creature_ = Creature(Human, dna);
}
void Analyze() {
creature_.Analyze();
}
};
// 中略
int main(void) {
PatientSpeaksJapanese tanaka(0, "田中");
tanaka.SetDNA("atgcatt...atcg");
tanaka.Introduce();
tanaka.Analyze();
}
実行結果
私は田中です。
患者IDは0です。
** Result **
Species Id:3
「ただそれだけ?」と思った方も少なくないでしょう。しかしながら、これは非常に重要な事なのです。というのも、継承の便利さに気付いた人は何でも継承しようとする病気に罹る事があります。そこで合成(Composition) の大切さをここで知っておくことで、この病気を未然に防ぐ事が出来るのです!
このような理由から、継承よりも合成の方が安全であるとされており、継承はどうしても必要な場合のみ行うようにするべきです。