オブジェクト = データ + メソッドという式であろう。確かに、オブジェクト指向の 考え方はこの式から始まる。これは、データと それを操作する関数を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がどのクラスにデータとして持たれているのかは 見えてないのである。
さらに付け加えておくことがある。それは、
「洗練されたクラス設計を行うことは非常に困難である」ということである。私の意見としては、 「クラスライブラリの設計者」と「クラスライブラリの利用者」は、 分業をすべきだと思う。(誰でもクラス設計すべきではないということ。)
オブジェクト = データ + メソッドこう書くと、「データ」はプリミティブ(intとかcharとか)を 表しているように見えるかもしれない。もちろん、そんなことはなく 「データ」はオブジェクトであってよい。そうすると、 この式で言う「データ」という言葉は不適切なのかもしれない。では、
オブジェクト = オブジェクト + メソッドこう書くべきか?いや、これでは、何を言いたいのか判らないだろう。
オブジェクト は 「内部的情報」と「インターフェース」から成る 「内部的情報」はオブジェクトのこともある。もちろん、「内部的情報」と「インターフェース」の両方とも、必ず必要なわけでもないのだが。 (C++では空っぽでもsizeofは1になるけど。) もう1つ、気になるのは、「C++でintはオブジェクトかどうか?」という ことであろうか。これは、はっきり断言することはできないが、 オブジェクトといってもよいのではないのだろうか。例えば、1+2という計算では、 「1」の「+」というインターフェースを呼んでいるようにも受け取れる。
ところで、「インターフェース」には、次のような種類がある。
オブジェクト = プロパティ + メッセージ + イベントというのもありそうである。