「いつもソートされたリスト」とは何か
「カプセル化とは何か 〜仕様と実装は別物です〜」では、「いつもソートされたリスト」のクラスを作りました。これは、追加された順に関係なく、いつもデータがソートされているリスト、というもので、次のようなメソッドを持っていました。
メソッド | 説明 |
---|---|
add | データを1つリストに追加する。 |
getAt(i) | i番目に小さなデータを取り出す。 |
「リスト」とは何か
ここで、あなたが「いつもソートされたリスト」を作る前に、もっといろいろな人が、いろいろなリストのクラスを作っていた、としましょう。
そして、それらを括るものとして、「リスト」を表す共通のインターフェース(抽象クラス)も、すでに用意されていた、としましょう。
「リスト」のインターフェースは、次のようなメソッドを持っています。
メソッド | 説明 |
---|---|
add | データを1つリストに追加する。 |
getAt(i) | i番目のデータを取り出す。 |
この2つのメソッドは、あなたが作った「いつもソートされたリスト」と、名前も、引数の型も、まったく同じです。
さて、ここで問題です。あなたが作った「いつもソートされたリスト」も、この「リスト」インターフェースを継承(実装)させて、他のいろいろなリストの仲間入りをさせた方が良いでしょうか?
「リスト」ならこんなに便利
「リスト」インターフェースを継承させると、いろいろと便利になります。
たとえば、「リスト」を引数に渡すと、いろいろな処理をやってくれる便利なライブラリが、すでに用意されているかもしれません。「リスト」の中から重複しているデータを探し出したり、「リスト」の内容をすべてファイルに書き出したり、2つの「リスト」が一致しているかどうかを調べてくれたり…。
「リスト」を継承すれば、わざわざ自分で作らなくても、「いつもソートされたリスト」でもそうした便利な機能を利用することができるようになります。
インターフェースが同じなら「リスト」の仲間、それがポリモーフィズム
ここで、もう一度、「リスト」のメソッドを見てみましょう。
メソッド | 説明 |
---|---|
add | データを1つリストに追加する。 |
getAt(i) | i番目のデータを取り出す。 |
「いつもソートされたリスト」も、同じメソッドを持っています。「いつもソートされたリスト」のgetAtメソッドは、小さい順にデータを取り出せるようになっていますが、i番目のデータを取り出せることには変わりありません。つまり、「いつもソートされたリスト」のメソッドは、「リスト」のメソッドの仕様を満たしています。
ですので、「いつもソートされたリスト」も、「リスト」インターフェースを継承して、リストの仲間入りをさせることにしましょう。
「リスト」の仲間たちは、すべて同じインターフェースを持っていますが、それぞれ、メソッドの実装は異なっています。つまり、それぞれのクラスは、少しずつ違った動きをします。
addメソッドを呼んだ時、「最小限のメモリしか消費しないリスト」なら、その場でデータ1つ分のメモリが新たに確保されるでしょうが、「メモリ確保・解放が頻繁に起こらないようにしたリスト」であれば、たいてい、addメソッドを呼ぶ前とメモリ消費量は変わらないでしょう。
getAtメソッドについては、「いつもソートされたリスト」なら、小さい順にデータが取り出されます。しかし、それ以外のクラスでは、おそらく、追加した順にデータが取り出されることでしょう。
このように、インターフェースが同じでも、それを継承した子クラス(具象クラス)によって動作が異なることを、ポリモーフィズム(多態性)と呼びます。
インターフェースが同じだけでは、正しく動作しないこともある
ところが、インターフェースが同じだからといって、かんたんに仲間に入れてしまうと、問題を生じることもあります。
例えば、「リスト」を使うあるソフトウェアでは、次のような処理が行われているかもしれません。
ここでは、2つのスレッドが並列に動いており、一方がデータを追加しながら、同時にもう一方で、データを1つずつ取り出しています。
これは、「リスト」インターフェースを継承するたいていのクラスについて、正しく動作するでしょう。データを追加した順に取り出せるだけの単純なリストなら、一部のデータを取り出した後で、さらにデータを追加しても、末尾に追加されるだけでしょうから。
しかし、「いつもソートされたリスト」に対しては、この処理では、正しくデータを取り出せません。データを追加するたびにソートされ、データの順序が変わってしまうためです。例えば、大きいデータから順に登録したとすると、取り出す方のスレッドでは、いつも同じデータしか取り出せないことになってしまいます。
「いつもソートされたリスト」は「リスト」と言えるか?
「いつもソートされたリスト」は、「リスト」インターフェースと同じく、addとgetAtという、2つのメソッドを持っていました。そして、どちらのメソッドも、「リスト」インターフェースのメソッドの仕様を満たしていました。それにも関わらず、「リスト」として「いつもソートされたリスト」を渡すと、正しく動作しないケースがありました。
ここで改めて、本稿の問題を考えてみましょう。「いつもソートされたリスト」は、「リスト」インターフェースを継承させても良いものでしょうか? 言い換えれば、「いつもソートされたリスト」は「リスト」と言えるのでしょうか?
その問いには、実は、YesともNoとも答えられません。すべて、「リスト」の仕様次第なのです。
「リスト」が持つ2つのメソッドの仕様は、次の通りでした。
メソッド | 説明 |
---|---|
add | データを1つリストに追加する。 |
getAt(i) | i番目のデータを取り出す。 |
しかし、個々のメソッドの機能は書かれていても、それを組み合わせて使った時の動きは、まったく書かれていません。つまり、メソッドの仕様はあっても、「リスト」のクラスそのものの仕様が、決まっていなかったのです。
もしも「リスト」が、データを1つずつ取り出している途中にはデータを追加してはいけない、という仕様であれば、「いつもソートされたリスト」は、「リスト」を継承しても良いことになります。この場合、さきほどの2つのスレッドが並列に動くような処理は、「リスト」の仕様に違反したものとなりますので、正しく動作しないのは当然です。
逆に、もしも「リスト」が、データを1つずつ取り出している途中にデータを追加しても、正しくデータを取り出せる、という仕様であれば、「いつもソートされたリスト」は、「リスト」を継承してはいけないことになります。個々のメソッドがどれほどそっくりに見えても、クラスの仕様が「リスト」に反しているためです。
もし後者なら、「いつもソートされたリスト」は、「リスト」ではない、何か別のものである、ということになります。
クラスの設計をする時は、個々のメソッドの単体の動きを決めるだけでは、仕様としては十分ではありません。クラスが提供するメソッドを組み合わせて呼び出したり、繰り返し呼び出したりした時に、クラスがどのような振る舞いをするか、そこまで考えて、初めてクラスの仕様を決めたことになります。
C言語の関数を作るのなら(大域変数や静的変数があれば別ですが)、それを一回呼び出した時の動作だけを考えれば済みます。しかし、オブジェクト指向言語では、クラス全体を見渡す、もっと広い視野を持つ必要があります。