オブジェクト指向の一側面: メッセージ送信と動的結合

オブジェクト指向という言葉は、使う人によって(あるいはどの点を重視して話すかによって)意味する内容が相当違ってくるので無用な混乱をまねきがち。その上「これこそがオブジェクト指向の本質だ」とか言い出すと、さらに余計な混乱を呼んだりする。
ここで取るのは「オブジェクト指向(というかオブジェクト指向言語の機能)で本質的なのは動的結合だ」という考え方。「本質」といっても、もちろん一面から見た場合のことにすぎない。
動的結合が本質だ、重要だと言ってみても、それだけでは動的結合がどう重要でどう使えばいいのかはさっぱりわからない。ちょうど「手続き・関数・モジュールに分割する機能は、プログラミング言語で重要だ」と言ってみても、それだけではどう重要でどう使えばいいのかわからないというのに似ている。
そこで動的結合の重要性の説明になる考え方として「メッセージ送信」を取り上げる。「ダックタイピング」も動的結合の重要さの説明に使えるけど、ここでは取り上げない。

メッセージ送信

Smalltalkに由来するオブジェクト指向についての由緒正しい考え方だけど、(オブジェクト指向自体の普及度に比べて)たぶんあまり普及していない見方でもある。「コンピュータはデータを処理する機械」というあたりまえの見方とは逆の見方をしているとも言える。

  • 「コンピュータがデータを処理する」という見方では、データの外に動作の主体(プロセッサとか)があって、データは処理をされる受身の立場になる。
  • 一方メッセージ送信の見方だと、処理をおこなうのはデータ(=オブジェクト)自身であり、データを使う側は単にメッセージを送るだけ(処理を依頼するだけ)になる。

この差は、機能分割・モジュール分割のやり方に影響を与える。

  • 「データを処理する」という見方を素朴に機能分割に適用すると、どう処理するのかという観点からプログラムを分割しがちになる。例えば「入力モジュール」「分析モジュール」「処理モジュール」「出力モジュール」とか。
  • メッセージ送信の見方では、データ(オブジェクト)の方が分割の基準になる。ただしデータを基準にモジュール分割するといってもデータの構造とかフォーマットとかの詳細を考えるのではなく、「どんなメッセージを受け取ってどんな反応をおこなうか」というもう少し抽象的な観点からデータを見ることになる。「メッセージを受け取って自分で処理をおこなう」とみなせるものが一つのオブジェクトになり、モジュール分割をするときにひとつの単位になる(なお多くのオブジェクト指向言語における「クラス」は、このモジュール分割の単位の役割と、データの型の役割という両方の役割を持っている)。そしてこのモジュールはプログラムの他の部分に対して「メッセージを受け取る」という緩く疎な繋がりだけを持つことになる。

でも「メッセージ送信」と言われても、実際のプログラムと結びつく感じがあまりしない。メッセージ送信の見方に基づいて、どうやってプログラムを書けばいいのか。
まず、関数呼び出し

f(a, b, c, ...)

をオブジェクトaへの、fというメッセージだと読み替える。残りの引数b、c、...はメッセージの引数と考える。そう読み替えた上で、f(a, b, c, ...)が「aへのメッセージ送信」だと見なせるようにプログラムを書く。

  • メッセージを送る側(オブジェクトを利用する側)は、オブジェクトの内部構造を直接いじったりせずに、「処理を依頼するかのように」プログラムを書く。
    • オブジェクトは、実際には配列だったり構造体(レコード型)だったりするけど、そうしたオブジェクトの内部構造のことは意識しない。
    • オブジェクトを利用する場合は、内部構造を直接いじらずに、オブジェクト側が用意したメッセージ関数f(a, ...)、g(a, ...)……を呼び出して処理を依頼する。
  • オブジェクト側は、メッセージ関数f(a, ...)、g(a, ...)……を実装する。
    • オブジェクトの内部構造を直接いじってよいのはメッセージ関数の中だけに限る(オブジェクト内部を意識するのは、オブジェクト自身だけ)。
    • 実装言語がモジュール化機能を持っているなら、オブジェクトのデータ構造の定義と、メッセージ関数の定義を一つのモジュールにまとめる。可能なら、データ構造の具体的な詳細はモジュール外部には見えないようにする。

これを見てわかるように、メッセージ送信という見方はモジュール化についての処方箋にもなっている。
もちろん万能な処方箋ではない。たとえば文字列に対する様々な操作のどれだけを文字列自身が処理するのかは、メッセージ送信の見方だけからは出てこない。オブジェクト自身に多くのことをやらせるのが良いかもしれないし、何でもかんでもオブジェクト自身に処理させるのではなく、手続き・操作的な見方と組み合わせて、外部から文字列の操作・処理をする別のオブジェクトとか文字列処理を集めたモジュールとかを作った方が良いかもしれない。
それから「使う側は用意された関数(=メッセージ)を使うだけで、内部構造のことは意識しない」というメッセージ送信的なプログラムの書き方は、抽象データ型と同じことをしているともいえる。
でも抽象データ型というと、スタックとかツリーとか有理数複素数みたいな汎用的なデータという印象を受けやすい(気がする)から、「抽象データ型を活用してプログラムを書け」よりも「メッセージ送信の見方でプログラムを書け」の方が適用場面が広くなった感じがしてプログラムを書くときの指針にはしやすいと思う。

動的結合

抽象データ型との類似に触れたので、抽象データ型の例にもよく出てくるスタックを例にとる。
メッセージ送信の見方では、スタックを使うというのは、スタックオブジェクトに「これをプッシュして」とか「ポップして(ポップしたものをちょうだい)」といったメッセージをスタックに送ることに等しい。送られたメッセージに対して実際にどのような内部処理をおこなうのかは、スタック側が勝手に決める。メッセージを送る側(スタックを使う側)にとっては、相手がスタックとして適切に振る舞ってくれることが重要であって、内部構造や処理の詳細は気にしなくてもよい。
したがって使う側は、スタックがどのように実装されているのか(配列とインデックスで実装されているのか、リストで実装されているのか、別の実装なのか)については気にせずに、スタックを使うことができる。この利点は抽象データ型でも強調される。でも、本当にそうなのか。
例えば、配列で実装されたスタックとリストで実装されたスタックの両方が使われていて、どちらのスタックも扱えるコードを書こうとしてみる。メッセージ送信の見方では、どんな処理をおこなうかはメッセージを受け取った側が自分で決めるのだから、スタックを使う側は相手がどちらの種類のスタックなのか気にせずに「ポップして」「プッシュして」とメッセージを送ればよい。
でも抽象データ型的なプログラミングや上で書いたようなメッセージを関数呼び出しに読み替えるやり方では、配列スタックとリストスタックで異なる関数を用意して相手に応じて適切な方の関数を呼び出すとか、タグ付けしてcase文なんかで処理を分岐させたりする必要がでてくる。結局、実装の詳細のことは気にしなくてよいにも関わらず、それらが異なる実装だということは気にしてプログラムを書かなければいけない。しかも新たな実装が追加される場合、その都度、処理の分岐の手直しをしなければならない。
これは、関数呼び出しをメッセージ送信に読み替えるだけでは不十分だということでもある。これを補うには、関数呼び出しの際に第一引数の種類(型など)に応じて適切な関数を自動選択するような仕組みを追加してやれば良い。このような仕組みを動的結合と呼ぶ。動的結合をサポートしていないプログラミング言語の多くでは、動的結合の仕組みを言語内で実現するのは面倒だったり不自然だったりするので、動的結合の機能を十分に利用するには言語自体の変更・拡張が必要になるかもしれない。

動的結合とメッセージ送信とのズレ

しかし「種類によって処理を自動選択する」という動的結合の機能を主にして考えると、動的結合とメッセージ送信の見方がうまく合わない場合もある。
単一のオブジェクトではなく複数のオブジェクトの種類に応じて処理を変えたいという状況を考える。言語機能的には、第一引数だけでなく第二引数、第三引数等に渡されるオブジェクトの種類も考慮して動的結合をおこなうようにすれば解決できる(マルチメソッドとか多重ディスパッチとか呼ばれる機能)。
でもこれは「どう処理するかはメッセージを受けた側が決める」というメッセージ送信の見方とはズレがある。メッセージを送信するという見方をあえてするなら、あるオブジェクトにメッセージを送るのではなく、オブジェクトの組(タプル)にメッセージを送り、オブジェクトの組が処理を決める、といった見方になる。また、メッセージ送信的な見方では自然なオブジェクトの種類(クラス)をモジュール化の単位に取るというやり方も、マルチメソッドが必要な状況では必ずしも良い方法とはいえない。
Vistorパターンを使えば多重ディスパッチを実現できるから、あくまでメッセージ送信的な見方にとどまることも一応できるけど、やっぱり何か不自然な感じがする。逆に「多重ディスパッチがオブジェクト指向的に不自然なんだ」という考え方もあるかもしれないけど。

動的結合以外のオブジェクト指向言語機能について

データ隠蔽(カプセル化)

データ隠蔽機能、もっと広くいえばモジュール化機能は、もちろん重要だと思うけど、オブジェクト指向機能として本当に必須のものなのかよくわからない。オブジェクト指向の機能とは別枠のモジュール化機能でも良い気もする。
実装とインターフェイスの分離の観点から、データ隠蔽機能と動的結合機能を比較すると、

  • データ隠蔽機能は、実装を隠すことによって外部インターフェイスとの分離をうながす。
  • 動的結合機能は、呼び出し時になるまでどの実装を実行するかの決定を遅らせることによって、インターフェイスとの分離をうながす。

みたいなことが言えると思う。

継承(実装の継承)

メッセージ送信、あるいは動的結合を中心にしてオブジェクト指向言語を見ると、実装継承機能は副次的なもので、オブジェクト指向言語にとって最重要の機能ではないように見える。せいぜい、外側から見て同じメッセージを受け取って同じように振る舞うオブジェクトは実は内部構造も似ている場合も多くてそういう場合は実装を受け継いで拡張・変更できると便利だ、というぐらいの関係性のような気がする。


(cf. 「プログラミング言語の基礎知識」)