委譲によるクラス継承とは
クラス継承には2通りのやり方があります。1つは、基準となるクラスに機能を追加した新しいクラスを作る言語的機能を使うもの、もう1つは「委譲」と呼ばれ、形式的にはクラス継承ではないですが、クラス継承とほぼ同様の効果が取得できます。ここでは、2番目に示した委譲によるクラス継承について述べます。次に、委譲(delegate)の種類と実際のプログラム例(Java)を示します。
- 必要に応じて内部でインスタンスのメソッドとして呼び出し、あとは固有処理。<例1>
- 上書きしない継承を模倣<例2>
- 固有処理<例3>
実際のコードを次に示します。例xは、次に示すプログラムのコメントに示す番号と対応付けています。
class Base{
private int x, y;
public int methodA(){} /* このクラスは上書きされる */
public int methodB(){} /* このクラスは上書きされる */
}
class Delivered {
Base b = new Base(); /* インスタンスとして基底クラスを持つ */
public int methodA(){ /* 上書きの要領 */
b.methodA(); /* 必要に応じて内部でインスタンスのメソッド<例1>*/
..... /* として呼び出す。あと固有処理。 */
}
public int methodB( ) {
b.methodB(); /* 上書きしない継承を模倣 <例2>*/
}
public int methodC( ) {
..... /* 固有処理 <例3> */
}
}
基底クラスの「継承」と「委譲」との違い
基底クラスの「継承」と「委譲」との違いを次に示します。一般にAはBの特別な種類であるときは継承関係、AはBの性質であるときには委譲を使うとよいとされています。
- 「継承」では基底クラスのメソッドを何もせずに使うことができるが、「委譲」ではわざわざ「基底クラスのインスタンスを使ってメソッドを呼び出す必要がある。これにはメリットとデメリットがある。
デメリットは当然、そのまま使いたいメソッドが多い場合には、わざわざ「基底クラスのインスタンスを通じてメソッド呼び出し」をするだけの細かいメソッドを、沢山定義する必要があることである。
逆にこれがメリットになる場合もある。なぜなら、アクセスを禁止したい基底クラスのメソッドは、「委譲」されたクラスで定義しなければ、呼び出されようがないのである。これは基底クラスのメソッドを整理し、アクセス制限をする有効な手段である。
- 「委譲」の方がクラス同士の相互作用に幅を持たせることが容易である。このため、デザインパターンでは、クラス継承以上に「委譲」が活躍する。また、「委譲」を使って Java では許されない多重継承のような手法を使うことが可能である。つまり2つのクラスのインスタンスを変数として保持し、その2つのインスタンスのメソッドをすべて、当該インスタンスのメソッド呼び出しするメソッドとして実装する。
| クラスの継承(is-a関係) | 委譲(has-a関係) | |
|---|---|---|
| 長所 | Javaの標準機能なので容易にコードの再利用が図れる | 2つのクラスは完全に分離している |
| 短所 | 親クラスを修正すると、その影響が子クラスに及ぶことがある | わざわざ別にインスタンスを生成しなければならない 必要なメソッドはいちいち取り込まなければならない |
委譲を使用したAdapterパターン例(Java)
Adapterパターンは、既存のクラスとは別に、必要なインタフェースを持つ「アダプタクラス」を用意します。このアダプタクラスは、既存のクライアントプログラムから使用されるために必要なインタフェースを持ち、そのメソッドの中から、既存クラスの別のメソッドを呼び出すことで、橋渡しを実現します。
例では、開発者はBank クラスにある、貯金額、借金額を表示させます。しかし、Bankクラスは表示の機能はない、かつ、修正できないとします。そこで、貯金額、借金額を表示するというPrint(インタフェースクラス)を作成し、Printクラスを継承したPrintBankクラスを作成して、PrintBankクラスでBankクラスのインスタンスを作成します。
class Bank {
private int deposit;
private int debt;
public Bank(int deposit, int debt) {
this.deposit = deposit;
this.debt = debt;
}
public int getDeposit() {
return this.deposit;
}
public int getDebt() {
return this.debt;
}
}
interface Print {
//貯金を表示
public abstract void printDeposit();
//借金を表示
public abstract void printDebt();
}
class PrintBank extends Bank implements Print {
public PrintBank(int deposit, int debt) {
super(deposit, debt);
}
public void printDeposit() {
System.out.println("貯金:" + this.getDeposit() + "円");
}
public void printDebt() {
System.out.println("借金:" + this.getDebt() + "円");
}
}
public class AdapterDemo {
public static void main(String[] args) {
Print p = new PrintBank(300,100);
//貯金額を表示
p.printDeposit();
//借金額を表示
p.printDebt();
}
実行した結果が次のようになります。
貯金:300円 借金:100円
参考-インタフェースの実装(Java)
インタフェースの実装はimplementsを使用して行います。インタフェースは1つのクラスに対し、複数実装することもできます。また、クラスがextendsを使用する場合は、implementsはextendsの後に指定します。
class クラス名 implements インタフェース名,・・・ class クラス名 extends スーパークラス名 implements インタフェース名