Deep Side of Java〜Java 言語再入門 第1回

オブジェクト指向の3つの特徴

オブジェクト指向の3つの特徴

「オブジェクト指向」には以下の3つの特徴があるとされる。

カプセル化(情報隠蔽)

どのようなシステム開発でも、情報管理は重要である。各モジュールの間の「結合性」を減らし、モジュール同士が互いに相手の情報を参照し合わないようにすることが、開発効率を上げるという事実がある。つまり、公開すべきデータ(や手続き)を制限し、あたかも各モジュールが「ブラックボックス」であるかのように扱うことで、開発効率を上げることができる、という結論を得るのである。

これはつまり、さまざまなデータや手続きの中で、本来インターフェイスとなる、そのクラスの機能として定義された手続きと、効率的に実装するための手続きとを分離することになるのである。

しかし、C言語ではデフォルトのスコープは「広すぎる」。デフォルトではすべてのグローバル変数と関数は、結合単位のすべてから参照可能である。だから、C言語の「良い」スタイルでは、static 宣言を活用すべきであると良く言われる。このような欠陥を改善すべく、多くのオブジェクト指向言語では、次の4種類のスコープを定めている。

public
どのパッケージからも参照が可能である。

package protected(デフォルト)
同一のパッケージに属するクラスか、そのクラスのサブクラスからは参照可能である。

protected
そのクラスを継承して作られたサブクラスからは参照可能である。

private
そのクラスでしか参照できない。

このうち、この講座では特別な場合を除いて メンバ変数は private スコープのみを使うこととする。逆に言えば、メンバ変数を操作する場合には、すべてメソッドを経由すべきである、ということである。

また手続きに関しては、インターフェイスである手続きには「public」を使い、実装用の手続きには原則的に「private」を使うのが定石となる。「package protected」や「protected」はなるべく使わないのが良いとされている。

この応用として、自分のクラスからは値のセットが可能であるが、他のクラスからは参照のみにしたいことが多い。このような制限を実現するために、良く「ゲッタ」「セッタ」と呼ばれるメソッドを定義する。つまり、

public class TestClass {
        private  int var;  /* メンバ */

        public int getVar() {  /* ゲッタ */
                return var;
        }
        private void setVar( int n ) {  /* セッタ */
                var = n;
        }
}

メンバ変数 var は、同一のクラスに所属する TestClass 内のメソッドからは値をセットできるが、他のクラスのメソッドからは読み出ししかできない。このようにすれば、有効にグローバル変数を管理できるのである。

クラス継承

クラスは設計上単なる「構造体」ではない。それどころか、このクラスが設計の単位となるのである。つまり、「クラスを設計する」ことがオブジェクト指向による「設計」であると言っても良い。だから、機能モジュールによって設計するのではなくて、ブラックボックス的な「もの」としてのオブジェクトが何をし、どういうインターフェイスを持つのか、ということを中心に設計していく「ボトムアップ設計」の一種である。つまりこれは、設計の上で「完全に機能を満たして動作するパーツをまず作り、それからそれらを組み合わせてアプリケーションを実装する」タイプの設計論である。読者の中にはこれと反対の「トップダウン設計」しか経験がない場合もあるかもしれないが、頭をちょっと切替えられたい。

さらに、このクラスは単に「オブジェクト」を定義するだけではなくて、既に定義されたオブジェクトを目的に合わせて拡張することもできる。これを比喩的に言えば、「汎用的な部品を買ってきて、それを特定用途のために加工する」ことに当たる。これが「継承」である。だから、オブジェクト指向の設計では、既存のクラスを「継承」して、目的に合わせてカスタマイズする、というニュアンスが出てくる。このため、「再利用」を前提にしてプログラムを書く、ということが理想として掲げられる。

このような方法論は、オブジェクト指向言語でなくても、一般にライブラリ作成では取られてきた考え方である。このような「ライブラリ作成風の」プログラミングスタイルを全体的なプログラミングスタイルとして採用すれば、「再利用性」が高まる、と期待されるのである。それゆえ、ライブラリのプログラミングでよくあるように、適切に「情報隠蔽」をし、「インターフェイス」は明示してライブラリを作成することになる。また、作成されたライブラリを特定用途に合わせて使いやすいように、汎用的に書くことが必要である。だから、「汎用的なライブラリが実現する機能」と「そのライブラリを特定用途のために使う」こととは分けて考えたほうが良い。

ということは、プログラムの記述において、その仕事をするどんなプログラムでも共通する部分と、その仕事で特有の部分を分けて書き、共通部分を「基底クラス」として「抽象的に」設計し、特有部分を「具象クラス」として「基底クラス」を継承させて、派生させるという仕事手順になる。このようなクラス間の関係に一定のパターンを見つけて整理したものが、「デザイン・パターン」である。講座後半では、「デザイン・パターン」を中心に解説していくことになる。

また、このクラス継承はオブジェクト指向言語でも、言語によって仕様が異なる部分である。C++ では複数のクラスから派生する「多重継承」が可能だが、Java では「多重継承」はなくて、その代わりに「interface」が存在する。

多相(ポリモルフィズム)

このように、クラス継承を前提とした設計の場合、ある基底クラスから派生したクラスが複数ある場合が当然ある。この時に、基底クラスのレベルで定義された機能によって、複数のクラスを同一視して扱いたい場合が頻繁にある。つまり、基底クラスでは抽象的に定義された機能は、派生クラスではそれぞれ具体的に定義されているわけである。これらの複数の派生クラスは、機能を抽象的に見れば同じであり、それならばそれらを抽象定義である基底クラスであるかのように扱い、実際に共通するインターフェイスを呼び出したときには、それぞれの具体的な独自の仕事をするようにさせることができる。

この事情は、例えばC言語で例を挙げてみると判りやすいかも知れない。次のようなコードはウィンドウプログラムなどで頻繁にお目にかかる。

struct Unified {
        int kind;
        union u {
                struct Work1 w1;
                struct Work2 w2;
                struct Work3 w3;
        }
};

int each_proc( struct Unified *un ) {
        switch( un->kind ) {
        case WORK_1:
                return do_it1( &un->u.w1 );
        case WORK_2:
                return do_it2( &un->u.w2 );
        case WORK_3:
                return do_it3( &un->u.w3 );
        }
        return -1;
}

つまり、汎用的な構造体である struct Unified は、メンバ kind がその具体的な種別を表しているのであり、その kind メンバによって具体的な仕事に割り振っているわけである。

「多相」はこのような仕組みを洗練したものである。つまり、作成されたインスタンスは、自分がどのクラスに属するのかを知っている。要するに内緒で kind メンバが書かれているようなものである。だから、具体的な派生クラスは、もし抽象的な基底クラスにキャストされた場合でも、「本当の所属クラス」を失わない。だから、見かけ上「基底クラス」の型を持っている場合でも、それぞれ派生クラスによって違う仕事を定義されたメソッドは、それが呼び出された場合には、具体的な派生クラスのメソッドを実行する。これを特に「実行時の多相」と呼ぶのであり、つまり実行時に動的に型を判定して、それが作成された時の型でメソッドが呼ばれるのである。

つまり、「多相」があることで、抽象的にプログラムを設計できるのである。そのため、抽象的な基底クラスの役割がとくに重要になる。基底クラスでは、抽象的に見て同じ仕事であるものを、実際の仕事を定義しない「ひな型」である「抽象メソッド」として定義し、これをインターフェイスとして公開する。実際に仕事をする実装クラスでは、この抽象クラスを継承し、この「抽象メソッド」を上書きして実際の仕事をするコードを記述する。実際に作られるインスタンスは「具象クラス」のものであるが、それらの利用の場面では、抽象的な基底クラスに属するものとして、コードが書かれるのである。これによってプログラム自体、作業自体を抽象化できるのであり、「デザイン・パターン」においてこの多相は大活躍する。

そのために、オブジェクト指向言語では「インターフェイスに対してプログラムをする」とまで言われる。Java には多重継承はないが、このような「インターフェイス」重視の流れから、言語仕様として「interface」があり、これは抽象メソッドの定義のみができるクラスであり、これだけは多重継承のように複数の「interface」と1つの「基底クラス」を継承できる。しかし、これを弱い多重継承と考えるよりも、「インターフェイスのみを再利用可能なかたちで定義できる」と考えた方が実感に合うと感じている。なぜなら、「同じメソッドを定義している」ことを保証し、それを現実の規定クラスとは無関係に実装できるからであり、雰囲気的には「型キャスト」のように使うことができるからである。→ Humor

その他の多相

また、言語によって「多相」という言葉を広く理解して、似たような結果を得る別なものも示す場合がある。

実行時の多相
既に述べたもの。

静的多相
C言語の関数定義では、名前が同じで引数型の異なる関数は定義できない。しかしオブジェクト指向言語では、引数の型の異なる同名の関数を許すケースが多い。これも「多相」の一種として扱うことができるが、コンパイル時に決定可能であるものを特に静的多相と呼ぶ。この場合でも引数の取り方だけが複数ありうるのであり、戻り値はどの静的多相関数でも共通して同じでなくてはならない(もし、そうでなければ戻り値の検証はできないよ!)。

パラメータ化クラス
これは C++ のみに「テンプレート」という名前であり、Java にはない(本家は Ada や Eiffel)。置換マクロ的に、複数の型に対して一つのひな型から実装定義を作り上げるもの。C++ でもあまり使わない方が良いとされがちな機能である。より深く知りたい人は...Java でも J2SDK1.5で追加予定。この件についての詳細は「Iterator〜1.5で導入される Generic 型について」を参照のこと。)



copyright by K.Sugiura, 1996-2006