オブジェクト指向入門指南
2018年にオブジェクト指向を使ったプログラミングについて教える機会があったので、その時に教えたことをまとめたものになります。
オブジェクト指向って何だろう?何のために使うの?って感じている人向けです。
用語解説
メソッド : クラスに所属する関数の事。
メンバ : クラスに所属する変数の事。
オブジェクト : ここでは、クラスのインスタンスを指すことが多い。
オブジェクト指向とは?
継承とは?
継承を行うと、あるクラスの機能(メソッドとメンバ)を他のクラスに引き継ぐことが出来ます。
//cpp class Player : public Character {};
//java class Player extends Character {}
このように書くことで、「Player
はCharacter
である」ということをコンパイラに伝えることが出来ます。
具体的には、Player
クラスのインスタンスに対してCharacter
クラスのメソッドを呼び出したり、Characer
クラスのメンバを持たせることが可能になります。
このとき、Player
クラスの側を派生クラス(サブクラス)、Character
クラス側を基底クラス(スーパークラス)といいます。
多態性とは?
C++やJavaなどの静的型付けの言語は、コンパイル時に変数の型を決定しなければならないため、変数は基本的に一つの型の機能しか持つことが出来ません。
しかし、一つの変数に対して様々な型の値を代入し、変数の振る舞いを変化させることは可能です。このように、一つの変数が複数の型の値を持つことが出来ることを、多態性(ポリモーフィズム)といいます。
継承を用いれば、簡単にこの多態性を実現することが出来ます。
//cpp Character* character = new Player();
//java Character character = new Player();
PlayerクラスがCharacter
クラスを継承していた場合、このようにCharacter
型のポインタや変数にPlayer
型の値を代入することが出来ます。
多態性を実現する方法は他にも多く存在します。例えばC++では、テンプレートを用いて以下のように多態性を実現することが可能です。
#include <iostream> class Player { public: void update() { /* 更新 */ std::cout << "update" << std::endl; } }; template<class CharType> class Character { public: void update() { charType_.update(); } private: CharType charType_; }; int main() { Character<Player> character; while (1) { character.update(); } return 0; }
Character<Player> character;
この部分のテンプレート引数に、update()
をメソッドとして持つ型を指定することによって、変数character
の振る舞いを変化させることが出来ます。
このように、テンプレート引数によってクラス内の処理を変化させる設計をPolicyと呼びます。
デザインパターンとは?
デザインパターンは、特にオブジェクト指向の言語で多くのプログラマーが開発に用いる手法や設計を、再利用可能なものとして明確化させたものです。代表的なものに書籍 『オブジェクト指向における再利用のためのデザインパターン』 で提案されたGoFの23のデザインパターンがあります。
手法が明文化され、名前が付いたことによって、より良い設計が広く使われるようになっただけでなく、会話や文章の中でスムーズに設計の意図を伝えることが出来るようになりました。
言葉で説明するだけではイメージし難いと思いますので、ここでデザインパターンを一つ紹介しておきます。
ゲームでは、一つの画面に主人公や敵モブなどの複数のキャラクターを描画し、その上でキャラクターが持つ武器や防具を描画しなければなりません。それそれを一つのオブジェクトで表すと、個々のクラスの実装は単純になるのですが、設計が複雑になります。
そこで、それぞれのオブジェクトに同じ仕組みを持たせることで管理しやすくしていきたいと思います。例えば以下のような抽象クラスを定義します。
class Node { public: void addChild(Node* child) { children_.push_back(child); } void draw() const { mydraw(); for (auto child : children_) child->draw(); } private: virtual void mydraw() const = 0; std::list<Node*> children_; };
まずは、privateメンバから見ていきます。
virtual void mydraw() const = 0;
この関数は、この抽象クラスを継承するクラスが、自分自身を描画するためのクラスです。どのような描画を行うかは、継承先によって異なるので、純粋仮想関数にして継承先に実装を強制します。
std::list<Node*> children_;
この配列はこのNodeがもつ子クラスを格納します。この親子関係を図で表すと以下のようになると言えます。
こうして、木構造のように親Nodeが子Nodeを知っている状態にします。
次に、publicなメンバを見ていきます。
void addChild(Node* child) { children_.push_back(child); }
このメソッドで子Nodeをchildren_
に追加することが出来ます。このメソッドは継承先によって機能が変化することはないので、仮想関数にしません。
void draw() const { mydraw(); for (auto child : children_) child->draw(); }
このメソッドが今回の抽象クラスのメインとなる部分です。やっていることは単純で、自分自身の描画関数であるmydraw()
を呼び出した後、children_に格納されている子Node全てのdraw()
を呼び出しています。こうすることで、自分だけでなく子Nodeにも漏れなく処理をいきわたらせることが出来ます。
では、実際にこの抽象クラスをどう使うのかを見てみましょう。例えば、ゲームのメニュー画面で、主人公だけでなくインベントリやその中の剣や盾も描画しなければならないとき、プログラムは以下のように書けるでしょう。
#include <iostream> #include <list> class Node { public: void addChild(Node* child) { children_.push_back(child); } void draw() const { mydraw(); for (auto child : children_) child->draw(); } private: virtual void mydraw() const = 0; std::list<Node*> children_; }; class Player : public Node { private: void mydraw() const override { std::cout << "Player" << std::endl; } }; class Inventory : public Node { private: void mydraw() const override { std::cout << "Inventory" << std::endl; } }; class Sword : public Node { private: void mydraw() const override { std::cout << "Sword" << std::endl; } }; class Shield : public Node { private: void mydraw() const override { std::cout << "Shield" << std::endl; } }; int main() { Player player; Inventory inventory; inventory.addChild(new Sword()); inventory.addChild(new Shield()); player.addChild(&inventory); while (1) { player.draw(); } return 0; }
ここで、Nodeを継承しているのはPlayer
、Inventry
、Sword
、Shield
です。
そして、main関数内でNodeの機能を用いて木構造を構築しています。イメージとしては下図のようなものを構築していると考えると良いでしょう。
ここで、木構造の中で最も根元にあるPlayer
に対してdraw()
を呼び出せば、それだけでこの構造内にあるすべてのオブジェクトの描画を行ってくれるのです。
このように、オブジェクトに木構造を持たせて管理することをCompositeパターンと呼びます。これは、先ほど紹介したGoFの23のデザインパターンの一つです。
クラス図とは?
その場しのぎの実装ばかりをしていれば、オブジェクトの継承関係や依存関係は、複雑で難解なものになってしまうでしょう。そこで、あらかじめクラスの関係を決定づけておくことで、明解でメンテナンスしやすいプログラムを書くことが出来ます。
クラス図は、クラスの関係を記述する方法の一つで、プログラムの設計では広く用いられるものです。デザインパターンを表す際にもよく使われており、上述のCompositeパターンは、以下のように表されます。
それぞれの四角がクラスを表しており、クラスの「名前、メンバ、メソッド」が記述されています。また、その間の矢印がクラスの関係を表しています。
クラス図の感覚をつかんでいただくためにも、上のクラス図の要素を順に見ていきましょう。
まず、「Component」が上述のコードのNode
クラスにあたります。「Component」の中の「operation()」はdraw()
に、「add()」はaddChild()
に対応しています。
次に、「Composite」はコード内のPlayer
、Inventry
などのクラスにあたります。「Composite」からは二つの矢印が出ており、斜めに出ているものがけ継承を表し、菱形がついている方が集約を表しています。集約というのは、複数の「Component」から、「Composite」が成り立っているというようなイメージです。実際に、Inventry
は配列内で複数のNode
を持っています。
最期に、「Leaf」はSword
やShield
などの、Node
を継承しているが、自分自身はNode
を所有していないクラスを表します。このことは、矢印を見れば分かります。「Leaf」は「Component」を継承はしていますが、集約はしていません。
これはデザインパターンを表す小さいものですが、ゲーム全体や、一つの場面を表すクラス図はとても大きいものになるので、小さいものから慣れていくと良いでしょう。
ここでは、厳密な書き方のご紹介は省略します。詳しくはこちらををご覧いただくとよいのではないでしょうか。
オブジェクト指向の思想
GoFの思想
GoFでデザインパターンは多くの場面で用いることが出でき、広く知られる設計方法となっています。
しかし、デザインパターンを理解するのは簡単ではなく、特にオブジェクト指向でのプログラミング経験が少ない者にはイメージが非常につかみにくいものとなっています。
逆に、オブジェクト指向でのプログラミングをしっかり身につけている人間にはごく当たり前のことのように受け入れられることも珍しくありません。デザインパターンの多くは、プログラマーが自然に辿り着く発想の域を出ないものなのです。
何が言いたいのかというと、デザインパターンは、初心者が躍起になって覚えるべきものでもないということです。全てを理解しようとするのではなく、デザインパターンで成そうとしている目的や設計の方向性を何となくでもつかんでおけば十分だと私は考えています。そうすれば、実践を続けるうちにデザインパターンのような美しい設計が自然にできるようになるはずです。
ここでは、その何となくの目的や考え方をつかめる手助けが出来ればと思います。
多態性の利便性を理解する
デザインパターンの多くが継承による多態性を利用して実現されています。その代表的なものとして、Strategyがあると言える。
Strategyパターンは、オブジェクトが所有するメンバを多態的に変化させて、それによってオブジェクト全体の振る舞いを変化させるパターンです。
例となるコードを見てみましょう
#include <iostream> class Weapon { public: virtual void attack() const = 0; }; class Sword : public Weapon { public: void attack() const override { std::cout << "剣で攻撃" << std::endl; } }; class Bow : public Weapon { public: void attack() const override { std::cout << "弓で攻撃" << std::endl; } }; class Player { public: Player(Weapon* weapon) : weapon_(weapon) {} void attack() const { weapon_->attack(); } private: Weapon * weapon_; }; int main() { Sword sword; Bow bow; Player swordPlayer(&sword); Player bowPlayer(&bow); swordPlayer.attack(); bowPlayer.attack(); return 0; }
実行結果は以下のようになるでしょう
剣で攻撃 弓で攻撃
このように、同じPlayer型でも、何も継承せず、if文やswitch文もなしに異なる振る舞いをさせることが出来ました。
原理は簡単で、Weapon
抽象クラス型の変数weapon_
に、Weapon
を継承したクラスのインスタンスを与えることによって、仮想関数を利用した処理の決定を行っています。ここで、その仮想関数はattack()
になります。
同じことをif文やswitch文で行うと実装が煩雑になってしまうため、このような実装はとてもスマートです。変更にも柔軟で、それぞれのパーツの汎用性も高いです。
このように、多態性を一定のパターンで用いることで、コードを簡潔にしてif文やswitch文による複雑怪奇なコードを排除し、汎用性の高いコードを書くことが出来ます。
多態性は、オブジェクト指向の中で非常に重要な役割を持つ要素なので、積極的に使い理解を深めることで、デザインパターンの習得が早まるでしょう。
役割を分割する
オブジェクト指向言語でのプログラミングでは、ついつい一つのオブジェクトに複数の機能を持たせてしまいがちです。膨大な機能を持つGODクラス
が、プログラミングの大部分を支配することも珍しくありません。
しかし、そのようなプログラムは非常に脆弱で、変更に大きな影響を受けやすく、可読性を著しく損ないます。
そこで、クラスの持つ役割を適切に分割しなくてはなりませ。デザインパターンには、クラスの役割を分割するための様々な方法が提唱されています。
代表的なものとしてAbstract Factoryパターンが挙げられます。
Abstract Factoryパターンではインスタンスの生成部分を専用のクラスで行います。以下に具体例を示します。
class Sword { public: Sword(int damage, int number) : damage_(damage), number_(number) {} private: int damage_; int number_; }; class SuperSword : public Sword { public: SuperSword(int damage, int num, int additionalDamage) : Sword(damage, num), additionalDamage_(additionalDamage) {} private: int additionalDamage_ }; class SwordFactory { public: Sword makeSword(int damage) { Sword ret = Sword(damage, count_); count_++; return ret; } SuperSword makeSword(int damage, int additionalDamage) { SuperSword ret = SuperSword(damage, count_, additionalDamage); count_++; return ret; } private: int count_; };
SwordFactory
クラスは、Sword
クラスを生成するためだけのクラスです。
Sword
クラスは、与ダメージとこれが何本目に生成されたの剣なのかをメンバとして持つとします。
このとき、Sword
クラスのコンストラクタでこれが何本目に生成されけ剣なのかを知るのは困難です。しかし、SwordFactory
で全ての剣を生成する場合、何度剣を生成されたかをカウントするだけでよくなります。それがSwordFactory
のメンバであるcount_
の役割です。
また、生成用の関数のオーバーロードを利用して異なるクラスを生成することも可能です。上の例では、makeSword
の引数によってオーバーロードを行い、場合によってはSuperSword
を生成できます。
このように、Abstruct Factoryでは、クラスの生成に必要な幾ばくかの処理を分離できるだけでなく、様々な柔軟な処理を行うことが出来ます。しかし、Abstruct Factoryのメリットは他にもあります。
クラス生成時の処理をコンストラクタから分離することによって、コンストラクで例外が発生しないようにできます。これは非常に重要なことで、C++のコンストラクタで例外が発生した場合、new演算子でのメモリ確保時にメモリリークが発生する恐れがあります。
他にも役割を分割するようなものにObserverパターンやCommandパターンがあります。このように、デザインパターンにはクラスの役割を分割するためのものがあり、オブジェクト指向プログラミングでの役割の分割がそれだけ重要なことなのかが分かります。
利用する側の視点に立つ
クラスの設計をするとき、最も重要なことはクラスの利用者の使い心地であるといえるでしょう。不要なメソッドが存在したり、内部の実装が外に漏れだしているようなクラスは、非常に使い勝手が悪く、チーム開発に大きな災いをもたらすでしょう。
そこで、デザインパターンの中には、クラスの使い勝手がよくなるような仕組みをもたらすものが存在します。
Template Method
パターンはその一つです。
Template Methodパターンでは、一つの関数が複数の関数の呼び出し順を管理し、管理される関数を仮想関数にして継承を用いて具象化させたり、Strategy
パターンを用いて付け替えていきます。
具体的には、以下のようなものです。
#include <iostream> class Player { public: void update() { move(); attack(); } private: virtual void move() { std::cout << "歩く" << std::endl; } virtual void attack() { std::cout << "攻撃する" << std::endl; } }; class FastPlayer : public Player { private: void move() override { std::cout << "早く歩く" << std::endl; } }; class StrongPlayer : public Player { private: void attack() override { std::cout << "強く攻撃する" << std::endl; } }; int main() { Player player; FastPlayer fastPlayer; StrongPlayer strongPlayer; player.update(); fastPlayer.update(); strongPlayer.update(); return 0; }
出力結果は以下のようになります。
歩く 攻撃する 早く歩く 攻撃する 歩く 強く攻撃する
ここでは、upadate()
がwalk()
とattack()
を管理しており。それぞれを仮想関数にすることによって、Player
を継承したクラスで細かく振る舞いを変化させることが出来るようになっています。
ここで注目してほしいのは、これらのクラスの扱いやすです。呼び出し側からみれば、たった一つのメソッドを呼び出すだけで、正しい順序、正しいパラメータで処理を行ってくれるのです。
公開するメソッドを限定することで、使いやすいクラスを設計することが可能になります。これはカプセル化の考え方と同一のものです。
他にも、FacedeパターンやProxyパターンも、クラスの利用者にとって使いやすくなる工夫を実現します。常に利用者にとって使いやすいクラスを設計することも、デザインパターンの目的と言えるでしょう。
カプセル化の思想
カプセル化はオブジェクト指向の中で最も大切な考え方で、オブジェクト指向に限らずそれなりの規模のプログラミングをするなら必ず知っておくべき概念です。
プログラムは作成すればそれで終わりではなく、その後のメンテナンスや変更に柔軟であることが求められます。また、プログラムを複数人で書く場合は、自分のプログラムが他のメンバーにも使いやすいように書かなくてはならないでしょう。
そこで、重要になってくるのがカプセル化の考え方です。カプセル化では、言葉の通りオブジェクトの実装をカプセルのように覆って隠蔽していきます。
言葉で説明しても分かりづらいところですので、具体例を述べていきたいと思います。まず、同じ機能を持った二つのクラスを用意しました。
//カプセル化されたPlayer class CapsulatePlayer { public: void update() { x_ += vx_; y_ += vy_; } private: int x_, y_; int vx_, vy_; }; //カプセル化されていないPlayer class RevealPlayer { public: int getX() { return x_; } int getY() { return y_; } int getVX() { return vx_; } int getVY() { return vy_; } void setX(int x) { x_ = x; } void setY(int y) { y_ = y; } private: int x_, y_; int vx_, vy_; };
これは、アクションゲームのプレイヤーを表すクラスだとします。
このプレイヤーオブジェクトは、速度vx
、vy
を持っており毎フレーム決まった速度で移動し、その結果を変数x
、y
に格納しなくてはなりません。しかし、その機能を行うとき、呼び出し側のコードは大きく異なります。
player.update()
CapsulatePlayer
では、この一行のみで成すべき処理が全て完了します。
int px = player.getX(); int py = player.getY(); int pvx = player.getVX(); int pvy = player.getVY(); px += pvx; py += pvy; player.setX(px); player.setY(py);
なんと、RevealPlayer
では、同じ処理を行うのにこれだけのコード量を呼び出し側に強要します。しかし、深刻なのはそれだけではありません。
getter関数とsetter関数が存在し、オブジェクトの情報が筒抜けな上に自由に書き換えまで出来てしまいます。加えて、やらなければならない処理を、呼び出す側が知っていなければなりません。
これでは、内部の値や実装が全く隠蔽できていないため、RevealPlayer
は
カプセル化が十分でないと言えるでしょう。
対して、CapsulatePlayer
は、内部の値や実装が外側からは見えないようになっており、情報が十分に隠蔽されているため、カプセル化されていると言えるでしょう。
カプセル化が出来ているかの基準は概ねこんな感じです。上でも少し述べましたが、カプセル化がされていない場合、様々なデメリットがあります。
まず、メソッドが非常に多いため、内部のメンバや実装を変更する場合に、その影響がpublicなメソッドにまで及ぶ可能性があります。そうなると、このクラスを利用しているコードを全て書き直さなければなりません。これでは、メンテナンスや修正が非常にやりにくくなってしまします。
上の例で説明していきます。例えば、Playerの座標と速度をfloat
型の変数に変更しなければならないとします。となると、それぞれの実装は以下のようになるでしょう。
class CapsulatePlayer { public: void update() { x_ += vx_; y_ += vy_; } private: float x_, y_; float vx_, vy_; }; class RevealPlayer { public: float getX() { return x_; } float getY() { return y_; } float getVX() { return vx_; } float getVY() { return vy_; } void setX(float x) { x_ = x; } void setY(float y) { y_ = y; } private: float x_, y_; float vx_, vy_; };
変更点としてはメンバやメソッドの引数、戻り値をfloatに変えただけですが、RevealPlayer
では、呼び出しのコードも以下のように変更しなくてはなりません。
float px = player.getX(); float py = player.getY(); float pvx = player.getVX(); float pvy = player.getVY(); px += pvx; py += pvy; player.setX(px); player.setY(py);
このように、呼び出し側でも変数の型をfloat
に変えなくてはならないのです。もちろん、CapsulatePlayer
では、呼び出し側は何一つ変更しなくてもよいです。
この例では、変更点は比較的少ないですが、大規模なプログラミングになると、変更箇所の洗い出しから変更には莫大な時間を要するでしょう。
もう一つ、デメリットを挙げると、カプセル化が十分でないクラスは、利用者に対しての負担が大きくなります。実装の隠蔽が十分でない場合、呼び出す側がそのクラスをどのように使うのかを詳しく知らなければならなかったり、本来意図していないような処理が引き起こされてしまう恐れがあります。
上の例では、すでに述べている通り、呼び出す側がPlayerクラスが何をしたいクラスでそれをどのように実装するべきなのかを完全に理解していないと、正しく扱うことが出来ません。
//間違えた使い方 int px = player.getX(); int py = player.getY(); int pvx = player.getVX(); int pvy = player.getVY(); px += px + pvx; py += py + pvy; player.setX(px); player.setY(py);
例えば、上記のコードのように現在の座標に対し、速度の値を加算しようとして、現在の座標を余計に足してしまったとします。たとえ、意図しないミスであったとしても、このコードはコンパイルエラーにもならず、予想外の結果をもたらしデバッグを困難にします。
このように、内部実装が十分に隠蔽されず外に漏れている場合、クラスは働きは非常に不安定で難解なものになってしまいます。
複数人で行うような規模のプログラミングでは、これらのカプセル化していない場合のデメリットは、深刻な問題となり、生産性を著しく下げてしまいます。カプセル化が十分なされているクラスは、そういったデメリットがなく、使いやすく安全なのです。
わざわざ、オブジェクトという分かりやすい単位に分割して処理をする機構をもつオブジェクト指向言語での開発で、カプセル化を用いない手はないでしょう。カプセル化はオブジェクト指向の最も根底にある考え方ですので、プログラミングをする人間は、誰でも知っておいて損はないと思います。
委譲と継承
クラスに様々な機能を持たせる際に使われる方法として、委譲と継承があります。どちらもクラスに大きな拡張性をもたらしますが、一般的には継承よりも委譲を優先するべきだと言われています。それは何故でしょうか。
具体的な例を見ていきましょう。
これかし示す例は、RPGでいうところの所謂職業を表すコードです。RPGでは職業によって戦闘時の行動が異なります。その挙動を継承を用いて実装したものと委譲を用いて実装したものを順に紹介します。
//継承を用いた実装 #include <iostream> class Character { public: virtual void battle() { std::cout << "たたかいます!" << std::endl; } virtual void item() { std::cout << "手持ちのアイテムを使います!" << std::endl; } virtual void skill() = 0; protected: int MP = 100; }; class Magician : public Character{ public: void battle() { std::cout << "魔力をためます!" << std::endl; } void skill() { std::cout << "魔法を放ちます!" << std::endl; MP -= 10; } }; class Thief : public Character { public: void item() { std::cout << "ケチなのでアイテムを使わない!" << std::endl; } void skill() { std::cout << "アイテムを盗みます!" << std::endl; } }; class Takashi : public Magician { public: void battle() { std::cout << "杖を振り回す!" << std::endl; } }; class Kenta : public Magician, public Thief { public: void skill() { std::cout << "魔法でアイテムを盗みます!" << std::endl; Magician::MP -= 10; } }; int main() { Takashi takashi; Kenta kenta; takashi.battle(); takashi.item(); takashi.skill(); std::cout << std::endl; kenta.Magician::battle(); kenta.Thief::item(); kenta.skill(); return 0; }
//委譲を用いた実装 #include <iostream> class Character_impl { public: void battle() { std::cout << "たたかいます!" << std::endl; } void item() { std::cout << "手持ちのアイテムを使います!" << std::endl; } }; class Magician_impl { public: void battle() { std::cout << "魔力をためます!" << std::endl; } void item() { c_impl.item(); } void skill(int& MP) { std::cout << "魔法を放ちます!" << std::endl; MP -= 10; } private: Character_impl c_impl; }; class Thief_impl { public: void battle() { c_impl.battle(); } void item() { std::cout << "ケチなのでアイテムを使わない!" << std::endl; } void skill() { std::cout << "アイテムを盗みます!" << std::endl; } private: Character_impl c_impl; }; class Takashi { public: void battle() { std::cout << "杖を振り回す!" << std::endl; } void item() { m_impl.item(); } void skill() { m_impl.skill(MP); } private: Magician_impl m_impl; int MP; }; class Kenta { public: void battle() { m_impl.battle(); } void item() { t_impl.item(); } void skill() { std::cout << "魔法でアイテムを盗みます!" << std::endl; MP -= 10; } private: Magician_impl m_impl; Thief_impl t_impl; int MP; }; int main() { Takashi takashi; Kenta kenta; takashi.battle(); takashi.item(); takashi.skill(); std::cout << std::endl; kenta.battle(); kenta.item(); kenta.skill(); return 0; }
実行結果は、ぜひとも皆さん予想して、実行してみてください。
両方のコードを見比べてみて、実行結果が予想しやすかったのはどちらでしょうか?おそらく多くの人が、委譲での実装の方が予想しやすいのではないでしょうか。
継承を用いた方法だと、仮想関数やprotectedなメンバの影響で、どうしてもコードの可読性が失われてしまいます。対して、委譲では複雑なアクセスはあまり起こらないので、継承よりもコードが読みやすくなります。今回は2段階の継承でしたが、もっと層が浮かくなると可読性は指数関数的に下がっていきます。
もうひとつ、継承を用いた実装では、MP
の前や、呼び出し側(main関数)でのメソッド呼び出しの前に奇妙なコードが書かれているのに気付いたでしょうか。これが継承の代表的な副作用である、ダイヤモンド継承の現われです。
今回の例のケンタ(Kenta
)くんは、上図のようにCharacter
を継承したMagician
とThief
の両方を継承してしまっています。その結果、CharacterのメンバであるMP
やbattle()
、item()
メソッドが、曖昧になってしまいます。Magician
が継承したCharacter
のメンバなのか、それともThief
が継承したCharacter
のものなのかがコンパイラには理解が出来ないのです。
そこで、Mgician::
やThief::
を挿入して明示的に指定する必要があります。しかし、この方法では、MP
の値が同期されるわけもなく、ケンタくんは実質MPを20も持っていることになります。
対して、委譲を用いた実装では、Kenta
クラスは以下のように実装されています。
class Kenta { public: void battle() { m_impl.battle(); } void item() { t_impl.item(); } void skill() { std::cout << "魔法でアイテムを盗みます!" << std::endl; MP -= 10; } private: Magician_impl m_impl; Thief_impl t_impl; int MP; };
このように、もともとメンバとしてそれぞれの職業の機能を持っているので、自然な形でメソッドを呼ぶ段階で明示的に呼び出されるクラスが決定されます。
このように、委譲は継承に比べてより分かりやすい実装を行うことが出来ます。
実際にはそれ以外にも、継承では基底クラスへ変更が強制的に派生クラスにも波及してしまうというデメリットがあります。委譲では、クラス同士の結びつきが継承よりも薄いので、それぞれのクラスの変更が他のクラスに影響しにくくなっています。
このようなことから、委譲は継承よりも優先される(ことが多い)のです。
その他のキーワード : デメテルの法則、SOLID原則の思想