オブジェクト指向とは

これは、C++におけるオブジェクト指向についての私見である。

カプセル化

オブジェクト指向の本でよく出てくるのは、

	オブジェクト = データ + メソッド
という式であろう。確かに、オブジェクト指向の 考え方はこの式から始まる。これは、データと それを操作する関数を1つのくくりとしてまとめよう、 というものである。

メソッドはSmallTalkで使われていた言葉であるが、 C++の本でもよく使われるようになった。
同じような意味で、インターフェースという言葉も使う。 要するに、オブジェクト(クラス)が外界に対し公開している、 切り口ということである。

Cでは、structの中身はメンバ変数だけであった。
C++では、struct(またはclass)の中身は、メンバ変数と メンバ関数の2つを使うことができる。(他にも、staticメンバ変数 とか、staticメンバ関数とか、列挙型とか入れ子クラスなども使えるが、 ここでは、とりあえず省略する。)

これによって、C++では、データ(メンバ変数)と メソッド(メンバ関数)を1つのクラス(structかclass)で まとめることができるようになった。
(ちなみに、structとclassの 違いは、デフォルトがpublicかprivateかだけの違いであって、 本質的に違いはない。個人的には、classというキーワードは 不必要であるように思う。何故なら、クラス定義では、良心的には 次のようにpublicなものから書き始める。(privateは隠したいものだから後にする。)

	class Sample {
	public:
		void SomeFunc();
		...
この書き方は、次のように書くのと(殆ど)変わりない。
	struct Sample {
		void SomeFunc();
		...
しかし、既に習慣から、classを使う方が圧倒的に多い。)

このように関連するものを1個所にまとめることが、 カプセル化であり、 これだけでも、プログラムの分かりやすさや保守性は格段に違ってくる。

では、オブジェクト指向とは、カプセル化だけであるかというと、 そうではない。カプセル化は、次の抽象化につながるための 入り口に過ぎない。抽象レベルのプログラミングこそ オブジェクト指向の醍醐味であるといってよい。

本来であれば、次の抽象化に進む前に、「継承」であるとか「仮想関数」とか 「多態」などという概念を覚えなくてはいけないのだが、 ここでは省略する。


抽象化

抽象レベルのプログラミングで大事なのは、その クラスが規定しているインターフェースである。 もはや、データは大事ではなく気にすべきでもない。
(つまり、最初に出てきた式を、ここで思い出してもしょうがない。 データを気にすべきところは、抽象レベルではなく、 もっと実装に近いところである。)

クラスを洗練されたものとするには、必要にして最小限の インターフェースを定義しなければならない。 (データについては、定義される場所は、いろいろなクラスになることもある。 よって、クラスライブラリを使う側では意識しないでもよかったりする。)

さて、具体的なサンプルを見てみよう。
シミュレーションでよく出てくる、

	「ある時刻にあるメソッドを実行するように登録する」
ということを実現することを考えてみる。

ここで、大事な作業が、

	登録されるクラスは「Event」というクラスであり、
	実行されるメソッドは「virtual void Do()」というメンバ関数である
という、インターフェースを 定義することである。
このようにすると、「登録する」という関数が、
	ScheduleAt(double time,Event* pEve);
というように決めることができる。
一見、これでは使い方が限定されており、不便と思われるかもしれない。 いやいや、ちょっと別のものを用意するだけで、いろいろなものに 対して使えるのである。

例えば、


	struct Signal {
		void ChangeLight(bool bIsBlue);
	};
というクラスのChangeLightを登録したいとしよう。
クラスやメソッドも違うし、メソッドの引数さえも違っているが、大丈夫。

ここで、


	template<class T,class Arg>
	struct Event2 : public Event {
		T*	m_p;
		void	(T::*m_f)(Arg);
		Arg	m_a;
		Event2(T* p,void (T::*f)(Arg),Arg a)
		 : m_p(p), m_f(f), m_a(a) {}
		void Do(){(p->*f)(a); delete this;}
	};
というテンプレートクラスを用意してみよう。
こうすると、
	ScheduleAt(10.,new Event2<Signal,bool>(pSig,Siganl::ChangeLight,true));
として使えるのである。もちろん、テンプレートであるから、 Siganalクラスに限らずに使うことができる。
このように、元のものに対し外観を少し変えて包むものを ラッパーという。今回のようにテンプレートのラッパークラスは 時として非常に便利である。

先ほどの書き方が煩雑すぎて気に食わないかもしれない。
そのときは、さらに次のテンプレート関数を用意しよう。

	template <class T,class Arg>
	void ScheduleAt(double d,T* p,void (T::*f)(Arg),Arg a) {
		ScheduleAt(d,new Event2<T,Arg>(p,f,a));
	}
こうすれば、

	ScheduleAt(10.,pSig,Siganl::ChangeLight,true);
というように非常にシンプルに書ける。実行効率も前と変わらない。
また、同じようなテンプレートクラスを用意すれば、 非メンバ関数なども「登録する」ことができる。

もちろん、いいことばかりではない。間にラッパーを はさむとそれだけ実行効率が悪くなる。しかし、抽象化 プログラミングでは、こればかりは仕方がない。

このように、いろいろなものに対して「登録」できるライブラリを 提供することができる。従って、「登録する」という言葉が、 抽象的な意味で使うことができる。
これは、最初にEventというクラスと Doというインターフェースを決め、そのインターフェースを 利用したラッパーを用意したためである。

つまり、「登録する」という機能を 抽象的に(あいまいにとか、いろいろなもので) 提供しているのは、インターフェースをかっちりと決めることから 来ているのである。
(このように、いろいろなものによる振る舞いの違いを 実現しているのは、仮想関数の機能によっている。 また、仮想関数はポインタを通して使わなければ 意味をなさないことにも注意が必要である。)

ところで、データがどこにあるか気にする必要がないという感覚が つかめたであろうか。例えば、ChangeLightのtrueという引数は、 ラッパーの中のデータになっていた。また、このラッパークラスは ライブラリを使うユーザには、意識しないでもよい作りになっている。 つまり、ユーザはtrueがどのクラスにデータとして持たれているのかは 見えてないのである。


補足

オブジェクト(あるいはクラス)には、 インターフェースとデータの2つの面がある。 しかし、C++の継承は両方、いっぺんにしかできない。 (その点、Javaは別々にできる。) この点が、C++での設計をいらただしいものにさせるが、 インターフェースだけのクラスを用意することによって ある程度は対処できる。

さらに付け加えておくことがある。それは、

	「洗練されたクラス設計を行うことは非常に困難である」
ということである。私の意見としては、 「クラスライブラリの設計者」と「クラスライブラリの利用者」は、 分業をすべきだと思う。(誰でもクラス設計すべきではないということ。)
また、プログラマの大半は利用者ということになると思われる。 実際のところ、洗練されるには、たくさん使われるのが不可欠かもしれない。

余談

再褐

	オブジェクト = データ + メソッド
こう書くと、「データ」はプリミティブ(intとかcharとか)を 表しているように見えるかもしれない。もちろん、そんなことはなく 「データ」はオブジェクトであってよい。そうすると、 この式で言う「データ」という言葉は不適切なのかもしれない。では、

	オブジェクト = オブジェクト + メソッド
こう書くべきか?いや、これでは、何を言いたいのか判らないだろう。
私なりに書けば、こんな所か。

	オブジェクト は 「内部的情報」と「インターフェース」から成る
	「内部的情報」はオブジェクトのこともある。
もちろん、「内部的情報」と「インターフェース」の両方とも、必ず必要なわけでもないのだが。 (C++では空っぽでもsizeofは1になるけど。) もう1つ、気になるのは、「C++でintはオブジェクトかどうか?」という ことであろうか。これは、はっきり断言することはできないが、 オブジェクトといってもよいのではないのだろうか。例えば、1+2という計算では、 「1」の「+」というインターフェースを呼んでいるようにも受け取れる。

ところで、「インターフェース」には、次のような種類がある。

最後のものは、派生して再定義することが前提となっているといえる。 (つまり、仮想関数であるべき。) この最後のやつを「イベント」とし、残りを「メッセージ」として、

	オブジェクト = プロパティ + メッセージ + イベント
というのもありそうである。

戻る