オブジェクト指向プログラミングとは結局なんなのか


この記事は第2のドワンゴ Advent Calendar 2015の5日目です。
ちなみに前日は@deflisさんでした。
先日の記事で分かる通りドワンゴ社員()なのですが、まぁ@mesoさんが「厳格な管理とかめんどくさいので、元社員も参加すればいいんじゃないかな。」とか言ってるしお目こぼし頂きたく…

去年のアドベントカレンダー記事は「関数型プログラミングとは結局なんなのか」というタイトルで、関数型プログラミングという語が何を指していて何を指していないのか、みたいなことをなるべく平易にまとめました。
なので今年は「オブジェクト指向プログラミング(以下OOP)とは結局なんなのか」という記事にしてみた…のですが、なにぶん語の指す範囲が広く、また自分も理解しきっているわけではないので、多少不正確な点があるかもしれません。
「関数型は流行りだけど、今更OOPかよ」とか思われるかもしれませんが、お付き合いいただければと思います。

OOPという語の曖昧さ

さて、一口にOOPといっても、人によってイメージするものがバラバラである可能性があります。
「動物クラスを継承した犬クラスと猫クラスが」とか「グワッと鳴けばアヒル」といった話から、「イマドキのオブジェクト指向はドメイン駆動ヘキサゴナルアーキテクチャだ」みたいな話までいろんなイメージを持つ方がいると思いますが、今回はオブジェクト指向の本質的な思想の部分を中心にまとめる予定です。

ところが困ったことに、OOPという語は2つの異なる系譜があったりします。

  • アラン・ケイがSmalltalkで提唱したOOP
  • ストラウストラップがC++で提唱したOOP

現在は、後者濃い目でこれらが混ざり合ったものに加えて

  • プログラミングではなく設計・モデリングの話

までOOPの範疇で語られていることも見かけることがあります。
これらの区別をつけないと議論の土台から噛み合わなくなってしまう可能性があるため、以降これらを区別して「アラン・ケイのOOP」「ストラウストラップのOOP」「オブジェクト指向のモデリング・設計」と呼び分け、順に見ていくことにします。

アラン・ケイのOOP

「オブジェクト指向」の生みの親

そもそも「オブジェクト指向プログラミング(Object Oriented Programming)」という単語は、アラン・ケイが創りだしたものです。
オブジェクトやクラスという概念はSimulaという言語で初めて実装された言語機構ですが、この言語が創られた時点ではそれらの概念を指す用語がなく、後にアラン・ケイが概念を指すための用語として使い始めたそうです。
アラン・ケイはまた、Simulaの影響を受けつつOOPにより特化したSmalltalkという言語を開発しています。

文と式

アラン・ケイのOOPを説明する前に、少しだけ命令型プログラミングや関数型プログラミングの基本的な部分を確認しておきましょう。

命令型プログラミングは「状態の書き換え命令を繋ぎあわせて計算するプログラミング手法」で、関数型プログラミングは「副作用のない関数を組み合わせて計算するプログラミング手法」です。
これをより細かく見ると、ある一つの処理単位に注目したときに「状態を書き換えるだけで値を返さない」場合は命令型的、「状態を書き換えず値を返す」場合は関数型的であると言えます。
大抵のプログラミング言語では、前者が「文(statement)」、後者が「式(expression)」という構文要素になっていることが多いようです。

命令型プログラミング言語と関数型プログラミング言語を分ける一つのポイントとして、制御構造が文と式のどちらを主眼において設計されているかが目安になります。
例えば、C言語のif文は、式ではなく文のため値を返しません。
値を返さないということは、内部の処理で状態書き換えなどの副作用を起こさないかぎり、外部に影響を与えられないことになります。
以下では、内部でgreeting変数への代入を行っています。

char[] greeting;
if (is_morning) {
  greeting = "good morning";
} else {
  greeting = "hello";
}

一方、OCamlのif式は、式なので値を返します。
以下では、if式全体の結果をgreetingという名前で変数束縛しています。

let greeting =
  if is_moring then
    "good morning"
  else
    "hello"

繰り返しも同じで、for文やwhile文は全体として値を返さないため、内部で副作用を起こすことを前提とした命令型的な言語機構です。
一方、再帰呼び出しは(基本的に)副作用のない関数の評価で繰り返しを表現するため、関数型的な言語機構です。
もちろん例外はありますが、基本構成要素が文であるか式であるか、は命令型と関数型を区別するポイントになると思います。

メッセージ式

さて、ではアラン・ケイのOOPについて、その思想が色濃く反映されたSmalltalkとともに見ていきましょう。

アラン・ケイはOOPを「全てがオブジェクトであること」「オブジェクト同士がメッセージを送り合ってコミュニケーションすること」と定義しています。
他にもメモリやクラスに関する定義が何項目かありますが、後に

オブジェクト指向プログラミングの概念は完全に間違って理解されているのだ。オブジェクト指向プログラミングの正しい概念とはオブジェクトとクラスに関してではなく、すべてがメッセージングということなのだ

と言っているので、特にメッセージングを重視している姿勢が見て取れます。(参考)

先ほど、命令型プログラミングでは文が、関数型プログラミングでは式が基本構成要素になる、という話をしました。
アラン・ケイのOOPを色濃く反映したSmalltalkでは、基本構成要素はメッセージ式になります。
メッセージ式とは、「オブジェクトへメッセージを送り、その結果を得る」ものです。

2+3の計算を考えてみましょう。
関数型プログラミング言語では(一般的な命令型プログラミング言語でも)この式は+という演算を23を引数に実行します。
一方Smalltalkでは、これは2というオブジェクトに対して+3というメッセージを送信する、というメッセージング処理になります。
このとき+をセレクタ、3を引数とよび、セレクタによって起動するメソッドが決まります。

ここから分かるとおり、普通の式は「関数を主体とし、値はそれに渡されるだけ」なのに対し、メッセージ式では「値を表すオブジェクトを主体とし、オブジェクトがメッセージに対する振る舞いを知っている」という違いがあります。

全てがメッセージング

制御構文もメッセージングになっています。
以下はSmalltalkで分岐処理を行う例です。

| greeting |
greeting := isMorning ifTrue: ['good morning'] ifFalse: ['hello']

この場合は、isMorningに対してifTrue: ['good morning'] ifFalse: ['hello']というメッセージを送信しています。
急に複雑な例になってしまいましたが、複数の引数を取るメソッド呼び出しでは、このように引数ごとにifTrueifFalseなどのキーワードを指定し、引数はキーワードごとに指定します。
このメッセージのセレクタ(=呼び出されるメソッド)は、キーワードの組み合わせであるifTrue:ifFalse:となります。

また[]で囲まれた部分はブロックとよばれ、「後から起動できるメッセージ式」を表したオブジェクトになります。
これは無名関数やrubyのブロックのようなもの、と思っておけば問題はないでしょう。

このように、Smalltalkではboolean自体が「ifTrue:ifFalse:という、自身がtrueならifTrue:キーワードで受け取ったブロックを、falseならifFalse:キーワードで受け取ったブロックを起動し結果を返すメソッド」を持っており、分岐処理を行いたい場合はbooleanに対するメッセージングで実現します。
繰り返し処理も同様に、整数値に対してtimesRepeat:セレクタによるメッセージを送ることで整数値の回数だけ引数ブロックを起動する、booleanを返すブロックに対してwhileTrue:セレクタによるメッセージを送ることでそのブロックが真になるまで引数ブロックを起動する、などで実現しています。

ここで重要なのは、booleanに限らずともifTrue:ifFalse:セレクタによるメッセージに返答できれば分岐処理が行える、というところです。
数値型に0か非0かで分岐するメソッドを生やせば数値に対してifTrue:ifFalse:セレクタによるメッセージを送れますし、自分で作った信号機オブジェクトにGreenかどうかで分岐するメソッドを生やすこともできます。
さらには、利用者側はメッセージを送る先がbooleanであるのか数値であるのか信号機であるのか、を気にする必要はありません。
先ほどの例でis_morningがbooleanでなく数値や信号機であっても一向に構わないわけです。(人間にとっての意味はわからなくなりますが)
この辺りを見ると、アラン・ケイのいう「全てがメッセージングであること」という言葉の意味や、命令型プログラミング・関数型プログラミングとの根本的な違いが理解しやすいのではないかと思います。

ストラウストラップのオブジェクト指向

独立してSimulaからの影響を受けたC++

OOPという単語はSimulaの影響を受けてSmalltalkを設計したアラン・ケイが創りだしたものですが、これとは別にSimulaの影響を受けてC++を設計したのがストラウストラップです。
ストラウストラップはオブジェクト指向という語の意味を体系化し、オブジェクトという道具立ては一緒ながらも根本思想は異なる概念として再定義しています。
そして実際のところ、SmalltalkよりC++のほうが機能や速度の面で実践的であり流行ったため、こちらが一般的に認識されるOOPのイメージになっています。

関数と手続き・モジュール・抽象データ型

こちらについても、本題に入る前に命令型プログラミング・関数型プログラミングで関連するトピックを取り上げておきましょう。

命令型にしろ関数型にしろ、基本構成要素である文や式をひたすら組み合わせるだけでは限界があります。
そこで、基本構成要素を意味のある単位でまとめるための手続き(procedure)や関数(function)といった仕組みが利用されます。
本来は手続きが「呼び出すことで命令的な処理を実行し、値を返さないか終了コードのみを返すもの」、関数が「呼び出すことで副作用のない計算を行い、その結果を返すもの」に当たるはずですが、手続きという単語は死語に近く、副作用を主目的にするものでも関数と呼ばれることが大半です。
このおかげで関数型プログラミングの話をするときにいちいち「副作用のない関数」と言わなければ意図が伝わらなくて面倒なのですが、それは別の話です。

処理のまとまりを作ることで、呼び出す側では内部で何が行われているかを気にする必要がなくなります。
いわゆるブラックボックスですね。
ところが、処理をブラックボックスにしても、その処理が対象にするデータ構造はそのままです。
例えばスタックを実装する場合、スタックへのpushpopは手続きや関数として記述できますが、肝心のスタック自身は単なる配列やインデックスの数値でしかありません。

int stack[10];
int stack_idx = 0;
 
void push(int val) {
    stack[stack_idx++] = val;
}
 
int pop() {
    return stack[--stack_idx];
}

もしスタックのデータ構造を配列からリストに変えようと思ったら、pushpopだけではなく、配列やインデックスを外部で直接触っているところがないかを確かめる必要があるでしょう。

これじゃ片手落ちだということで、データ構造とそれに付随する操作をまとめるためのモジュールという機構が考案されます。
Stackモジュールにスタックを表すデータ類とpushpopといった操作をまとめて、外部には操作だけを公開してやれば、データに直接触られる心配はなくなります。
このようにデータ構造とそれに対する操作をまとめたものを抽象データ型といいます。

これで一安心…と思いきや、命令型プログラミングではこの仕組は上手く働きません。
スタックモジュールにスタックのデータを閉じ込めると、そのモジュールで扱えるスタックが1つだけになってしまうからです。
処理は1つのモジュールに1つの実体でいいのですが、データは1つのモジュールに対して多数の実体が必要になります。

これを解決するために1つのモジュールから複数のデータ実体を作りライフサイクル管理をする仕組みが必要になりました。
どこかで聞いた話ですね。
お察しの通り、ここからクラスとオブジェクトの必要性が出てきて、Simulaにこれらが実装されることになります。
めでたしめでたし。

ちなみにモジュールについて「命令型プログラミングではこの仕組は上手く働きません」と書きました。
関数型プログラミングでは(というかOCamlやHaskellでは)、そもそもデータの書き換えができないため、モジュールにデータ構造を直接持たせることはできません。
そこでモジュールにはデータ構造ではなくデータ型を持たせたうえで、外部にはその型を「抽象型」として公開します。
抽象型は「その型であることはわかるがそれ以上の情報がない」型で、内部構造の情報がないため具体的な値を取り出すことができず、その抽象型を受け取る関数に渡す以外の使い道がなくなるものです。
これにより、データ実体はモジュールとは独立して生成しつつ、その型の中身はモジュール内からしかわからないため、外部からは弄ることができなります。
この辺りから、「オブジェクト指向プログラミングのパラダイムは命令型プログラミングの系譜である」ことが分かるかと思います。

抽象データ型としてのクラス

さて、ストラウストラップはSimulaからクラスやオブジェクトをC++に取り入れるに当たり、上述の「抽象データ型」としての側面を重視しつつ、静的型システムへの組み込みを行いました。
ストラウストラップのOOPは、「カプセル化・継承・多態性(ポリモーフィズム)」とされています。
外部から直接内部構造を触られるとデータ型としての一貫性が保てなくなるため、カプセル化は抽象データ型の備えるべき性質であると言ってよいでしょう。

残る継承と多態性については、クラス定義の動的性を確保しつつ静的型システムに組み込むための機能という感があります。
「どのクラスがメッセージを受け取るか」を動的に評価するSmalltalkと違い、C++ではクラスを静的型システムに組み込んでいるため、コンパイル時点でどのクラスのクラス関数が呼ばれているかを確定しなければいけません。
ここに自由度の幅を持たせるためには、クラス間の階層関係を作る継承と、同じメソッド名でもそれぞれに応じたクラス関数が呼ばれる多態性の仕組みが必要になるでしょう。

こう考えると、ストラウストラップのOOPでは「静的な抽象データ型としてのクラスを用いたプログラミング」という側面が強いように感じます。
これは、メッセージを介して徹底的に動的性を追求しているアラン・ケイのOOPとは、道具立ては一緒でも目指している方向は正反対に近いといえるでしょう。

オブジェクト指向のモデリング・設計

先に断っておきますが、これはOOPの話ではないです。
オブジェクト指向プログラミングの範疇ではなく、オブジェクト指向モデリングやオブジェクト指向設計の範疇の話になります。

この記事ではモデリングを「現実の問題領域をどのようにプログラムへマッピングするかの分析」、設計を「プログラムの中でどのように関心事を分割するかの検討」の意味で使っています。
前者の例として、「現実の会社・従業員などをそれぞれクラスとして考え、相互作用を分析する」、後者の例として「レイヤードアーキテクチャを採用し、データベースへの書き込みを抽象化する」などが挙げられます。
このモデリングと設計の区別は自分がイメージしてるものなので正式な用語の定義とずれがあるかもしれませんが、いずれにしろこういう部分は「オブジェクト指向プログラミング」の本質的な話ではない、と考えています。

それでもこういった話がOOPの話に混じってきがちなのは、オブジェクト指向プログラミングと全く無関係ではなく相互に絡みあう話だからだと思います。
自分としてもプログラミングは設計とは独立した工程だ、とまでは思っておらず、あくまで「議論領域としてプログラミングパラダイムの話と設計の話は切り分けたほうが建設的である」という程度の感覚です。

OOPとは結局なんなのか

だいぶ長くなったので一旦まとめましょう。

アラン・ケイのOOPは「プログラムの全ての構成要素をオブジェクトとメッセージングで表現するプログラミング手法」でした。
一方ストラウストラップのOOPは「静的な抽象データ型としてのクラスを用いたプログラミング手法」でした。
前者がいろいろなものを動的に交換可能にするのに対し、後者がクラスという抽象データ型を用いてより静的にプログラムを整理しようという方向性であることから、一口にOOPといっても方向性はだいぶ異なっているのが分かるかと思います。

ところで、アラン・ケイのOOPでは「メッセージング」という、文や式と同じレベルでプログラムを構成する概念が出てきていました。
一方、ストラウストラップのOOPではあくまで「抽象データ型」という大きな括りの話に終始しており、より細かいレベルの話は出てきません。
実際、C++はマルチパラダイムの言語であり、基本的な処理の仕組みはCを引き継いて命令的な設計のままになっています。
このことから、実はアラン・ケイのOOPとストラウストラップのOOPは方向性が異なるだけではなく、議論領域のレイヤーも異なっていることがわかります。

既存の概念とレイヤーを合わせて整理すると、以下のような感じになりそうです。

  • 基本的な処理単位のレベル
    • 文(命令型プログラミング)
    • 式(関数型プログラミング)
    • メッセージ式(アラン・ケイのOOP)
  • 処理・データをまとめる単位のレベル
    • モジュール
    • 動的なクラス・オブジェクト(アラン・ケイのOOP)
    • 静的なクラス・オブジェクト(ストラウストラップのOOP)

更にこの上位に設計・モデリングのレイヤーがありますが、割愛します。

何がいいたいのかというと、「ストラウストラップのOOPと命令型プログラミング・関数型プログラミングは議論レイヤーが合わないから比較できないよ」という話です。
関数型についての記事でも書いたのですが、特にオブジェクト指向は設計から基本的な処理単位まで幅広いため、議論のレイヤーがあっているかは注意しないと空中戦に陥りやすいような気がしています。

まとめ

前の記事と対応させて「OOPのメリット・デメリット」や「OOPとは関係のない項目」もまとめようと思ったんですが、力尽きました…
今回はNGK2015Bを犠牲にして、なんとか担当日中に書き上げる事ができました。
もっと早く着手すればいいんでしょうが、私の開発スタイルはDDD(Deadline Driven Development,  〆切駆動開発)なので…

この記事を書くに当たり、以下を参考にさせていただきました。

第2のドワンゴ Advent Calendar、明日6日は@sileさんです。
お楽しみに!


2 Comments

  1. 正しく丁寧にOOPを切り分けた説明、すばらしいと思います。

    細かいことで恐縮ですがすこし指摘させてください。Smalltalkの文法は基本的に「レシーバー メッセージ」で二項メッセージ式である2+3もこれに準じます。したがって、「2というオブジェクトに対して+というメッセージを3を引数に送信」ではなく「2 に対して + 3 というメッセージを送信」が正しいです。関連して、この場合“+”はメッセージではなく「セレクタ」(より正確には「メッセージセレクタ」)です。なおセレクタは通常の言語におけるメソッド名に相当します。「メッセージ=セレクタ+引数」という構成になっていると考えると統一した説明がしやすいと思います。

    似たような誤りで、ifTrue:ifFalse:セレクタを用いたメッセージの例のところで「ifTrue:セレクタで受け取ったブロックを」とありますが、この場合、ifTrue:やifFlase:は独立したセレクタではなく「キーワード」と呼びます(ただこれはそう呼ぶだけで、セレクタのような言語要素ではありません。為念)。たとえば「キーワード1キーワード2」のように構成されるセレクタならばキーワードで分断して「キーワード1 引数1 キーワード2 引数2」と配置することでメッセージを成すと理解するとよいと思います。

    蛇足ですが、コロンを含めてセレクタ(あるいはキーワード)を扱っていただけているのは(とかく他言語使いの方にはルーズに扱われがちなので)、とてもよく理解していらっしゃるなと感心しました。ありがとうございます。

    • kokuyouwind

      2015年12月6日 at 11:10 AM

      ご指摘ありがとうございます。
      なるべく正確な記述を心がけたのですが、Smalltalkの用語には詳しくないため、メッセージ・セレクタ・キーワードをやや混同してしまっていました。

      セレクタは通常の言語におけるメソッド名に相当します。「メッセージ=セレクタ+引数」という構成になっていると考えると統一した説明がしやすいと思います。

      この説明、シンプルで大変分かりやすかったです、ありがとうございます!
      ご指摘頂いた内容を元に、Smalltalkの例に関する説明を修正しました。
      まだ怪しい点があればご指摘いただければと思います。

コメントを残す

Your email address will not be published.