カプセル化とは何か 〜仕様と実装は別物です〜

作成:2008年1月18日

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

オブジェクト指向の特徴の1つに、カプセル化があります。カプセル化によって、利用者は、中身の詳しい作りを知らなくても、その振る舞いだけを知っていれば、クラスを利用することができます。

カプセル化によって、クラスの仕様と実装は、まったく別物となります。振る舞いの仕様を満たしていれば、中身をどのように実装しようとも自由です。そのため、時には仕様と実装が大きくかけ離れたクラスができることもあります。

ここでは、具体的な例を見ながら、クラスの仕様と実装の違いを考えてみましょう。

目次

  1. いつもソートされたリストを作ろう
  2. 言われたとおりに作る
  3. ちょっと変わったやり方で作る
  4. もっと特殊なやり方で作る
  5. どれが一番良い方法か

いつもソートされたリストを作ろう

ここでは例として、「いつもソートされたリスト」を作ってみましょう。

C++言語のテンプレートライブラリやJava言語に用意されているリストは、データが追加された順に並んでいます。ここでは、追加された順に関係なく、いつもデータがソートされているリスト、というものを作ってみます。

いつもソートされたリスト

「いつもソートされたリスト」は、次の2つのメソッドを持つものとします。これが、「いつもソートされたリスト」の要求仕様です。

メソッド 説明
add データを1つリストに追加する。
getAt(i) i番目に小さなデータを取り出す。

言われたとおりに作る

「いつもソートされたリスト」を、言われたとおりに素直に作ると、次のようになるでしょう。

class AlwaysSortedList {
        private int[] array = new int[10000];
        private int num = 0;

        public void add ( int value ) {
                array[num++] = value;

                // 並べ替え
                for ( int n = num - 1 ; n > 0 ; n-- ) {
                        if ( array[n] < array[n-1] ) {
                                int swap = array[n];
                                array[n] = array[n-1];
                                array[n-1] = swap;
                        }
                }
        }

        public int getAt ( int i ) {
                return array[i];
        }
}
        

※ここでは例として、Java言語で実装しています。また、単純化のため、データは整数のみ、データ数は最大1万個までとし、エラー処理は省略しました。

addメソッドでデータを追加するたびに並べ替えを行っているので、このリストは、「いつもソートされた」状態になっています。もちろん、getAtメソッドを順に呼び出せば、値の小さなものから順に取り出せます。

ちょっと変わったやり方で作る

しかし、次のようなやり方もあります。

class AlwaysSortedList {
        private int[] array = new int[10000];
        private int num = 0;
        private boolean sorted = true;

        public void add ( int value ) {
                array[num++] = value;
                sorted = false;
        }

        public int getAt ( int i ) {
                // 並べ替え
                if ( sorted == false ) {
                        int[] ar = new int[num];
                        for ( int n = 0 ; n < num ; n++ ) {
                                ar[n] = array[n];
                        }
                        java.util.Arrays.sort(ar);
                        for ( int n = 0 ; n < num ; n++ ) {
                                array[n] = ar[n];
                        }
                        sorted = true;
                }
                return array[i];
        }
}
        

さきほどの実装と違って、addメソッドでデータを追加した時点では、並べ替えを行いません。getAtメソッドを呼んだ時点で、初めて並べ替えを行っています。

実は、この実装では、arrayフィールドの中身は「いつもソートされた」状態にはなっていません。

しかし、このクラスが要求仕様を満たしていない、という訳ではありません。addメソッドを呼べば、データを1つリストに追加できますし、getAtメソッドを呼べば、きちんと、i番目に小さなデータを取り出すことができます。

ここでは、「いつもソートされたリスト」を作っています。「いつもソートされた」とは、sortメソッドなどを呼び出さなくても、いつgetAtメソッドを呼んでもソートされた順にデータが取り出せる、という意味です。これが、クラスの振る舞いの仕様です。

arrayフィールドの中身は、クラスの実装の話です。これが「いつもソートされた」状態かどうかは、クラスの仕様とは関係がないのです。

もっと特殊なやり方で作る

もう1つ、別のやり方を考えてみましょう。

class AlwaysSortedList {
        private int[] array = new int[10000];
        private int num = 0;
        private boolean sorted = true;

        public void add ( int value ) {
                array[num++] = value;
                sorted = false;
        }

        public int getAt ( int i ) {
                if ( sorted == false ) {
                        if ( i == 0 ) {
                                // 最小値を探す
                                int min_n = 0;
                                for ( int n = 1 ; n < num ; n++ ) {
                                        if ( array[min_n] > array[n] ) {
                                                min_n = n;
                                        }
                                }
                                return array[min_n];
                        } else if ( i == num - 1 ) {
                                // 最大値を探す
                                int max_n = 0;
                                for ( int n = 1 ; n < num ; n++ ) {
                                        if ( array[max_n] < array[n] ) {
                                                max_n = n;
                                        }
                                }
                                return array[max_n];
                        } else {
                                // 並べ替え
                                int[] ar = new int[num];
                                for ( int n = 0 ; n < num ; n++ ) {
                                        ar[n] = array[n];
                                }
                                java.util.Arrays.sort(ar);
                                for ( int n = 0 ; n < num ; n++ ) {
                                        array[n] = ar[n];
                                }
                                sorted = true;
                        }
                }
                return array[i];
        }
}
        

この実装では、addメソッドを呼んだ時点だけでなく、getAtメソッドを呼んだ時点でも、並べ替えを行わないことがあります。最小値または最大値を取り出すだけであれば、一通り調べてみるだけで、データを並べ替えません。

だいぶ変わった作りになっていますが、このクラスも、addメソッドを呼べば、データを1つリストに追加できますし、getAtメソッドを呼べば、きちんと、i番目に小さなデータを取り出すことができますので、要求仕様を満たしている「いつもソートされたリスト」です。

どれが一番良い方法か

ここでは、「いつもソートされたリスト」を、3通りの方法で実装しました。これらは、振る舞いの仕様はすべて同じです。ただ、実行速度には違いがあります。

次の4つのケースで、それぞれの実装の速度を比べてみましょう。

  1. 1万個のランダムなデータを追加する。1つデータを追加するたびに、ランダムにi番目のデータを取り出す。
  2. 1万個のランダムなデータを追加する。100個データを追加するたびに、ランダムにi番目のデータを取り出す。
  3. 1万個のランダムなデータを追加する。すべてのデータを追加し終わったら、最後に一度だけ、0番目から9999番目までのデータを順に取り出す。
  4. 1万個のランダムなデータを追加する。1つデータを追加するたびに、最大値を取り出す。すべてのデータを追加し終わったら、最後に一度だけ、0番目から9999番目までのデータを順に取り出す。

それぞれの実行にかかった時間を測定すると、次のようになりました。

実装方法 ケース1 ケース2 ケース3 ケース4
言われたとおりに作ったクラス 234 235 188 235
ちょっと変わったやり方で作ったクラス 4031 63 31 4172
もっと特殊なやり方で作ったクラス 4172 110 32 219

なお、データの数を1万個から2万個に増やすと、次のようになりました。

実装方法 ケース1 ケース2 ケース3 ケース4
言われたとおりに作ったクラス 781 828 890 812
ちょっと変わったやり方で作ったクラス 17140 312 31 17000
もっと特殊なやり方で作ったクラス 17062 172 31 921

言われたとおりに作ったクラスは、どのようなケースでも同じくらいの時間がかかりました。

ちょっと変わったやり方で作ったクラスや、もっと特殊なやり方で作ったクラスは、ケース1では、かなり遅くなってしまいました。

しかし、リストにデータを追加しながら、同じくらいの頻度でデータを取り出すことは、実際にはあまり無さそうです。もし、データを取り出す回数が少ないのであれば、ちょっと変わったやり方で作ったクラスの方が、良いパフォーマンスを発揮できます。

さらに、最小値や最大値は良く使うけれども、実は、真ん中あたりのデータはめったに取り出さない、ということも、あるかもしれません。もしそうなら、もっと特殊なやり方で作ったクラスの方が、良いパフォーマンスを発揮できます。

言われたとおりに作ったクラスは、仕様と実装が一致しているので、分かりやすいです。しかし、必ずしも、それが最適な方法ではないこともあります。

このように、「いつもソートされたリスト」といっても、必ずしも、そのとおりに実装するとは限りません。クラスの振る舞いの仕様と、中身の実装とは、まったく別物です。これが、オブジェクト指向におけるカプセル化ということなのです。

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