なぜわからなくなってしまうのか?

いい加減、わけのわからない「たとえ話」はやめよう

オブジェクト指向の入門書では、 「謎のたとえ話」から始まるものが多いように思います。

典型的な例は、はじめにでも挙げた、 「哺乳類を継承して犬と猫を作り、 『鳴く』というメッセージを送ると犬なら『わん』、猫なら『にゃあ』と鳴く」 って奴でしょう。 他にも、ちょっとWebをぐるぐるすると、清原選手をオブジェクトにしてみたり、 箪笥をオブジェクトにしてみたりなどなど、 およそプログラミングとはかけ離れた説明が蔓延しています。

こんな説明を読んで、なんだかわかったような気分になれる人は、 どっちかというと思考力に欠ける人なんじゃないかと思います。 「わけわからん」という反応のほうが技術屋としては正常でしょう。

いい加減、こういうわけのわからないたとえ話はやめたらどうかと。

あんなもん、わかったつもりの半可通と、 理解できない挫折組を生み出すだけではないかと。

そして… おそらくこの点については、私も罪を負っていると思います。 「Java謎+落とし穴…」では、クラスとインスタンスの説明として、 「タイヤキ型とタイヤキ」というたとえ話を出していますが、 こういうたとえ話って、「わかっている人」はうんうんと頷くでしょうが、 「わかっていない人」には混乱の元にしかならないのでは… と、今になって思っています。

現実世界をそのままモデリング?

オブジェクト指向設計の説明として、 「現実世界の『モノ』をオブジェクトとし、 現実世界をそのままモデリングすればよい」 という説明も蔓延しています。

これも嘘でしょう。 確かに、オブジェクト指向設計には 「自然なモデリング」と「不自然なモデリング」が存在しますが、 「自然なモデリング」が必ずしも「現実世界をそのまま」 表現しているわけではありません(実例は後ほど示します)。

また、仕様書から名詞を拾ったり、 「このオブジェクトはこのオブジェクトに対してこういう操作をするから、 関連で結ぶ」といった作業も、少なくとも初心者にとって、 モデリングの助けになるとは思えません。 むしろこういう説明は、

この説明に沿って機械的に作業していけば、 「オブジェクト指向設計」ができるんだ。

という印象を与えかねないという点で有害だと思います。

手元に「憂鬱なプログラマのためのオブジェクト指向開発講座」 という本があるんですが、この本、 例題としてインベーダーゲームを取り上げています。

この本によれば、仕様から「名詞」を抽出し、それに対する「操作」を抽出して、 あるクラスAがあるクラスBに対して「操作」を行うのであれば、 AからBに「関連」を付けるのだそうです。そして、「関連」はポインタで表現します。 関連先が複数の場合は、ポインタの配列を使えばよいそうです。

まあ、大筋で間違ってはいないでしょうが、例題として、 インベーダーゲームで、 ビーム砲※1 の発射したビームからインベーダーに関連を張ってしまっているのは 明らかに変でしょう。

ビームが発射された瞬間、55匹のインベーダーに対してポインタを張るのでしょうか? もし、UFOが出現したり、別のビームがインベーダーを破壊したら ※2、 そのポインタ配列をいちいちメンテナンスするのでしょうか?

まあ、この本には「一時的な関連」という説明もありますが(p.109)、 p.143には 「プログラムコード上ではミサイルクラスはそのメンバとして インベーダークラスへのポインタを「持っている」必要はあります」と明記してあるし… ※3

このように、仕様書にある名詞や動作から、機械的にクラス設計を行っても、 おそらくろくな設計にはなりません。

確かに、 ソフト屋は常に一定以上の品質のプログラムを供給しなければならないものであり、 その意味では、「機械的にやっていけば誰でもちゃんとした設計ができる」 という手法があるのならば大変結構なことです。しかし、残念ながら、 オブジェクト指向設計はそこまでいっていないと私は思っています ※4

オブジェクトに「メッセージ」を送って…

オブジェクト指向をわかりにくいものにしている最大の要因として、 「メッセージ」という言葉が挙げられます。

オブジェクト指向の入門書を読むと、 「オブジェクト指向では、オブジェクト同士が相互にメッセージを送ることにより 協調動作しながら動作する」などと書かれています。

こんな説明を読むと、まるで各オブジェクトにそれぞれプロセスが 割り当てられていて、プロセス間通信で動作するかのように思えますが、 実際にはそんなことはありません。 「メッセージ送出」と呼ばれているのは、 たいていのオブジェクト指向言語では ※5 実のところ一種の関数呼び出しです ※6。 こんなものに「メッセージ」などという大仰な名前を付けていることが、 結果としてオブジェクト指向の理解を妨げていると思います。

オブジェクト指向で再利用性は高まるか?

オブジェクト指向のメリットとして、プログラムの再利用性が高まる、 という点がよく挙げられます。

しかし、一度書いたプログラムを再利用したいなら、 関数(サブルーチン)にすればよいのでは?と、 ベテランプログラマであれば思うことでしょう。 オブジェクト指向における再利用と、関数による再利用との差を説明できない限り、 「オブジェクト指向で再利用性が高まる」と言われても説得力がありません。

実際、オブジェクト指向などというものが普及するはるか以前から、 それこそどんなプログラミング言語であっても、 「ライブラリ」という形で、再利用可能なプログラム(関数)群が提供されていたわけです。 「ライブラリ」は、プログラムの部品として使うことができ、 生産性の向上に大いに役立ちました。 そこに、「オブジェクト指向でプログラムを部品化することで再利用性が高まる」 なんて言われても、「ハァ? 今更何言ってるの?」としか思えないでしょう。

ただ、私は、オブジェクト指向によりプログラムの再利用性は高まると思っています。 オブジェクト指向の「マルチプルインスタンス」という特性が、 「状態」を持つ部品を、プログラムのあちこちで使いまわすことを可能にしているからです。 この点についてはこちらで後述します。

それは「カプセル化」なのか?

オブジェクト指向の特徴として真っ先に挙げられるのが「カプセル化」です。

しかし、上で書いたような「ライブラリ」でも、 そのライブラリの中でしか参照しない変数や、下請け的な仕事をする関数は、 使用者に対しては公開しないはずです。 「使用者に公開する必要のない変数や関数を隠すこと」をカプセル化と呼ぶのなら、 これだって立派なカプセル化です。 つまり、カプセル化は、別にオブジェクト指向に固有の概念ではありません。

さらに、オブジェクト指向の入門書では、これまた機械的に 「こうすればカプセル化できる」という指針として、 「データフィールドは常にprivateにしろ」と主張しているものが多いものです。

確かに、多くの場合、データフィールドはprivateにした方がよいと思います。 しかし、「データフィールドは常にprivateにして、 それに対してpublicなgetXXX(), setXXX()というメソッドを作る」 なんてことを機械的にやってたら、 結局データフィールドをpublicにしているのと同じことです。 publicなsetXXX()メソッドを書いた時点で、 そのデータは外部から自由に書き換え可能になっているのですから。

プログラムを書く上では、Cにおける構造体と変わらない使い方をするクラスが 登場するはずです。 たとえば、2次元の座標を表現するPointクラスといったものが考えられます。

class Point {
    int x;
    int y;
}

このPointクラスのx, yをprivateにして、 publicなgetX(), getY(), setX(), setY()を付けることに、 いったいどれほどの価値があるでしょうか。

class Point {
    private int x;
    private int y;
    public int getX() {
        return x;    
    }
    public int getY() {
        return y;
    }
    public void setX(int x) {
        this.x = x;
    }
    public void setY(int y) {
        this.y = y;
    }
}

こんなメソッドをせっせと書いたって、setX()とsetY()がpublicになっている以上、 カプセル化にも何にもなっていません。 Pointを、単なる構造体的な「データを保持するためのクラス」と位置づけるのであれば、 xとyを最初からpublicにした方がまだマシというものでしょう ※7

カプセル化とは、「実装詳細の隠蔽」のことです。 外部に対しては必要最低限のインターフェースのみを公開し、 それ以外は隠蔽することで、実装の変更を可能にしたり、 そのモジュールを他でも使いまわすことを可能にする、という概念です。 データメンバに機械的にgetXXX(), setXXX()を付けるのがカプセル化ではありません。

ところで「Javaの格言」という本では、 Pointのx, yをprivateにするメリットとして、 マルチスレッド時の排他制御を挙げています。 しかし、getX(), getY(), setX(), setY() を作ってそれぞれをsynchronizedにしたところで、 この仕様ではマルチスレッドには対応できません。 ちょっと考えてみればわかるように、 getとsetが分かれていたり、setのXとYが分かれていたりしていたのでは、 複数のスレッドで実行されたら結局変なことが起きます。

継承について

はじめに」にて、 「継承やポリモルフィズムは、必ずしも最初に覚えなければならないことではありません」 と述べました。

なのでここでことさら継承について語るつもりはないのですが、 ひとつだけ言っておきたいことがあります。 継承は、ソースの再利用の手段ではないということです。

継承は、ずいぶん長い間「プログラムの再利用のための機能である」と誤解されてきました。 上でも挙げた「憂鬱なプログラマのためのオブジェクト指向開発講座」では、 6章の章タイトルが「継承---プログラムを『作らない』ためのテクニック---」と なっています。

しかし、実際問題、継承をプログラムの再利用のために使うべき局面は そうそうないですし、使ってみてもろくな結果になりません。 実際、「憂鬱な〜」では、「文字列」クラスを継承して 「末尾スペース除去可能文字列」なるものを作っていますが、 こんなクラス、作ったからといって何の役にも立ちません。 たとえばTextFieldというGUIコンポーネントがあったとすれば、 そこから取得できる文字列は依然として普通の「文字列」であるはずで、 その文字列からは末尾のスペースを除去できないからです。

文字列末尾のスペースを除去したいのであれば、 そのような関数(Javaならstaticメソッド)を別途作れば済む話です。 継承では解決になりません。

まとめ



ひとつ上のページに戻る | ひとつ前のページ | ひとつ後のページ | トップページに戻る
mailto:PXU00211@nifty.ne.jp

このページに対してご意見・ご質問・ご感想等をいただいた場合、 公開することがあります。