図5 Stackクラスの実装例
図5 Stackクラスの実装例
[画像のクリックで拡大表示]
図6 図5のクラスを継承したクラスの例
図6 図5のクラスを継承したクラスの例
[画像のクリックで拡大表示]

ひな形

 プログラム中にオブジェクトが登場する場合,同じ動作をするものが多いでしょう。交通シミュレーション・プログラムでは車を表すオブジェクトや信号を表すオブジェクトが数多く登場します。同じ種別のオブジェクトはみな同じ性質を持っていますが,位置や色などそれぞれ状態が異なります。

 抽象化の原則から言えば,同じものが数多く登場する場合にはグループ化してまとめてしまうべきです。このことをDRY(Don't Repeat Yourself)原則と呼ぶこともあります。

 既に見てきたように,プログラムの重複は諸悪の根源です。重複があるとプログラムの修正が広範囲に及びますから,修正コストが高くなります。変更カ所が複数に及び,そのうちのたった1つでも修正を忘れてしまうとプログラムは正常に動作しません。重複はプログラムの信頼性を低下させる可能性が高いといえます。

 さらに言えば,重複のあるプログラムは冗長ですから,人間が読む時にプログラムの「意図」を解釈するコストも増大します。コードの重複が多い図4のプログラムが,重複のない図3のプログラムよりも意図を理解しにくかったことを思い出してください。コンピュータはプログラムが読みにくいかどうか,重複があるかどうかなど気にしません。しかし,人間の方はプログラム開発する間,数え切れないほどプログラムを読み,解釈し,心の中で挙動を想像しています。ですから,プログラムが人間にとって読みやすいかどうかは生産性に直結します。重複が多く冗長で読みにくいプログラムはそれだけで生産性を低下させます。プログラミング中のにコピー・アンド・ペーストを繰り返すのは,重複部分を増やすために推奨されない行為だと言えるでしょう。

 話題をオブジェクトに戻しましょう。同じ種類のオブジェクトがたくさん存在している場合,重複を避けるため同種のオブジェクトをまとめる方法は大きく分けて2つあります。

 一つはクローン型です。元になるオブジェクトのコピーを作ることで,同種のオブジェクトを作り出します。実はオブジェクト指向プログラミング言語ではクローン型は少数派です。SelfやIoなどの言語が採用しています。少々意外なことにJavaScriptもクローン型です。今や最も有名なクローン型オブジェクト指向言語かもしれません。
 もう一つはひな形を用意する方法です。例えるならば,たい焼きの型とたい焼きの関係でしょうか。型があれば同じ形のたい焼きをたくさん作ることができます。このひな形にあたるものをオブジェクト指向プログラミングでは「クラス」と呼びます。同じ種別のオブジェクトはそれぞれ同じクラスに属し,操作や性質を共有します。

 クローン型と違って,クラス型オブジェクト指向言語はひな形となるクラスとひな形から作られたオブジェクトを明確に区別します。たい焼きの型はたい焼きそのものではないことと同じです。クラスとオブジェクトの関係についてさらに例を挙げると,整数クラスと数字の「1」オブジェクト,犬クラスと特定の「ポチ」オブジェクトなどがあるでしょう。クラスと対比し,違いを際立たせるためにオブジェクトのことをしばしばインスタンスと呼びます。呼び方が違うだけでこれまで説明したオブジェクトと同じものを指します。

 クラス型オブジェクト指向言語であるRubyでは*5,クラスを定義する際に「class文」を使います。図3で登場したStackも,実はStackクラスです。Stackクラスの定義を図5に示しました。

 classの後ろに書かれているのがクラス名です。図5[拡大表示]では,Stackです。Rubyではクラス名の先頭の文字を大文字にするというルールがあります。classから対応する(末尾の)endまでがクラスの定義になります。ここでは3つの手続き,initialize,push,popが定義されています。

 initializeは図3の2行目で呼び出されます。

stack = Stack.new

 スタックが作られる度に呼ばれる初期化のための手続きです。

 図5では,@stack(スタックの実体となる配列)と@sp(配列のインデックス)という2つの変数を初期化しています。Rubyでは「@」で始まる変数はオブジェクトごとに独立した値を持つ変数で,インスタンス変数と呼ばれます。複数のスタックを作った場合には,それぞれ別の@stackと@spを持つことになります。

 pushとpopはスタック操作の手続きです。図4にあるスタック操作を手続きにまとめただけのものです。

 図5のinitializeのようにクラスに対して定義されたオブジェクト内部の操作手続きを「メソッド」と呼びます。

 なお,説明を単純化するため,図5の例では領域チェックなどは行っていません。実際にはインデックスが負にならないかどうかなどのチェックが必要になるでしょう。

似た部分をくくり出す継承

 ソフトウエアの規模が拡大し,中に含まれるクラスが多くなってくると似た性質を持つクラスが複数登場します。既に見てきたように同じことを何度も繰り返すのはDRY原則に違反します。無駄ですし,理解の妨げにもなります。変更のコストが高くなり,生産性も下がります。ですから,似たような性質を持つクラスが複数あるなら,オブジェクトと同様にクラスについても似た部分をくくりだする「仕掛け」があれば良いでしょう。

 「継承」とは似た部分をくくり出す仕掛けです。具体的には,継承はあるクラスの性質を受け継いだ新しいクラスを作る機能です。元になったクラスのことをスーパークラス,新しく作られるクラスのことをサブクラスと呼びます。サブクラスはスーパークラスのすべてのメソッドを受け継いでいますし,必要であれば新しいメソッドを追加できます。さらに受け継いだメソッドを自分の要求に合わせて置き換えることもできます。

 図6[拡大表示]に,図5で定義したStackクラスを継承したFixedStackクラスを示しました。class文のクラス名の後ろにある「< Stack」でスーパークラスを指定しています。これはFixedStackクラスがStackクラスのサブクラスで,メソッドなどのStackクラスの性質をそのまま受け継ぐことを意味しています

 FixedStackクラスではinitializeメソッドとpushメソッドが置き換えられています。それぞれの定義の中で「super」を呼び出していますが,これは,スーパークラスの同じ名前のメソッド(super)を呼び出すことを意味します。このような仕組みによってスーパークラスのメソッドの中身に立ち入ることなく,サブクラスだけでメソッドの動作を変更できます。

 initializeメソッドはオブジェクトの初期化のときに呼ばれますから,以下のように書けば,initializeメソッドが10を引数として呼び出され,10が要素数の上限としてインスタンス変数@limitに設定されます。

stack = FixedStack.new(10)

 図6では,末尾にスタックの先頭要素を取り除くことなく,単に参照するためのtopメソッドを追加しています。スーパークラスに備わっていないメソッドを追加した例と考えてください。

 図6のように既存のクラスを利用して新しいクラスを作り出すことを「差分プログラミング」と呼びます。抽象化によって共通部分をスーパークラスとしてくくり出すことと,既存のクラスを利用して新しいクラスを作ることは同じ手法の両面のようなものです。前者をボトムアップ・アプローチ,後者はトップダウン・アプローチと呼びます。

 さて,Rubyをはじめとする多くの言語では1つのクラスに対して,1つのスーパークラスが決まります。このような継承を「単純継承」と呼びます。継承について,クラスを拡張するトップダウン・アプローチから考えると,1つのクラスからそれを拡張した別のクラスを作るということは,ごく自然なことです。

 しかしながら,これまで見てきたような共通部分をくくり出すというボトムアップ・アプローチから考えると,1つのクラスに1つのスーパークラスというのはかなり厳しい制約です。実は,C++やLispなど複数のスーパークラスを持つことができる言語もたくさんあります。そのような継承を「多重継承」と呼びます。多重継承には利点はもちろん,欠点もあります。多重継承については次回扱うことにします。

*          *          *

 今回はオブジェクト指向プログラミングを,構造化プログラミングに続くソフトウエアの抽象化の流れの一環としてとらえました。オブジェクト指向プログラミングの原則のうち,抽象データと継承について解説しました。抽象化によってオブジェクト指向プログラミングがプログラム開発の生産性を高める効果があることがイメージできたのではないでしょうか。

出典:日経Linux 2005年6月号 130ページより
記事は執筆時の情報に基づいており、現在では異なる場合があります。