はかせのラボ

私の頭の中を書いていく雑記ブログです

プログラミング 継承とかインターフェースとかコンポジションとか

あいさつ

どうも、はかせです。
今回は設計の話で継承とかその辺のクラス間のかかわりについての
私の意見や言われたことを書いていきます。

継承

あるクラスの特性を他のクラスに引き継がせることです。

class Hoge 
{
public:
	int hogeNum;
};

class HogeHoge :public Hoge
{
	//書いていないがhogeNumが使える
};

共通のものがあるなら親にまとめて子の記述量を減らすことができます。

インターフェース

クラスに対して実装を強制します。

class IHoge 
{
public:
	virtual int GetHogeNum() = 0;
};

class HogeHoge :public IHoge
{
	//実装しないとコンパイルエラーが出る
	virtual int GetHogeNum() override {};
};

実装の強制をするためインターフェース経由でメソッドの呼び出しが
できます。

また実装しなければエラーになるため
実装忘れを防ぐことができます。

基本的に実装はインターフェース実装先が行い
インターフェースでは行いません。

コンポジション

あるクラスが他のクラスを所有することです。

class Hoge 
{
public:
	int hogeNum;
};

class HogeHoge
{
	//Hogeクラスのポインタを所有
	Hoge* mHoge;
};

HogeHogeはHogeクラスのポインタを持ち
Hogeクラスの機能などを使えるようになります。

基本的にコンポジションで使うクラスは必要最小限の単機能の実装のみを行います。

どれを使うのが良いのか

※これから書くことは今の私の考えです

基本的にはコンポジションで機能拡張を行い、
他のクラスとのやり取りはインターフェースを
介して行うのがいいと思っています。
そのどれでも解決できない場合は継承を使うという感じですかね。
継承はあまり使いたくないです。

こういった話の時大事なのは
責任と影響だと思っています。

責任と影響とは

責任というのは
あるクラスが持つ役割のこと、
影響というのは
あるクラスと関わりのあるクラスのことです。

この責任や影響が不明瞭だとあるクラスの機能を修正したとき
色んな所を2度3度と修正する必要が生まれる可能性があります

継承をあまり使いたくない理由

責任と影響が不明確になりやすい

開発が進むと度重なる仕様の変更や追加が発生します。
そうすると時間や状況によっては実装を最優先し
親クラスにどんどんメソッドや変数を追加していきます

そうすると段々親クラスが行うことが増えてきて
責任と影響が不明確になっていきます
親クラスの責任と影響が不明確ということは
継承している子クラスも責任と影響が不明確になるということでもあります。

機能の拡張・修正がしにくい

責任と影響が不明確で迂闊に触れないということは
何か機能を追加したり逆に修正したりすることが難しくなるということです。
(変更によって何が起こるかわかりにくいため)

おまけにこういったクラスは
意図しない使い方や危ない変更を加えたとしても
コンパイルが通ってしまうことがあります。
プログラムの文法や型などは問題ないからコンパイル時にはエラーとしてでないからですね。

そしてランタイム中にエラーを起こして
アプリクラッシュなどを起こすわけですね。

スパゲッティコード化しやすい

機能がどんどん追加されるということは
それだけコードも増えていくということです。

責任や影響が不明確なため
処理に一貫性がなく
なぜこのクラスにこんな機能が?
みたいことが頻発します。

その状態で数千から下手したら万まで行数がいく
コードを読まなければいけないわけです。
(ゴールのないマラソンに等しいと思っています)

何故インターフェースやコンポジションを使うのか

理由は
①責任と影響を明確にする
②機能拡張・修正をしやすくする
スパゲッティコードになるのを防ぐ

という感じです。
順に説明していきますね。

責任と影響を明確にする

例えば

//四則演算を管理するクラス
class Arithmetic
{
public:
	int Add(int addnum1,int addnum2) 
	{ 
		return addnum1 + addnum2; 
	}
	int Sub(int subnum1,int subnum2) 
	{ 
		return subnum1 - subnum2; 
	}
	int Mul(int mulnum1,int mulnum2) 
	{ 
		return mulnum1 * mulnum2; 
	}
	int Div(int divnum1,int divnum2) 
	{ 
		return divnum1 / divnum2; 
	}
};

class Hoge
{
	//Arithmeticクラスのポインタを所有
	Arithmetic* mHoge;
};

こんな感じになっていたとしたら
四則演算に関してはArithmeticクラスを弄れば変えることが出来そうです。
そして四則演算を行いたいときはこのクラスをコンポジションすればよさそうです。

このクラスは四則演算にしか干渉していないので
四則演算以外のバグや異変に関してこのクラスが一切関係がないこともわかります。

インターフェースも

//四則演算メソッドの実装のみを強制する
class IArithmetic
{
	virtual int Add(int addnum1, int addnum2) = 0;
	virtual int Sub(int subnum1, int subnum2) = 0;
	virtual int Mul(int mulnum1, int mulnum2) = 0;
	virtual int Div(int divnum1, int divnum2) = 0;
};

こんな感じで責任を明確にできます。

各四則演算メソッドの中身はインターフェース実装先で
実装するので変わった四則演算を作りたくなったら
このインターフェースを弄るのではなく
このインターフェースを実装したクラスで弄るだけで済みます。

インターフェースはそれ自体が型として持てるので

class HogeHoge
{
private:
	IArithmetic* mArithmetic;
};

こんな感じで持ってあげることで変更も容易になります。

機能拡張・修正をしやすくする

コンポジションは基本単機能の実装のみを行います。
なのであるクラスに機能を追加したければ
追加したい単機能を実装したクラスをコンポジションするだけ
追加することが出来ます。

あくまで単機能クラスを持つだけなので
他クラスへの影響を最小限にすることが出来ます。

インターフェースの場合は実装の強制を行います。
インターフェースを使ってシステムを抽象的に作成して
実体は実装先で作ると変更に強いとされます。

これはインターフェースによってデータの
入力と出力が固定されるからです。
インターフェースの縛りを無視した変更などは
コンパイルエラーとなるためミスにも気づきやすいです。

例え実装がどうなっていようとインターフェース実装を行い
固定された通りの入力と出力をすることで
システムが破綻することなく安全に機能拡張ができます。

スパゲッティコード化を防ぐ

何度も書いていますが
コンポジションは単機能のみを実装したクラスを作り、
インターフェースは機能実装の強制のみをし実装はしません。
なのでこれらがスパゲッティコードになることは考えにくいです。

結論

長々と書いてきましたが
私が継承をあまり使わず
インターフェースやコンポジションがいいと思っている理由は
安全に機能の拡張・修正がそのコードを書いた人以外でもしやすいからです。

今回上げた様な問題が起こらないような設計や組み方をするならば
継承を使うのは大して問題にならないと思っています。
実際問題それが難しいと感じているので
あまり使いたくないって感じですね。

あとがき

今回は継承とかの是非についてでした。
今回書いた考えは私が自分でやってみて思ったり
ネットで見聞きしたものがメインとなります。

なので間違いや私は理解しているが
この記事を読んだ方はイマイチわからないなんてことがあるかもしれません。
その時はコメントなりTwitterなりで言って下さるとうれしいです。

それでは今回はこの辺でノシ