「いつもソートされたリスト」は「リスト」と言えるか?

作成:2008年1月28日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   オブジェクト指向

オブジェクト指向言語では、親クラスが持つメソッドを、子クラスでオーバーライドして、動きを変えることができます。同じメソッドでも、子クラスごとに違った動作をすることを、ポリモーフィズム(多態性)と呼びます。

動きを変える、と言っても、オーバーライドするメソッドの機能や役割は、親クラスで決められています。子クラスでは、親クラスの仕様に合わせて、メソッドを作らなければなりません。

この時、オーバーライドするメソッドの仕様だけでなく、親クラスが持つすべてのメソッドに、注意を払う必要があります。オブジェクト指向では、メソッドは互いに無関係ではありません。それらが組み合わされてどのように動作すべきかが、クラスの仕様として定められています。個々のメソッドは、そのクラス全体の仕様に沿った動きをしなければなりません。

逆に、多くの人に継承される抽象クラスやインターフェースを作る時は、個々のメソッドの仕様だけでなく、クラス全体の仕様も、きちんと決めておく必要があります。それが曖昧になっていると、子クラスごとに勝手な解釈をしてしまうことになり、ポリモーフィズムが破綻してしまいます。

目次

  1. 「いつもソートされたリスト」とは何か
  2. 「リスト」とは何か
  3. 「リスト」ならこんなに便利
  4. インターフェースが同じなら「リスト」の仲間、それがポリモーフィズム
  5. インターフェースが同じだけでは、正しく動作しないこともある
  6. 「いつもソートされたリスト」は「リスト」と言えるか?

「いつもソートされたリスト」とは何か

カプセル化とは何か 〜仕様と実装は別物です〜」では、「いつもソートされたリスト」のクラスを作りました。これは、追加された順に関係なく、いつもデータがソートされているリスト、というもので、次のようなメソッドを持っていました。

メソッド 説明
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言語の関数を作るのなら(大域変数や静的変数があれば別ですが)、それを一回呼び出した時の動作だけを考えれば済みます。しかし、オブジェクト指向言語では、クラス全体を見渡す、もっと広い視野を持つ必要があります。

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.