Go to list of users who liked
More than 1 year has passed since last update.
新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡
あわせて読みたい
- 新人プログラマに知ってもらいたいメソッドを読みやすく維持するいくつかの原則
- ペアプログラミングして気がついた新人プログラマの成長を阻害する悪習
- 「オブジェクト指向プログラミング」と「関数型プログラミング」のたった一つのシンプルな違い
- あきらめるにはまだ早い!ソースコードの品質向上に効果的なアプローチ
- 2015年に備えて知っておきたいリアクティブアーキテクチャの潮流
この記事について
この記事は新人向けの研修内容を再編集してお送りいたします。
ここで述べる内容はどのようにして現在のプログラミングスタイルが生まれてきたかを理解することで、よりよいプログラムを書くためのもので、正確なソフトウェア工学の歴史を学ぶためのものではありません。正確な歴史を把握したい場合は、原典をあたるようにしてください。
また、想定している読者は「よくあるオブジェクト指向プログラミングの学習」を既にしている、学校や趣味でのプログラミングはしてきたけど、実務でのOOPプログラムに不安を持つ人などです。
「よくあるオブジェクト指向の学習」というのは、いわゆるフルーツの特殊化がリンゴで、とか動物の一部が犬でといった若干天下りなアプローチで説明している学習を想定しています。
オブジェクト指向に至る軌跡
現在理解されているオブジェクト指向プログラミング、あるいはオブジェクト指向言語は、それに至るまでの様々なアイデアを統合し、再編され、また現実的な制約の中で歪みながら生まれてきたものだったりする。
当たり前のことではあるが、オブジェクト指向にせよ、その他のプログラミングパラダイムにせよ、現実世界のプログラミングという人間活動の中で生じた課題をどのように整理していくかという中で生まれてきた。
私自身をはじめ、OOP言語が当たり前の世の中でプログラムを書き始めると、忘れがちであるので、簡単な歴史とともに理解していきたいと思う。
ソフトウェア危機
1960年代の後半、私は生きていなかったのでよく知らないし、正しく理解していないかもしれないが、コンピュータが進歩するにつれて、より複雑なソフトウェアが求められ始める時代、その複雑さをコントロールするための道具やアイデアはあまり多くなかった。
(存在しなかった訳ではないが、いつの時代でも進歩的なものよりも支配的なものがプロジェクトの主流だったのだろう。)
プロジェクトは、複雑化する一方なのに、管理手法もなければ、
データ型は基本的な数値でしかなく、変数はメモリアロケーションそのものだった。
また、プログラムの流れは、gotoやjump命令のようにプログラムカウンタを直にコントロールする抽象度の低いもので制御されることが多かった。
プログラムはフローチャートで記述され、それをマシン語としてパンチするといったプロジェクトX的な世界のことを考えれば、その理解が正しいのかもしれない。
なんにせよ、そういった当時の人からすると逼迫していたが、今から見るとなんとも牧歌的な世界観の中で、構造化プログラミングという概念が生まれる。
##構造化プログラミング
もっとも有名なプログラマの一人でありながら、ほとんど自分用のコンピュータを持たなかったらしいダイクストラ。彼は構造化プログラミングという技法を提案したことでも有名だ。
ときどき、勘違いされているが構造化プログラミングとは「手続き型言語」のことでもなければ「gotoを使わないプログラミング」のことでもない。
構造化プログラミングとは、
構造化プログラミングではプログラミング言語が持つステートメントを直接使ってプログラムを記述するのではなく、それらを抽象化したステートメントを持つ仮想機械を想定し、その仮想機械上でプログラムを記述する。普通、抽象化は1段階ではなく階層的である。各階層での実装の詳細は他の階層と隔離されており、実装の変更の影響はその階層内のみに留まる([4] Abstract data structures)。各階層はアプリケーションに近い抽象的な方から土台に向かって順序付けられている。この順序は各階層を設計した時間的な順番とは必ずしも一致しない([4] Concluding remarks)。
つまり、現代風に言い換えると「レイヤリングアーキテクチャ」のようなもので、ある土台の上にさらに抽象化した土台をおき、その上にさらに・・・というようにプログラムをくみ上げていく考え方のことだ。
これは、現在のプログラミングにおいても当たり前となっている考え方だ。
だから、我々は、ひとつのアーキテクチャないし関数の中で異なる抽象化レイヤの実装を同居することをさける。
一方、耳目を集めやすいgoto文有害論とともに構造化技法の一部である構造化定理(
任意のフローチャートは、for文とif文で記述できる)が注目され、手続き型プログラミング言語を現代の形に押し上げていった。
ダイクストラは後に何をさすのか定義しきれなかったこと、商標をとらなかったため、他社にビジネスサイドからの普及があったことなど後悔したそうだ。
そして彼は、「構造化プログラミング」という言葉をさけるようになった。
今でもよくあることだが、ある技術的用語が異なった理解のされ方とともにバズワード化していったことが原因のようだ。
これは後述するが、オブジェクト指向という言葉の発明者がその命名は失敗だったと述べていることとシンクロしてとても面白い。
モジュラプログラミング
こういった背景のなか、プログラムは大きく複雑になり続ける。
至極自然な流れとして、それを分割しようとしていく。
凝集度と結合度
モジュールの分割には、大きな指針がなかった。
現在でもやろうと思えば全然関係のない機能を1つのモジュールに詰め込むことはできる。
熟練したプログラマとそうでないプログラマで、作り出すモジュールの品質は違う。
その品質の尺度として、凝集度と結合度という概念がしばらくして生まれた。
(オブジェクト指向の発明よりも遅いが、オブジェクト指向プログラミングに限定されない概念なので、ここで紹介しておく。)
結合度:よいコラボレーションとわるいコラボレーションを定義した
http://ja.wikipedia.org/wiki/結合度
凝集度:よい機能群のまとめ方とわるい機能のまとめ方を定義した
http://ja.wikipedia.org/wiki/凝集度
これらは「関心の分離」を行うためにどのようにするべきかという指針でもあった。
http://ja.wikipedia.org/wiki/関心の分離
この「関心」とはそのモジュールの「責任」「責務」と言い換えてもいいかもしれない。
この責任とモジュールが一致した状態にできるとそのモジュールは凝集度が高く、結合度を低くすることができる。
それぞれ悪い例と良い例を見ていき、「責任」「責務」の分解とは何かをとらえていこう。
悪い結合、良い結合
悪い結合としては、あるモジュールが依存しているモジュールの内部データをそのまま使っていたり(内容結合)、同じグローバル変数(共通結合)をお互いに参照していたりというようなつながり方だ。
こうなってしまうとモジュールは自分の足でたっていられなくなる。つまり、片方を修正するともう片方も修正せざるをえなくなったり、予想外の動作を強いられることになる。
逆に良い結合としては、定められたデータの受け渡し(データ結合)やメッセージの送信(メッセージ結合)のように内部構造に依存せず、情報のやり取りが明示的になっている状態を言う。
これはまさにカプセル化とメッセージパッシングのことだよね、と思った方は正しい。
オブジェクト指向は良い結合を導くために考えだされたのだから。
悪い凝集、良い凝集
凝集度が低い状態とはなにか、つまり悪い凝集とは何か、
アトランダムに選んできた処理を集めたモジュールは悪い。何を根拠に集めたのかわからないものも悪い凝集だ(暗合的凝集)。これは理解しやすい。
だが、それだけではなく、論理的に似ている処理だからという理由だけで集めてはいけない。(論理的凝集)
これはどういうことか。たとえば、これは入出力の処理だからといって、
functionopen(type,name){switch(type){case"json":...break;case"yaml":...break;case"csv":...break;case"txt":...break;:}returnresult;}
open
という関数にif文やswitch文を大量に入れて、あらゆるopen処理をまとめた関数をイメージしてもらいたい。(その論理的な関係を一つの記述にまとめたいと思うこと自体は悪い発想じゃないが、同じ場所に書くことで、もっと大事なデータとの関係が危うくなってしまう。その矛盾をうまく解決するのが同じメッセージをデータ構造ごとに異なる解釈をさせるポリモーフィズムだ。)
そういった種類のものがメンテナンスしづらいというのはイメージしやすいだろう。
他にも同じようなタイミングで実施されるからといって、モジュール化するのもの問題がある(時間的凝集)。たとえば、init
という関数の中ですべてのデータ構造の初期化をするイメージをしてほしい。
一方、良い凝集とはなんなのか、それはとあるデータに触れる処理をまとめること(通信的凝集)であるとか、適切な概念とデータ構造とアルゴリズムをひとまとめにすること(情報的凝集)。それによって、ひとつのうまく定義されたタスクをこなせるように集めること(機能的凝集)である。
状態と副作用の支配
結合度、凝集度を見ていく中で、「よいモジュール分割」とはなにかおぼろげに見えてきた。それは、処理とそれに関連するデータの関係性を明らかにして支配していくことの重要性だ。できれば、完全にデータの存在を隠蔽できてしまえると良いが、現実のプログラムではそうは行かない場合も多い。
こういった実務プログラミングの中で何が難しいかというと、それが状態と副作用を持つことだ。
たとえば、
functionadd(a,b){returna+b;}
このような副作用を持たない関数はテストもしやすく、バグが入り込む隙が少ない。
たとえば、計算機のレジスタ機能をこの関数に導入し、
varr=0;functionadd(a,b){r=a+(isUndefined(b)||r)returnr}
このようにすると途端に考慮するべき事柄が増える。
関連する状態や副作用を含めて、関数を大別すると次のようになる。
オブジェクト指向に至るモジュラプログラミングは、こういった状態や副作用に対して、積極的に命名、可視化、粗結合化をしていくことで「関心の分離」を実現しようとした。
たとえば、現在でもC言語のプロジェクトなどでは、
構造体とそれを引数とする関数群ごとにモジュールを分割し、大規模なプログラミングを行っている。
typedefstruct{:}Person;voidperson_init(person*p,...){:}char*person_get_name(person*p){:}voidperson_set_name(person*p,char*name){:}
よくあるのは、上記のように構造体の名前のprefixとしてつけ、構造体のポインタを第一引数として渡す手法だ。
その名残なのか、正確なところはよく知らないが、pythonやperlのオブジェクト指向では、自分自身を表すデータが、第一引数として関数に渡される。
classPerson(object):def__init__(self,a,b):self.a=aself.b=b
packagePerson{subnew(){my($class,$a,$b)=@_;my$self=bless{},$class;$self->init($a,$b);return$self;}subinit{my($self,$a,$b)=@_;$self->{a}=$a;$self->{b}=$b;}}
あくまで関数の純粋性を犠牲にしないように発展を続けた関数型プログラミングと、状態や副作用をデータ構造として主役にしていった手続き型プログラミングの分かれ目として理解すると面白い。
抽象データ型
よいモジュール化の肝は、状態と副作用を隠蔽し、データとアルゴリズムをひとまとめにすることだった。
それらを言語的に支援するために抽象データ型という概念が誕生した。
抽象データ型は、今で言うクラスのことだ。すなわちデータとそれに関連する処理をひとまとめにしたデータ型のことだ。ようやくオブジェクト指向の話に近づいてきた。ダイクストラの構造化プログラミングでは、データ処理をどのように抽象化するかが課題として残っていた。
また、データ型と実際のメモリアロケーションは別であるので、新たに変数を定義するとデータの共有はしない。あるデータ型を実際に存在するメモリに割り当てることをインスタンス化という。
抽象データ型のポイントは、その内部データへのアクセスを抽象データ型にひもづいた関数でしか操作することができないという考え方だ。
これはつまり、たとえば、先ほどのC言語の例でいうと
typedefstruct{//内部構造も公開している}people;voidpeople_init(people*p,...);char*people_get_name(people*p);voidpeople_set_name(people*p,char*name);
このままだと、構造体の内部構造も公開しているので、
peopleuser;user.age=10;printf("%d years old",user.age);
のように内部構造に直接アクセスできてしまう。
C言語では、テクニックとして
typedefstructsPersonperson;voidperson_init(person*p,...);char*person_get_name(person*p);voidperson_set_name(person*p,char*name);
#include"person.h";structsPerson{// ここに内部構造};//非公開用関数_person_private(person*p,....);
公開するヘッダと非公開のヘッダを分けることで、情報の隠蔽を行い
抽象データ型としての役目を成り立たせている。
抽象データ型の情報隠蔽とカプセル化
C言語の構造体であっても、ヘッダファイルの定義と実装を分けることで、抽象データ型の内部構造を隠蔽することができたが、言語機能として外部からのアクセスに対する制限を明示できるようにサポートした。カプセル化やブラックボックス化というのは情報隠蔽よりも広い概念ではあるが、これらの機能によって、「悪い結合」を引き起こさないようにしている。
JavaやC#などのアクセス修飾子がそれにあたる。
PerlやJavaScriptなどアクセス修飾子の無い言語では、公開と非公開を明確に区別せず、_privateMethod
のようにアンダースコアを先頭につけることで、擬似的に公開と非公開を区別する。
いずれにしても、ポイントは抽象化されたデータを取り扱うレイヤは、抽象化されていない生の階層を直接触ることがないという階層化の考え方だ。
これによって、複雑化した要求を抽象化の階層を定義していくという現代的なプログラミングスタイルが確立した。
オブジェクト指向?
さて、ようやく本題のオブジェクト指向に入れる。ここでお詫びと訂正をすると、最初のオブジェクト指向言語(そういう名前はついていなかった)は、1960年代のうちに出ている。Simulaという言語だ。
これはシミュレーション記述のために作られた言語であったが、後に汎用言語となった。
オブジェクト、クラス(抽象データ型)、動的ディスパッチ、継承が既にあり、ガーベジコレクトまで実装されていたらしい。汎用言語としてそこまではやることはなかったが、これらの優れたコンセプトは今現在まで生き残っている。
こういった過去の優れたコンセプトが、再評価され実際的なプログラミング言語として生まれ変わることで、一般的になるというのはコンピュータサイエンスの界隈ではよくおこる。今現在でももっとも柔軟なマルチパラダイム言語であるlispは1950'sにはすでにその着想が存在していたりと人間の想像力は本当にすばらしいなと思う。
ここからちょっと事態は複雑になる。
Simulaの優れたコンセプトをもとに2つの今でも使われているC言語拡張が生まれた。
一つはC++。もう一つはObjective-Cである。
C言語はとても実際的なものだったので、それにプリプロセッサの形で優れたコンセプトを輸入しようとしたのは当然の成り行きといえばそうだ。
SimulaのコンセプトをもとにSmalltalkという言語というか環境が爆誕した。
Smalltalkは、Simulaのコンセプトに「メッセージング」という概念を加え、それらを再統合した。Smalltalkはすべての処理がメッセージ式として記述される「純粋オブジェクト指向言語」だ。
そもそもオブジェクト指向という言葉はここで誕生した。
オブジェクト指向という言葉の発明者であるアランケイは後に「オブジェクト指向という名前は失敗だった」と述べている。メッセージングの概念が軽視されて伝わってしまうからだという。
何にせよ、このSmalltalkの概念をもとにC言語を拡張したのがObjective-Cだ。
Simula & C++のオブジェクト指向
C++の作者であるビャーネ・ストロヴストルップは、オブジェクト指向を「『継承』機構と『多態性』を付加した『抽象データ型』のスーパーセット」として整理した。
C++ではメソッドのことをメンバー関数と呼ぶ。これはSimulaがメンバープロシージャと読んでいるところに由来する。メソッドは、Smalltalkが発明した用語だ。
こういったところにも出自の名残がある。
どの処理を呼び出すか決めるメカニズム
さて、継承と多態を足した抽象データ型といっても、なんだか良くわからない。
特に多態がいまいちわかりにくい。オブジェクト指向プログラミングの説明で
string=number.StringValuestring=date.StringValue
これで、それぞれ違う関数が呼び出されるのがポリモーフィズムですよと呼ばれる。
これだけだとシグネチャも違うので、違う処理が呼ばれるのも当たり前に見える。
では、こう書いてみたらどうか
string=stringValue(number)// 実際にはNumberToStringが呼ばれるstring=stringValue(date)// 実際にはDateToStringが呼ばれる
このようにしたときに、すこし理解がしやすくなる。引数の型によって呼ばれる関数が変わる。こういう関数をpolymorphic(poly-複数に morphic-変化する)な関数という。
これをみたときに"関数のオーバーロード"じゃないか?と思った人は鋭い。
http://ja.wikipedia.org/wiki/多重定義
多態とは異なる概念とされるが、引数によって呼ばれる関数が変わるという意味では似ている。しかし、次のようなケースで変わってくる。
functiontoString(IStringValuesv)string{returnStringValue(sv)}
IStringValueはStringValueという関数を実装しているオブジェクトを表すインターフェースだ。これを受け取ったときに、関数のオーバーロードでは、どの関数に解決したら良いか判断がつかない。関数のオーバーロードは、コンパイル時に型情報を付与した関数を自動的に呼ぶ仕組みだからだ。
stringValue(number:Number)=>StringValue-Number(number)stringValue(date:Date)=>StringValue-Date(date)
functiontoString(IStringValuesv)string{returnStringValue(sv)=>StringValue-IStringValue(無い!)}
それに対して、動的なポリモーフィズムを持つコードの場合、次のように動作してくれるので、インターフェースを用いた例でも予想通りの動作をする。
functionStringValue(v:IstringValue){switch(v.class){//オブジェクトが自分が何者かということを知っている。caseNumber:returnStringValue-Number(number)caseDate:returnStringValue-Date(date)}}
このようにどの関数を呼び出すのかをデータ自身に覚えさせておき、
実行時に探索して呼び出す手法を動的分配、動的ディスパッチと呼ぶ。
このように動的なディスパッチによる多態性はどのような意味があるのか。
それはインターフェースによるコードの再利用と分離である。
特定のインターフェースを満たすオブジェクトであれば、それを利用したコードを別のオブジェクトを作ったとしても再利用できる。
これによって、悪い凝集で例に挙げた論理的凝集をさけながら、
汎用的な処理を記述することができるのだ。
オブジェクト指向がはやり始めた当時は、再利用という言葉が比較的バズったが、
現在的に言い換えるなら、インターフェースに依存した汎用処理として記述すれば、結合度が下がり、テストが書きやすくなったり、仕様変更に強くなったりする。
動的ディスパッチ
動的ディスパッチのキモは、オブジェクト自身が自分が何者であるか知っており、また、実行時に関数テーブルを探索して、どの関数を実行するかというところにある。SimulaもC++もvirtualという予約語を用いて、仮想関数の動的分配をすることを宣言できる。
/*Vtable for B1B1::_ZTV2B1: 3u entries0 (int (*)(...))08 (int (*)(...))(& _ZTI2B1)16 B1::f1Class B1 size=16 align=8 base size=16 base align=8B1 (0x7ff8afb7ad90) 0 vptr=((& B1::_ZTV2B1) + 16u) */classB1{public:voidf0(){}virtualvoidf1(){}charbefore_b0_char;intmember_b1;};/*Class B0 size=4 align=4 base size=4 base align=4B0 (0x7ff8afb7e1c0) 0 */classB0{private:voidf(){};intmember_b1;};
このようにデータ自身にvtable(仮想関数テーブル)へのポインタを埋め込んであり、
それをたどることで解決する。
逆にvirtual宣言をしなければ、仮想関数テーブルをたどるというオーバーヘッドなしに関数を呼ぶことができる。Javaでは、デフォルトでvirtual宣言されているのと等価に動的なディスパッチが行われる。
C++やC#では、動的ディスパッチのコストを必要なときにしか利用しないために(ゼロオーバーヘッドポリシー)、virtual宣言を明示的にする必要がある。
objective-Cも同様であるが、関数ポインタを直に取得することでこのオーバーヘッドを回避することができる。
SELselector=@selector(f0);IMPp_func=[objmethodForSelector:selector];// p_funcを保持しておいて、繰り返しなどで:pfunc(obj,selector);// pfunc使うと、探索コストを減らせる。// 何か重要でない限りする必要はない。
疑似コードで、この動的なディスパッチを表現するとこのようになる。
varPERSON_TABLE={"getName":function(self){returnself.name},};varobject={_vt_:PERSON_TABLE,// 自分が何ができるか教えるname:"daichi hiroki"};// メソッドを動的に呼び出すfunctionmethodCall(object,methodName){// オブジェクト自身を第一引数として束縛するreturnobject._vt_[methodName](object)}methodCall(object,"getName");
こうなってくると、多態を実現するためには、3つの要素が必要だとわかる。
- データに自分自身が何者か教える機能
- メソッドを呼び出した際にそれを探索する機能
- オブジェクト自身を参照できるように引数に束縛する機能
あとからオブジェクト指向的機能を追加したperl5の例が、これらを端的に追加しているので見ていこう。
packagePerson;subnew{my($class,$ref)=@_;#リファレンスとパッケージを結びつけるbless関数# $classはPersonパッケージを表すreturnbless($object,$ref);}subget_name{my($self)=@_;$self->{name};}#メソッドの動的な探索と第一引数に束縛する->アロー演算子my$person=Person->new({name=>"daichi hiroki"});$person->get_name;
このなかで、bless
関数はリファレンスに対して、リファレンス自身が「関数を探索するべきモジュールはここですよ。」と教えている。(blessは祝福するという意味。パッケージのご加護が守護霊みたいにくっつくイメージ。)
また->
演算子を使うことで、自動的に探索と呼び出しを実現している。
あと付けでOOP機能を足そうというときに、たった二つの機能で多態を実現したPerl5のアプローチにはたぐいまれなセンスを感じる。
継承と委譲
継承
さて、SimulaとC++がもたらした最後の要素は継承だ。継承は、あるクラスの機能をもったまま、別の機能を追加したもう一つのクラスを作る仕組みだ。
まずはデータだけで考えてみよう。
生徒と先生の管理をしたいというときに、
二つに共通しているデータ構造は名前、性別、年齢であり、
生徒は追加して、学科と年次を管理し、
先生は追加して、専門と月収を管理したいとする。
typedefstruct{intage;intsex;char*name;}Person;typedefstruct{Peoplepeople;intgrade;intstudy:}Student;typedefstruct{Peoplepeople;intfield;intsalary;}Teacher;Teachert;t.people.age=10;
とするとこのように構造体に構造体を埋め込むことで、共通するデータ構造を持つことができる。
これに処理を追加する場合、次のようにするだろう。
char*person_get_name(Person*self){returnself->name;}char*teacher_get_name(Teacher*self){returnperson_get_name((People*)self);}char*teacher_get_name_2(Teacher*self){returnperson_get_name(&self.person);}Teacher*pt=teacher_alloc_init(30,MALE,"daichi hiroki",MATH,30);teacher_get_name(pt);
このようにアップキャストして、埋め込んだ構造体内部にアクセスすることができる。
それか、埋め込んだ構造体をそのまま渡すなどして、処理の共通化を実現する。
しかし、これでは処理の共通化をするごとにその呼び出しコードを追加する必要がある。
これをうまく提供してくれるのが継承機能だ。
public/protectedなメンバー関数やメンバー変数に対して、継承関係をたどって
探すことができる。
そのため
Teacher*t=newTeacher;t->get_name;// Teacher自体に宣言がなくても、Peopleクラスを探索してくれる。
のように書くことができる。
また、
stringnameFormat(People*p){returnsprintf("%s(%d) %s",p->get_name,p->get_age,(p->get_sex==MALE)?"男性":"女性");}
というような関数があったときに、
Person*p=newPerson;Student*s=newStudent;Teacher*t=newTeacher;nameFormat(p);nameFormat(s);nameFormat(t);
Person自身かそのサブクラスであれば、共通の処理を利用することができる。
この継承関係を言語機能として提供するためにperl5では、もう一つの機能を追加する。
それが@ISA
だ。
packagePerson;subget_name{"person"}packageStudent;# @ISAにパッケージを追加するとblessされたパッケージに関数がなかった場合にそちらを探索に行くour@ISA=qw/Person/;packageTeacher;our@ISA=qw/Person/;
このようにどこを探索するのかという情報だけ宣言できるようにすれば、問題なく継承関係を表現することができる。
ちょうど、FQNで表記すると
@Teacher::ISA="Person"
という表現になり、teacher is a personという関係が成り立っていることを表現している。
このときのメソッド探索を疑似コードで書くと次のようになる。
varPERSON_TABLE={"getName":function(self){returnself.name}};varSTUDENT_TABLE={"getGrade":function(self){returnself.grade},"#is-a#":PERSON_TABLE};varobject={_vt_:STUDENT_TABLE,// 自分が何ができるか教えるname:"daichi hiroki"};// メソッドを動的に呼び出すfunctionmethodCall(object,methodName){varvt=object._vt_;// is-aを順番にたどってmethodを見つけて実行するwhile(vt){varmethod=vt[methodName];if(method)returnmethod(object);vt=vt["#is-a#"];}throwError;}methodCall(object,"getName");
委譲
継承の代わりに委譲という手段を用いているプログラミング言語がある。
これはSimulaとC++の系譜とは少し違うが、動的ディスパッチの話をしたので
簡単に説明する。
これは、クラスベースのオブジェクト指向に対してプロトタイプベースのオブジェクト指向と呼ばれたりする。身近な例ではJavaScriptなどだ。
継承と委譲の違いは先ほどのC言語の例で言えば、すごく単純で埋め込む構造体が
ポインタかそうでないかという違いくらいだ。
typedefstruct{intage;intsex;char*name;}Person;typedefstruct{Person*person;intgrade;intstudy:}Student;typedefstruct{Person*person;intfield;intsalary;}Teacher;
委譲は、探索先のオブジェクトを動的に書き換えることができる。
t->person=newPerson;
疑似コードで言えば、
varhogetaro={getName:function(self){returnself.name},name:"hogetaro"};varobject={_prototype_:hogetaro,// 次に探索するオブジェクトを決めるname:"daichi hiroki"};// メソッドを動的に呼び出すfunctionmethodCall(object,methodName){// 最初は自分自身varpt=object;// is-aを順番にたどってmethodを見つけて実行するwhile(pt){varmethod=pt[methodName];if(method)returnmethod(object);pt=pt._prototype_;}throwError;}methodCall(object,"getName");object._prototype_={getName:function(){return"hello"}};// プロトタイプは動的に書き換えることができる。methodCall(object,"getName");
このようになる。
こうやって、prototypeを順番に追って検索していくのをjavascriptではプロトタイプチェーンと読んでいる。luaであれば同じ役割をするのがmetatableというものがある。
こういった委譲によるメソッド探索は、動的継承とも呼ばれている。
このようにメソッドの動的な探索に対して、どのような機構をつけるのかというのが
オブジェクト指向では重要な構成要素と言える。
rubyのmoduleやそのinclude,prepend、特異メソッド、特異クラスなどは
まさにその例だ。
それらをjavascriptで疑似コード的に実装した例として、こちらを参照してもらいたい。
http://qiita.com/hirokidaichi/items/f653a843208971981c37
オブジェクト指向の要素
このようにオブジェクト指向のための機能は、
- 抽象データ型:データと処理をひもづける
- 抽象データ型:情報の隠蔽を行うことができる
- オブジェクト:データ自身が何者か知っている
- 動的多態:オブジェクト自身のデータと処理を自動的に探索する
- 探索先の設定:継承、委譲
ということになる。
Smalltalk & Objective-Cのオブジェクト指向
Smalltalkの作者の一人であるアランケイがオブジェクト指向という言葉について次のように定義づけている。
「パーソナルコンピューティングに関わる全てを『オブジェクト』とそれらの間で交わされる『メッセージ送信』によって表現すること」
C++の世界観とはまた異なっているのがわかると思う。
仮想機械としてのオブジェクト
アランケイの世界観の中では、メモリとCPUとそれに対する命令を持つ機械をさらに抽象化するとしたら、それは同じくデータと処理と命令セットをもつ仮想機械で抽象化されるべきだと考えていた。
これは、構造化プログラミングの中でダイクストラが仮想機械として階層的に抽象化すべきだと言っていたこととかぶる。
個人的には、背景の違いこそあれ同じことを言っているように思う。
オブジェクトは独立した機械と見なせるため、それに対してメッセージを送り、自ら持つデータの責任は自らが負う。
Smalltalkの実行環境もまた仮想機械として作られている。
メッセージング
Smalltalkでメッセージ送信はreceiver message
のように記述する。
Objective-Cであれば、C言語の中に次のように書くことでメッセージ送信機構を動かすことができる。[receiver message]
[receiver methodName:args1 with:args]
メッセージ送信と関数呼び出しは厳密には異なる。Objective-Cにとって、実装上はほとんど同じことではあるが。
メッセージとは通信のアナロジーだ。たとえばEメールをイメージしてもらいたい。メールアドレスさえ知っていれば、メッセージは自由に送れる。受信者(レシーバ)はメッセージを受け取っているにすぎないので、その解釈は自由に行うことができる。
このメッセージらしさが出てくる特徴をいくつか紹介しよう。
動的な送信
メッセージ内容もまたオブジェクトにすぎないので、動的に作成し、送ることができる。
たとえば、rubyのObject#sendがその性質をそのまま表現している。
classAdefhellop"hello"endenda=A.new# 動的にメソッドを作成method="he"+"ll"+"o"# それを呼び出すa.send(method)
LL言語では、こういった動的な性質は普通のことになってきているが、
Objective-Cでも、セレクタ型とともにperformSelectorメッセージを送ったり、NSInvocation*を使うことで動的に作られたメッセージを送信することができる。
カスケード式
Smalltalkの機能で、カスケード式というのがある。これは、複数のメッセージを同時にまとめて送るという機能だ。
これもまたメッセージのアナロジーならではと言えるだろう。
| collection|collection:=OrderedCollectionnewadd:0;add:1;add:2;add:3;add:4;yourself.
JSON RPCのBulk Requestのイメージに近い。
メッセージ転送
受け取ったメッセージは、仮にメソッド定義がなかったとしても自由に取り扱うことができる。
rubyのmethod_missing
やObjective-CのforwardInvocation
がそれにあたる。他にもPerlのAUTOLOADなど、最近の動的型言語には用意されていることが多い。
Smalltalkであれば、doesNotUnderstandメソッドが呼ばれ、その中で煮るなり焼くなりできる。
classProxydefmethod_missing(name,*args,&block)target.send(name,*args,&block)enddeftarget@target||=[]endend
たとえば、proxyクラスをこのように定義してあげると
すべてのメッセージをtargetのオブジェクトにそのまま転送してあげることができる。
これもメッセージというアナロジーならではの考え方だ。
非同期送信
ほとんどの言語でメッセージの結果を同期的に受け取るようになっているので、意識しづらいが、メッセージというアナロジーである以上、それを同期的に待ち受ける必要はない。
objectfoo//同期呼び出しfuture=object@foo//非同期呼び出し
非同期なメッセージパッシングを中心にオブジェクト間の相互のやり取りをモデル化したものとして、アクターモデルがある。
scalaのActorを利用するとこんな感じ
classHelloActorextendsActor{defreceive={case"Hello"=>println("helloworld")case_=>println("errror")}}
このようにメッセージパッシングというアナロジーを使うことで、様々な性質がオブジェクト指向には加わることになった。
しかし、オブジェクト指向という言葉が意味しているのが、C++の再定義したオブジェクト指向として理解されることで、このメッセージパッシングの要素が意識されなくなってしまったため、前述したようにアランケイはその命名が不適切だったと考えているらしい
この記事は今までの議論の流れをふまえると、理解がしやすいと思う。
特に
私は、オブジェクト指向プログラミングというものに疑問を持ち始めました。Erlangはオブジェクト指向ではなく、関数型プログラミング言語だと考えました。そして、私の論文の指導教官が言いました。「だが、あなたは間違っている。Erlangはきわめてオブジェクト指向です。」 彼は、オブジェクト指向言語はオブジェクト指向ではないといいました。これを信じるかどうかは確かではありませんでしたが、Erlangは唯一のオブジェクト指向言語かもしれないと思いました。オブジェクト指向プログラミングの3つの主義は、メッセージ送信に基づいて、オブジェクト間で分離し、ポリモーフィズムを持つものです。
このくだりは。
さらに面白いのはそのErlangの開発に深く関わっている人物が
「オブジェクト指向はなんでくそったれか!」http://www.sics.se/~joe/bluetail/vol1/v1_oo.html
のような記事を出していたりして、なかなか面白いことになってます。
Qiita上に翻訳もあったのでぜひご一読ください。
オブジェクト指向はクソか?
まとめ
オブジェクト指向も構造化プログラミングも問題の抽象化で同じことを見ていた。
C++はSimulaからモジュール化や抽象データ型、動的多態といった良い性質を採用した。
一方、SmalltalkはSimulaの着想をメッセージとオブジェクトという概念で統合した。
それによって、様々な動的な性質を現在の言語にもたらしてきた。
また、メッセージパッシングという概念は、本質的には現在注目を浴びているActorやCSPのような並行モデルと似通っており、興味深い。
あとがき
少しはオブジェクト指向という考え方の背景が見えてきて、
それがより良い設計やコーディングにつながればうれしいです。
この説明は、オブジェクト指向の説明の本流ではない、いわば傍流的なものではありますが、より実際的で、より技術的理解を必要とするものなので、初学者向けではなかったかと思います。ですが、これを理解することで、様々な言語機能の背景を推察することができ、バラバラの事柄が有機的につながることを期待しています。
参考文献
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme