コンポジションと継承
構成(Composition)と継承(Inheritance)は、よく聞かれるインタビューの質問の1つです。継承よりも構成を使用することも聞いたことがあるでしょう。
構成 と 継承 の比較
組成と継承は、オブジェクト指向プログラミングの概念です。これらはJavaなどの特定のプログラミング言語に縛られていません。プログラム上で継承よりも組成を比較する前に、それらの簡単な定義を見てみましょう。
作文
オブジェクト指向プログラミングにおける構成は、オブジェクト間の「〜を持つ」といった関係を実現するためのデザイン技法です。Javaにおいては、他のオブジェクトのインスタンス変数を使用することで構成を実現します。例えば、職業を持つ人物は、以下のようにJavaのオブジェクト指向プログラミングで実装されます。
package com.scdev.composition;
public class Job {
// variables, methods etc.
}
package com.scdev.composition;
public class Person {
//composition has-a relationship
private Job job;
//variables, methods, constructors etc. object-oriented
相続 (そうぞく)
継承は、オブジェクト指向プログラミングにおける設計技術であり、オブジェクト間のis-a関係を実装するために使用されます。Javaにおいては、継承はextendsキーワードを用いて実装されます。例えば、CatはAnimalの関係を持つというJavaのプログラミングでは、以下のように実装されます。
package com.scdev.inheritance;
public class Animal {
// variables, methods etc.
}
package com.scdev.inheritance;
public class Cat extends Animal{
}
継承よりも構造の方が重要です。
構成と継承は、異なるアプローチを通じてコードの再利用を促進します。ではどちらを選ぶべきでしょうか? 構成と継承を比較する方法を見てみましょう。プログラミングでは、継承よりも構成を優先すべきと言われていることを聞いたことがあるかもしれません。構成 vs 継承を選ぶ際に役立つ理由のいくつかを見てみましょう。
-
- 継承は緊密に結びついているのに対し、コンポジションは緩く結びついています。以下のような継承を持つクラスがあると仮定しましょう。
package com.scdev.java.examples;
public class ClassA {
public void foo(){
}
}
class ClassB extends ClassA{
public void bar(){
}
}
シンプルにするため、スーパークラスとサブクラスを同一のパッケージにしています。しかし、通常は別々のコードベースになります。スーパークラスClassAを継承するクラスは多く存在する可能性があります。この状況の非常に一般的な例は、Exceptionクラスを拡張することです。さて、ClassAの実装を以下のように変更した場合、新しいメソッドbar()が追加されます。
package com.scdev.java.examples;
public class ClassA {
public void foo(){
}
public int bar(){
return 0;
}
}
新しいClassAの実装を使用し始めると、ClassBでコンパイル時エラーが発生します。ClassBでのreturnタイプがClassA.bar()と互換性がないためです。解決策は、スーパークラスまたはサブクラスのbar()メソッドのいずれかを変更して、互換性を確保することです。もし継承ではなくコンポジションを使用していれば、この問題に直面することはありません。以下のようなコンポジションを使用したClassBの実装は、簡単な例です。
class ClassB{
ClassA classA = new ClassA();
public void bar(){
classA.foo();
classA.bar();
}
}
継承にはアクセス制御がなく、コンポジションではアクセスを制限することができます。特定のサブクラスにアクセス権限のある他のクラスに対して、すべてのスーパークラスのメソッドが公開されます。そのため、スーパークラスに新しいメソッドが導入されたり、セキュリティホールがある場合、サブクラスは脆弱になります。コンポジションでは、使用するメソッドを選択できるため、継承よりも安全性が高いです。例えば、以下のようにClassBでClassAのfoo()メソッドを他のクラスに公開できます。
class ClassB {
ClassA classA = new ClassA();
public void foo(){
classA.foo();
}
public void bar(){
}
}
これは、コンポジションの継承の利点の一つです。
コンポジションは、複数のサブクラスのシナリオでのメソッドの呼び出しの柔軟性を提供します。例えば、以下のような継承のシナリオがあるとします。
abstract class Abs {
abstract void foo();
}
public class ClassA extends Abs{
public void foo(){
}
}
class ClassB extends Abs{
public void foo(){
}
}
class Test {
ClassA a = new ClassA();
ClassB b = new ClassB();
public void test(){
a.foo();
b.foo();
}
}
では、もしサブクラスがさらに増えた場合、コンポジションによってコードが複雑になりませんか?いいえ、Testクラスは以下のように再書きできます。
class Test {
Abs obj = null;
Test1(Abs o){
this.obj = o;
}
public void foo(){
this.obj.foo();
}
}
これにより、コンストラクタで使用するオブジェクトに基づいて任意のサブクラスを使用する柔軟性が得られます。
継承に対するコンポジションのもう一つの利点は、テストの範囲です。コンポジションでは、他のクラスから使用しているメソッドを把握しているため、ユニットテストが容易です。テストのためにそれをモックアップすることができます。一方、継承ではスーパークラスに大きく依存しており、スーパークラスのすべてのメソッドが使用されるかわかりません。したがって、スーパークラスのすべてのメソッドをテストする必要があります。これは余分な作業で、継承のために不必要に行わなければならない作業です。
「継承と組成に関する説明は以上です。継承ではなく組成を選ぶための十分な理由が揃いました。親クラスが変更されないことが確かな場合に限り、継承を使用してください。それ以外の場合は組成を選んでください。」