図1 順接,分岐,反復の処理手続き
図1 順接,分岐,反復の処理手続き
[画像のクリックで拡大表示]
図2 スタックの構造
図2 スタックの構造
[画像のクリックで拡大表示]
図3 Rubyで記述したスタック操作
図3 Rubyで記述したスタック操作
[画像のクリックで拡大表示]
図4 図3のコードを配列を使って実現した例
図4 図3のコードを配列を使って実現した例
[画像のクリックで拡大表示]

複雑さという敵

 実際にオブジェクト指向プログラミングを進めるために重要な原則に戻りましょう。

 ソフトウエア開発の最大の敵は複雑さです。人間の脳はあまり複雑なものを扱うことができないのです。記憶力と理解力に限界があるためです。人間は一度に把握できる概念の数に限界がありますし,複数のことを記憶しようとするとたいていいくつかは忘れてしまいます。

 ソフトウエアを実行するコンピュータにはこのような制約はなく,コンピュータはどんなに複雑なソフトウエアだろうとも,どんなに処理するデータが増えようとも処理時間が長くなるだけで処理できなくなることはありません。世の中のデータ処理がどんどんコンピュータ化されるにつれ,ソフトウエアへの要求は増し,開発されるソフトウエアはどんどん複雑になってきています。

 年々性能が向上するコンピュータの処理能力の限界よりも,人間の理解力の限界によるソフトウエア生産性の限界の方が,より厳しい制約になってきています。これだけコンピュータが高速になった現在,多少性能が劣っても,より複雑で,より大規模なソフトウエアをより速く開発できることが期待されているのです。

構造化プログラミングの意味

 このようなソフトウエア開発の複雑さに対する最初の挑戦として登場したのが「構造化プログラミング」です。構造化プログラミングはプログラムの制御の流れ,つまり実行順序を「順接」,「分岐」,「反復」の3種類だけに制限し,かつ共通する処理をサブルーチンとしてくくり出すことを基本とした考え方です(図1[拡大表示])。

 構造化プログラミングが登場する以前には,goto文などを用いて制御の流れを任意の場所に移していました。一方,構造化プログラミングでは制御の流れを3種類に制約しています。制御の流れの複雑さを減らし,加えて,似たような処理のうち異なる部分だけをパラメータとして外側から与えることにより,より抽象的な処理の塊(サブルーチン)として取り扱うようにするためです。

 構造化プログラミングが取った「制約」と「抽象化」こそ,ソフトウエアの複雑さを人間が取り扱える範囲内に抑えるために非常に有効な手法なのです。

 制約によって自由度を減らすことで,組み合わせの爆発を抑え,結果が複雑になりすぎないようにしているのです。もっとも,自由度を減らしたために「実現できること」が減っては困ります。どのような制約を導入するかが難しいところです。構造化プログラミングの場合,先ほどの「順接」,「分岐」,「反復」の3種類だけであらゆるアルゴリズムが記述可能だそうですから,自由度と複雑性は減っても記述力は減っていないことになります。

 抽象化の目的はいくつかのまとまりに名前を付けることで,内部の詳細を気にせずに取り扱うことです。「ブラックボックス化」とも言います。ブラックボックスでは入力と出力だけが決まっていて内部の処理を隠しています*2

 例えばサブルーチンは入力となる引数と出力となる戻り値だけが分かれば,内部でどのような処理をしているかを気にせずに利用できます。システムがブラックボックスの組み合わせで構築されていると,システムの複雑な構造がブラックボックスに隠されているので,全体の見通しがよくなります。

 ブラックボックスの内部を含めて考えるとシステム全体の複雑さは変化していません。しかし,内部を考えなければ,システムの複雑さを人間の取り扱える範囲に抑えることができます。さらに,内部が隠されているということは,入力と出力が同じであれば,内部の処理の方法をどのように変更しても外部に影響がない,つまり,将来の変更に対して強いことを意味します。ソフトウエアに変更はつきものですから,このような変化に強い性質は大変望ましいことです。

 このように構造化プログラミングは制御の流れの複雑さに対抗するために,制約と抽象化という武器を使って対抗しようという試みでした。構造化プログラミングは成功し,すっかり定着しました。いまやほぼすべてのプログラミング言語は構造化プログラミングに従った制御構造を備えていますし,今さらわざわざ構造化プログラミングと呼ぶまでもなく常識となっています。

データも抽象化

 しかし,プログラムに含まれるものは制御構造だけではありません。実際には処理の対象となるデータがあって初めて処理が完成します。構造化プログラミングによって制御の流れの複雑さを抑制しても,取り扱うデータの数や種別が増大すると全体としての複雑さは抑えきれません。オブジェクト指向プログラミングは,データによる複雑さへの対抗手段として登場したのです。

 世界最初のオブジェクト指向プログラミング言語Simulaがシミュレーション用言語であったことを思い出してください。シミュレーションのように処理するデータの種別が多様になると,プログラムの処理の内容と処理の対象となるデータを別々に取り扱うことによる複雑さが無視できなくなります。正しい結果を得るためには,処理とデータを常に一貫性を保つ必要があり,そのコストが馬鹿になりません。そのために使われたテクニックが「抽象データ」です。

 抽象データとは,データと手続きをまとめたものです。データの中身は所定の手続きを通さないと見えません。データとその取り扱い方法をまとめてブラックボックス化できるわけです。

 データ構造の一例としてスタックを考えてみましょう。スタックとは先に入れたものが後から出てくるデータの入れ物です*3。ファストフードで使うトレイを積み重ねたものをイメージしていただくとよいでしょう(図2[拡大表示])。スタックに対しては2種類の操作だけが可能です。データをスタックに積む(push)とは一番先頭にデータを置くことであり,データをスタックから取り出す(pop)とは先頭のデータ(一番最近にpushされたデータ)を取り出すことです。

 このスタックの操作をRubyで記述しました*4図3[拡大表示]を見るとスタックの操作にpushとpopだけを使っていることがはっきり分かります。これ以外の方法では,スタックの内部にアクセスできません。スタックのような抽象データを使わずに同じデータ構造を実現しようとすると図4のようになります。図4[拡大表示]では配列とインデックス(添え字)を使うことスタックを表現しています。図3と比較すると,どちらが簡単かは一目見ただけで分かりますね。

 図3のプログラムの方が図4のプログラムよりも優れている点はいくつかあります。第一に図4のプログラムでは「配列とインデックス」という内部構造が見えてしまっていますが,図3ではStackというデータ構造の中に隠しています。図3の方法でスタックを使う人はスタックがどのように実装されているか気にする必要がありませんし,将来なんらかの事情でスタックの実装が変化してもプログラムを修正する必要はありません。

 それに対して図4のプログラムでは実装の変更に伴い,必ずプログラムの修正が必要になります。スタックを利用している場所すべてを変更しなければならないので,プログラムの規模が大きくなればなるほど変更が大変になります。これではプログラムを改善するためであっても,スタックの実装を変更したくないと尻込みしてしまうでしょう。「変化に強い」というのは抽象データの大きな利点です。

 もう一つの利点は,処理のイメージがつかみやすいことです。例えばスタックにデータをプッシュする操作は,図3では次のように表現しています。

stack.push(5)

 一方,図4では以下のように記述しています。

stack[sp] = 5
sp += 1

 図3の方が「スタックにプッシュする」という操作が直接的に表現できています。データを操作する側は,図4のような処理の詳細ではなく,むしろ「何をしようしているか」に興味があります。そのため,処理の詳細が隠される抽象データの方がコードが明確になり,目的にかなうのです。

 イメージしやすいのは個別の操作だけではありません。抽象データは「特定の操作に反応するインテリジェントなデータ」としてとらえることができます。各種の刺激に応じて反応を示す現実世界の実体との関連付けが容易になるというメリットがあります。

 抽象データにより,プログラムがとりあつかうデータが単なる数値や文字列のようなあまり具体性のないものから,人間の頭脳がイメージしやすいより具体的なものに変化します。コードの「抽象化」によりイメージが「具象化」しているわけです。このようなインテリジェントなデータを,現実世界の実体(もの)との対応から,しばしば「オブジェクト」と呼び,オブジェクト指向プログラミングの名前の由来となっています。