オブジェクト指向はほとんどのプログラミング初心者にとって「よくわからない」存在だと思います。
その原因は、用語がカタカナ語でとっつきにくいことやオブジェクト指向というものそれ自身の抽象性に加えて、「猫ならニャー、犬ならワンと鳴く」だとか「スーパークラスの車を継承してスポーツカーを作る」というような、分かったつもりにさせるだけで、現実にどんなメリットがあるのかよくわからない説明しか与えられない状況にあると信じています。

オブジェクト指向は、大規模化してもわかりやすいコードを維持するための考え方(テクニック)の一つです。なので、C言語はオブジェクト指向ではなくてC++はオブジェクト指向だ、というような説明がなされることもありますが、C++がオブジェクト指向でプログラミングしやすいように設計してあるというだけで、C言語でもオブジェクト指向プログラミングは可能です。具体的にその考え方に触れる前に、まずオブジェクト指向を使う目的から議論を始めようと思います。

なぜオブジェクト指向を使うのか


オブジェクト指向を使う目的は、
  1. 機能ごとにコードを区切る/まとめることで管理しやすくなる
  2. 複数の「状態」が管理できる
  3. うまく設計すれば機能の再利用ができる
などが考えられます。

1. 機能ごとにコードを区切れるので管理しやすい


オブジェクト指向プログラミングで実装できる機能は、関数を使って適当に実装しても実現できたりします。ではなぜわざわざオブジェクト指向にするのでしょう。オブジェクト指向で設計されたプログラムは一般に読みやすく管理しやすいため、ヒューマンエラーを未然に防ぐことができます。あるいはある機能を完全にブラックボックス化することで、その機能を使う人が余計なことを考えずに機能だけ享受したりできるようになるのです。

例えば今、音楽ファイルを管理しようというとき、ファイル名に直接、

宇多田ヒカル_Deep River_01_SAKURAドロップス.mp3

なんて書き込んだのをずらずら1000曲以上並べた日には管理しづらくて仕方ありません。それでもこれだけ丁寧なファイル名が付けられているならいいほうで、

SAKURAドロップス.mp3

などとしてしまうと、もはやファイル名だけではアーティスト名で選別することすらできません。(iTunesなどのソフトが使えないとすれば) フォルダ機能を使って、

宇多田ヒカル / Deep River / 01.SAKURAドロップス.mp3

のような構造を作るほうが好ましいでしょう。これなら、宇多田ヒカルの曲だけを別のフォルダにコピーするなどの作業が非常に簡単になります。プログラミングの世界でも、文字列操作の機能なら文字列操作だけの機能、猫に関する機能なら猫だけの機能を、それぞれの種類ごとにグループ分けしてやると管理がしやすくなります。この種類ごとのグループ(正確には単に種類)をオブジェクト指向の世界ではクラスと言います。

これは可読性の一例であり、オブジェクト指向では他にもコードを読みやすく、分かりやすくする工夫がなされます。

もう一つ挙げた利点であるブラックボックス化について説明します。例えばある図形の面積を取得する機能を作るとしましょう。このとき、その図形の面積は繰り返し使う可能性があると仮定します。この機能を実現する方法として最も単純なのは、呼び出されるたびに毎回面積を計算することです。しかし、求めた面積は繰り返し使うことが分かっているにも拘わらず、そのたびにわざわざ計算するのは馬鹿馬鹿しい話です。そこで求めた面積を一時的に保存しておくことにしました。オブジェクト指向を使わなければ、
  • 面積を保存しておく変数: areaSize
  • 面積を計算して変数areaSizeに代入する関数: measureAreaSize(targetArea)
のような変数と関数で管理する方法が考えられます。ここで、targetAreaは対象の図形です。(この図形targetAreaどのように定義されていても構いません。イメージしやすいように一例を挙げるならば、targetArea[640][480]のような2次元配列で、各要素が640x480サイズの画像の1ピクセルに対応している構造などが考えられるでしょう。この場合、面積とは真っ白でないピクセルを数え上げものを意味する、などと定義できるでしょう。) 初めてその図形の面積を使うときに、measureAreaSize(targetArea)関数を実行してareaSize変数を更新し、areaSize変数の値を直接読んで面積を得る、という運用をします。オブジェクト指向の説明でよくあるような「このareaSize変数を間違って誰かが直接書き換えてしまったら値の整合性が…!」なんてのはこの場合、心配のし過ぎじゃないかと思います(静的型付き言語の場合)が、しかしこの方法では、今まで考えていた図形とは別の、新しい図形を考えることになった際に、measureAreaSize(targetArea)関数を呼び出すの忘れてしまって、間違った面積で目的の処理を実行してしまうかもしれません。面積を求める以外部分でも似たような仕様で実装するとどんどん複雑化しますし、更に仕事で複数人数で開発している場合などは間違える危険性がかなり高くなるでしょう。

そこでオブジェクト指向では、例えば、図形というオブジェクト(物/対象)に着目してArea(領域)という名前のクラスを作って、次のように管理します:
  • 領域についてのクラス: Area
    • 対象の図形: area
    • 面積を保存する変数: size
    • 面積が既に計算されているかどうかを表す変数: sizeIsAvailable
    • 面積を取得する関数: getSize()
Areaクラスより下はすべてAreaクラスの配下にあります。areaは最初の例のtargetAreaと同じもので、sizeは同じくareaSizeです。最初にAreaクラスを呼び出すときにarea変数を設定し、getSize()関数で面積を取得する、というのが基本的な使い方です。Areaというグループ(クラス)で区切ることで、各々の変数名/関数名の短縮に成功しているのにも注目してください。(但し、measureがgetに変わっているのは本質ではありません。面積の計測から取得へと行為の意図が変わったのを反映した結果ですが、無視しても大丈夫です。) では内部ではどのような処理をしているのでしょう。Areaクラスが最初に呼び出された時に、sizeIsAvailable変数をfalseで初期化しておきます。そして、getSize()関数が呼び出されるとsizeIsAvailable変数を確認し、falseならその場で面積を計算しsize変数に代入、sizeIsAvailable変数をtrueに更新して、更にsize変数を戻り値として返します。一方、sizeIsAvailable変数がtrueだった場合、計算せずにそのままsize変数を戻り値として返します。ここで重要なのは、この機能を使うプログラマはsizeIsAvailable変数について全く考慮する必要もなければ、存在を知っている必要さえありません。このようにクラスの内部の処理や構造を隠蔽することでそのプログラマが余計なことを考えなくても利用できるようにすることをカプセル化と言います。(よく、変数をprivate宣言してareaやsizeなどの内部変数に直接触れないようにすることがカプセル化だなどと説明されますが、それはカプセル化の手法の一つに過ぎず、本質ではありません。)

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クラスを実現出来ます。

オブジェクト指向とは結局何なのか


オブジェクト指向に於いて、オブジェクトとはコンピュータ処理の対象(英語でオブジェクト)のことであり、クラスとはオブジェクトの種類(英語でクラス)のことです。すなわち、オブジェクト指向プログラミングは、処理対象を種類ごとにモデル化し、その性質や状態について考えながらプログラミングする手法であると定義できるでしょう。このモデル化によりプログラムの構造化が確立し、先に述べたような恩恵をプログラミングの世界にもたらしました。この記事が、皆様がその恩恵を受けるための手助けになることを願っています。

参考文献