その原因は、用語がカタカナ語でとっつきにくいことやオブジェクト指向というものそれ自身の抽象性に加えて、「猫ならニャー、犬ならワンと鳴く」だとか「スーパークラスの車を継承してスポーツカーを作る」というような、分かったつもりにさせるだけで、現実にどんなメリットがあるのかよくわからない説明しか与えられない状況にあると信じています。
オブジェクト指向は、大規模化してもわかりやすいコードを維持するための考え方(テクニック)の一つです。なので、C言語はオブジェクト指向ではなくてC++はオブジェクト指向だ、というような説明がなされることもありますが、C++がオブジェクト指向でプログラミングしやすいように設計してあるというだけで、C言語でもオブジェクト指向プログラミングは可能です。具体的にその考え方に触れる前に、まずオブジェクト指向を使う目的から議論を始めようと思います。
なぜオブジェクト指向を使うのか
オブジェクト指向を使う目的は、
- 機能ごとにコードを区切る/まとめることで管理しやすくなる
- 複数の「状態」が管理できる
- うまく設計すれば機能の再利用ができる
1. 機能ごとにコードを区切れるので管理しやすい
オブジェクト指向プログラミングで実装できる機能は、関数を使って適当に実装しても実現できたりします。ではなぜわざわざオブジェクト指向にするのでしょう。オブジェクト指向で設計されたプログラムは一般に読みやすく管理しやすいため、ヒューマンエラーを未然に防ぐことができます。あるいはある機能を完全にブラックボックス化することで、その機能を使う人が余計なことを考えずに機能だけ享受したりできるようになるのです。
例えば今、音楽ファイルを管理しようというとき、ファイル名に直接、
宇多田ヒカル_Deep River_01_SAKURAドロップス.mp3
なんて書き込んだのをずらずら1000曲以上並べた日には管理しづらくて仕方ありません。それでもこれだけ丁寧なファイル名が付けられているならいいほうで、
SAKURAドロップス.mp3
などとしてしまうと、もはやファイル名だけではアーティスト名で選別することすらできません。(iTunesなどのソフトが使えないとすれば) フォルダ機能を使って、
宇多田ヒカル / Deep River / 01.SAKURAドロップス.mp3
のような構造を作るほうが好ましいでしょう。これなら、宇多田ヒカルの曲だけを別のフォルダにコピーするなどの作業が非常に簡単になります。プログラミングの世界でも、文字列操作の機能なら文字列操作だけの機能、猫に関する機能なら猫だけの機能を、それぞれの種類ごとにグループ分けしてやると管理がしやすくなります。この種類ごとのグループ(正確には単に種類)をオブジェクト指向の世界ではクラスと言います。
これは可読性の一例であり、オブジェクト指向では他にもコードを読みやすく、分かりやすくする工夫がなされます。
もう一つ挙げた利点であるブラックボックス化について説明します。例えばある図形の面積を取得する機能を作るとしましょう。このとき、その図形の面積は繰り返し使う可能性があると仮定します。この機能を実現する方法として最も単純なのは、呼び出されるたびに毎回面積を計算することです。しかし、求めた面積は繰り返し使うことが分かっているにも拘わらず、そのたびにわざわざ計算するのは馬鹿馬鹿しい話です。そこで求めた面積を一時的に保存しておくことにしました。オブジェクト指向を使わなければ、
- 面積を保存しておく変数: areaSize
- 面積を計算して変数areaSizeに代入する関数: measureAreaSize(targetArea)
そこでオブジェクト指向では、例えば、図形というオブジェクト(物/対象)に着目してArea(領域)という名前のクラスを作って、次のように管理します:
- 領域についてのクラス: Area
- 対象の図形: area
- 面積を保存する変数: size
- 面積が既に計算されているかどうかを表す変数: sizeIsAvailable
- 面積を取得する関数: getSize()
measureAreaSize(targetArea)関数を実行するとareaSize変数に図形の面積を代入する、という仕様がよくないんだ! measureAreaSize()関数をgetAreaSize()関数に書き換えて、areaSizeIsAvailable変数を用意して、例にあ るAreaクラスのgetSize()関数と同じ手法で計算すれば同じじゃないか! という反論があると思いますが、内部の挙動を隠蔽するその手法こそが正にカプセル化なのです。(本当はareaSize変数、areaSizeIsAvailable変数を外部から触れないように隠蔽する操作も必要ですが、手法としてはカプセル化です。) オブジェクト指向とはただの考え方でしたから、それだけでオブジェクト指向プログラミングにかなり近づいていると言えるでしょう。(領域という概念をオブジェクトとする、というプロセスが抜けているのでオブジェクト指向としては不十分ではあります。)
カプセル化のメリットはその機能を使うユーザーにとって分かりやすいという事の他に、変更に強いということが挙げられます。例えば、もともとの仕様ではgetSize()関数は毎回愚直に面積を求めていたという場合でも、上のようなsize変数とsizeIsAvailable変数をこっそり導入することで、機能を使う人に全く気づかれずに仕様変更ができます。
さらに、カプセル化されたコードはプログラムの「部品」として高い独立性を持っています。この独立性によりそれぞれの「部品」は他のソフトウェアに容易に移植できますし、複数人で開発する際に、「部品」ごとに別々のエンジニアが担当しても整合性が損なわれにくくなります。
2. 複数の「状態」が管理できる
さて、先ほど私は「最初にAreaクラスを呼び出すときに」と言いましたが、これは正確な表現ではありません。正確には、「Areaクラスのインスタンスを生成するときに」というべきです。ではインスタンスとは何でしょうか。
英語で「インスタンス」といえば「例」という意味であり、「Areaクラスのインスタンス」というのは、つまり「領域の例」という意味になります。領域と言えば世の中には様々な種類の領域があります。図形に限って考えても丸や三角や四角、イラストのある線で囲まれた部分も領域です。そのうちの一つが「領域の例」つまりプログラム上では「Areaクラスのインスタンス」に当たります。抽象的で分かりにくいですね。では前節で挙げた例を再び取り出して具体的に考えてみましょう。
オブジェクト指向を用いない、古典的な手法について、前節で「新しい図形を考えることになった際に、measureAreaSize(targetArea)関数を呼び出すの忘れてしまって、間違った面積で目的の処理を実行してしまうかもしれません」と言いました。この原因は、areaSize変数がひとつしか無いことにあります。領域の実例は先程挙げたように無数に存在するにもかからず、その面積をすべて1つのareaSize変数で管理しようとすると、今areaSize変数の値は一体どの領域の面積を指しているのか分からなくなります。
一方、オブジェクト指向を用いた場合は、新しい図形を考える際には新しい「領域の例」、即ち、新しい「Areaクラスのインスタンス」を生成します。インスタンスは各々が独立したarea、size、sizeIsAvailable変数を持っており、設計を間違えない限り、同じインスタンスに属するこれら3つの変数は互いに同じ図形(領域)についての情報です。先ほどのareaSize変数のように、どの図形の面積のことを言っているのか分からないという状況には本質的になりえません。また、新しい図形について考えるときは新しいAreaクラスのインスタンスを生成する、という構図は人間の直感と合致しており、コードが書きやすく、また理解しやすくなるでしょう。
さて、図形の面積というのはその図形の「状態」を表す情報の一つといえるでしょう。Areaクラスは図形(領域)の「状態」を管理する機能群であると見做すことも出来ると思います。この「状態」というものはコンピュータに於いては非常に重要な役割を果たします。コンピュータは、例えば、入力を待っている状態、データをダウンロードしている状態、あるいはユーザーが作成している文章の状態など、その時の「状態」に合わせて挙動を変化させる必要があります。「状態」を管理する機能群と見做せるところのクラスというものは、コンピュータを意図通り動かす上で非常に強力なツールとなることがわかると思います。
「状態」を管理するのはもちろんオブジェクト指向でなくても可能です。図形の面積の例では、図形のそれぞれに番号をつけ、areaSizeを配列にして、図形ごとに面積を保存することで可能になるでしょう。しかし、例えばある図形のデータが要らなくなった時に、その図形に関連するデータを1個ずつ削除していくのは面倒です。ならばとareaSizeとtargetAreaをセットで管理すること(C言語なら構造体、Pythonならタプルなど)にする訳ですが、そうこうしてるうちにどんどんオブジェクト指向での書き方に近づいてきたのが分かるでしょうか。オブジェクト指向は、「状態」をオブジェクトという枠組みに取り込み、隠蔽して管理しやすいインターフェイスで操作できるようにする確立した手法なのです。
(ところで、状態によって挙動が変わるという性質は、ある状態のために書いたコードをコピペして別種の状態のために使おうとしてもそのままでは動かないということを意味し、また、昨今のマルチコアCPU上で並列処理をしようという時に状態があちらこちらから書き換えられて管理が非常に難しいという状況を生み出しています。このとこから、状態にとらわれないプログラミング、換言すればコンピュータ側の理屈ではなくて人間側の理屈でプログラミングができる関数型言語が注目されていたりします。興味があれば調べてみましょう。)
3. うまく設計すれば機能の再利用ができる
資源をうまく再利用した綺麗な実装というのは難しいので、小規模なコードならそんなことを考えずにゴリゴリプログラミングしたほうが効率がいいことが多いのですが、大規模かつよく利用されるシステムやライブラリなどでは、再利用可能性は非常に重要なオブジェクト指向の特徴です。
オブジェクト指向プログラミング言語の多くは継承の仕組みを持っています。継承とは、すでにあるクラスの一部を書き換えて、別のクラスを作成する機能です。
例えば将棋の次の手を考える人工知能(AI)を完成させたが、初心者が対戦するには強すぎることが分かったとします。こういう時にAIクラスを継承し、思考ルーチンの一部だけをもっと弱いものに書き換えたStupidAIクラスというものを作ることができます。AIクラスは数多くの変数や関数で構成されていると予想されますが、うまく実装すればその関数の内の一つを実装しなおすだけでStupidAIクラスを実現出来ます。
オブジェクト指向とは結局何なのか
オブジェクト指向に於いて、オブジェクトとはコンピュータ処理の対象(英語でオブジェクト)のことであり、クラスとはオブジェクトの種類(英語でクラス)のことです。すなわち、オブジェクト指向プログラミングは、処理対象を種類ごとにモデル化し、その性質や状態について考えながらプログラミングする手法であると定義できるでしょう。このモデル化によりプログラムの構造化が確立し、先に述べたような恩恵をプログラミングの世界にもたらしました。この記事が、皆様がその恩恵を受けるための手助けになることを願っています。
参考文献
- 疑りぶかいあなたのためのオブジェクト指向再入門
http://kmaebashi.com/programmer/object/index.html - オブジェクト指向
http://www.aerith.net/design/object-j.html