オブジェクト指向とは

これは、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」の「+」というインターフェースを呼んでいるようにも受け取れる。

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

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

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

戻る