オブジェクト指向言語とオブジェクト指向設計の基礎
プログラミング環境の重要な技術として、オブジェクト指向プログラミングがある。今回は、オブジェクト指向言語であるC++とJavaについて簡単に説明し、オブジェクト指向の考えをつかったオブジェクト指向設計の基礎について述べる。後半の資料は、Scott Meyersの”Effective C++” (岩谷訳、ソフトバンク、ISBN4−89052−401−0)の第6章「継承とオブジェクト指向設計」である。
1つのプログラミング言語を知っていることと、その言語を正しくつかって実際にプログラムを書けることはおなじではない。特に、オブジェクト指向言語の場合には正しくつかえば、非常に効果的な保守性に優れたプログラムになるが、間違ってつかった場合には非常に醜いプログラムになってしまう。C++のような非常に多機能なプログラミング言語の場合はその差は大きいものになってしまう。今回の講義では、そのあたりの意味するところを理解してほしい。講義では第6章だけを取り上げるが、いい本なので一読を薦める。
オブジェクト指向言語C++
C++は、Cをベースにオブジェクト指向言語であり、1980年代半ばにBjarne Stroustropによって設計された言語である。Cをベースにしているため、Cを知っている人にはとっつきやすいが(たとえば、Cのプログラムならば少々の変更でコンパイルできる)、逆にCをベースにしているためにわかりにくくなっているところがある。オブジェクトはclassで宣言する。下の例では、社員のデータをオブジェクトとして、定義している。この定義には、メンバー関数printが定義されており、employee eに対して、e.print()でメンバー関数を呼び出す。右の例では、managerというオブジェクトを定義している。オブジェクト型employeeを「継承」しており、empolyee のメンバーに加えて、管理する社員へのポインターgroupを持つオブジェクト型であることを意味する。ここで、managerはemployeeから、導出された(deriverd)という。逆に、employeeはmanagerの基本クラス(base class)であるという。個々で現れているpublicの意味は、このキーワード以降のメンバーは他のオブジェクトからアクセスできることを意味しメンバーの「可視性」を制御する。
以下に特徴をあげる:
l オブジェクトを定義するためにclassを導入。データ型に対し、その操作を定義するメンバー関数を宣言できる。ちなみにCの構造体であるstructは、全メンバーが公開(public)なclassと同値。
l クラス定義において、継承(inheritance)関係を定義でき、メンバーの可視性を制御できる。2つ以上のベースクラスも持つことができる。(Multiple inheritance)
l クラス定義においては、クラスを生成する構築子(constructor)と消滅子(destructor)を宣言でき、クラスが生成・消滅するときに呼び出される。
l new / delete 演算子
l 仮想メンバー関数 (virtual function)
l オブジェクトに対し、演算子をできる(operator overloading)
l 多義関数名、int foo(int x)とint foo(double)は違う関数となる。ただし、「暗黙の型変換」が行われるので注意。
l defaultの引数が使える。
l 引数のReference渡しが使える。
l Template機能。Genericなプログラミングができる。
これらを使ったプログラミングの例として、行列、ベクトル演算のライブラリの例を挙げる。
オブジェクト指向言語Java
ネットワーク向けのプログラミング言語として注目されているJavaであるが、オブジェクト指向言語としてC++と比較されることが多い。
l すべてのプログラムはクラス定義の集まりで定義される。Cのように、関数だけ、データ定義だけというのはない。
l オブジェクト指向言語。メンバー関数、メンバーの可視化制御、継承ができる。
l Constructorはあるが、destructorはない。参照されなくなったオブジェクトは自動的にガベージコレクションされる。
l ポインタはない。すべてのオブジェクトは、C++でいえばポインターで表現されている。メンバー関数はすべてvirtualメンバー関数。
l ひとつのオブジェクトからしか、継承できない。
l interface定義。(C++の仮想クラス定義に相当する)
l オブジェクト型に演算子は定義できない。Operator overloadingなし。
l Template機能もなし。
C++と比較して議論されることもあるjavaであるが、むしろ、その発想としてはsmalltalkに近い。プログラムは通常クラスファイルというjavaバイトコードからなる中間形式にコンパイルされ、java virtual machineと呼ばれるバイトコードインタープリタで実行される。この実行形式がネットワーク上の言語としてのjavaの柔軟性を与えているといえる。
オブジェクト指向設計(オブジェクト指向プログラミングの原則)
オブジェクト指向言語でプログラミングするときには、どれをオブジェクトにして、どのようなメンバー、メンバー関数を作るかを考えなくてはならない。プログラムを見通しよく作るには、プログラムする対象を反映したオブジェクトを設計、定義する必要がある。オブジェクト指向プログラミングに限らず、以下を考えることは重要である。
l
保守性:後から、見たとき、あるいはデバック中にも容易に理解できるようなプログラムを作ること。他の人が見たときにわかりやすいこと(可読性)も重要である。
l
拡張性:プログラムの機能を加えるときに、なるべくほかのコードを変更せずに機能を加えることができることが望ましい。
l
再利用性:ほかのプログラムに転用できるような部品として設計しておけば、プログラムの価値は高まる。
l
効率:そして、プログラムは速くなくてはならない。
それでは、オブジェクト指向プログラミングをするときにオブジェクト設計の原則についてみていくことにする。
publicな継承が” is a”関係であることをしっかり理解する(項目35)
クラスAからpublicな継承をするクラスBは、タイプBのオブジェクトはすべて、タイプAであることを意味している。たとえば、
class Person { …
};
class Student : public Person { … };
void dance(Person & p); void study(Student& a);
を考えてみる。 Persion p; Student s; に対して、dance(p)でも、dance(s)でもOKであるが、study(s)はOKであるが、study(p)はNGである。つまり、publicの継承は「特殊化」という意味を持つ。言い換えれば、publicに継承するということは、ベースクラスは派生するクラスよりも一般的な概念であるということである。ベースクラスに特殊なpublicなメンバーを定義することは間違いを引き起こす。このことは、Javaのpublicの継承にもいえる。
クラス間の関係としては、”has a”関係と”implemented in terms of”関係がある。
インタフェースと継承と実装の継承を区別する(項目36)
仮想メンバー関数の意味について考えてみる。C++では、インタフェースのみを定義するためには純粋仮想関数というものを用いる。
class Shape {
public:
virtual void
draw() const = 0; /* 純粋仮想関数 */
int objectID();
….
}
class Retangle: public Shape { …. };
class Oval : public Shape { … };
Shapeを継承するRectanleもOvalも、メンバー関数drawを定義しなくてはならない。インタフェースの継承とは、それを継承するメンバー関数は同じインタフェースを持っていることを強制することを意味する。純粋仮想関数を宣言する目的は、派生するクラスにインタフェースだけを継承させることである。純粋仮想関数だけを定義するクラスを定義する場合があり、これをC++では抽象ベースクラス(Abstract Base Class, ABC)という。
これに対し、通常の関数では派生されたクラス側で仮想関数をオーバーライドすることができる。つまり、特殊化した側でメンバー関数を事情に合わせて変更できる。もしも、ない場合にはベースクラス側のメンバー関数が使われる。すなわち、通常の仮想関数を用いる目的は、派生クラスに関数のインタフェースと関数のデフォールトの実装を継承させる。しかし、この機能は便利のように見えるが、デフォールトの実装が間違いを引き起こすもとになる可能性があるので注意。
Javaの場合にはC++からみれば、仮想関数のみであるといえる。また、インタフェースのみを定義する場合には、inteface定義という別の仕組みが用意されており、extendsでなく、implementsで継承することになっており、これについては概念的に整理されている。
さて、仮想関数でない通常の非仮想関数は、派生されるクラスにインタフェースと強制的な実装の両方を継承させるという意味になる。つまり、特殊化しても変わらない機能を定義するものであり、原則、継承するクラス側では定義してはならない。
継承した非仮想関数の別定義を設けてはならない(項目37)
理由は上に述べたとおりであるが、次の例について考えてみる。
B x; A *pa
= &x; B *pb
= &x; pa->
f(); /* call a::f() */ pb->f(); /* call b::f() */ pa->g(); /* call b::g() */ pb->g(); /* call b::g() */
非仮想関数の場合には、どのメンバー関数が呼び出されるのかがコンパイル時にポインタのタイプから決定されるため、指されているオブジェクトがBであってもAのメンバー関数が呼び出されることになる。
再度、上に述べたことを繰り返すと、通常のpubicな継承は”is a”の関係であり、インタフェースと実装の両方を継承する(べきである)。したがって、別の定義をもってはならないということである。Javaの場合には、この区別がないので、これを区別して用いることが必要である。
継承の階層をダウンキャストすることを避ける(項目39)
クラスの階層の下方、すなわち派生クラスにキャストすることをダウンキャストという。いろいろな派生されたクラスのオブジェクトをベースクラスのオブジェクトとして一様に扱いたい場合に、その中で、特定の派生クラスのメンバー関数を呼び出す必要がある場合がある。この場合に、ベースクラスを派生クラスへのキャストすることで呼び出すことがあるが、これを乱用すると拡張性のないプログラムになってしまう。
層化によって”has a”関係や”is implemented in terms of”関係を表現する(項目40)
層化(layering)とはクラス定義の中にデータメンバーとして別のクラスのオブジェクトを定義することである。たとえば、
class Name { … };
class Address { …. };
class Person {
private:
Name name;
Address
address;
…. }
この上でわかるように、この関係は”has a”関係である。また、集合SetをリストListで表現する場合には、
class Set: List { …なかには、Set用のメンバー関数… };
で表現できる。しかし、このようにしてしまうと、Setのオブジェクトからは、Listのメンバー関数も呼べてしまうことになる。これを避けるためには、継承関係をprivateにするか、
class Set {
private:
List
rep;
… };
とすれば、よい。すなわち、層化は…を用いて実装する、”is implemented in terms of”関係を定義するということになる。
Privateな継承は、正しくつかう(項目41)
上の例でみたとおり、privateな継承の意味は、”is implemented in terms of”関係を定義することである。Setを使う場合には、ほかからはListのメンバー関数をアクセスすることはできない。ソフトウエアの設計の間には意味がなく、実装の時にのみに意味がある。層化が使える時には層化を使うべきであるが、privateの継承を使う理由はコードが単純化できる場合があるからである。しかし、コンストラクタの呼ばれる関係など、複雑な場合があるので注意。
継承とテンプレートを使い分ける(項目42)
多重継承は正しく使う(項目43)
次回は、デザインパターンについて解説する。