Javaのコンポジションと継承との違い

Javaのコンポジションについて

Javaにおいて、コンポジション(Composition)は、オブジェクト指向プログラミングの概念の一部であり、異なるクラスのオブジェクトを組み合わせて新しい機能を提供する方法です。
これは継承(Inheritance)とは異なりますが、オブジェクト同士の関係を構築する際に利用されます。

コンポジションでは、一つのクラスが他のクラスのオブジェクトを含みます。
これにより、新しいクラスは他のクラスの機能を再利用し、より柔軟でメンテナンスしやすいコードを作成することができます。

以下は、Javaでのコンポジションの基本的な例です。

// コンポジションを使用した例

// Engineクラス
class Engine {
    public void start() {
        System.out.println("Engine starting...");
    }
}

// CarクラスがEngineクラスをコンポジション
class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }

    public void start() {
        System.out.println("Car starting...");
        engine.start(); // Engineの機能を呼び出す
    }
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.start();
    }
}

この例では、Car クラスが Engine クラスのオブジェクトを含んでいます。
Car クラスは start メソッドを持ち、その中で Engine クラスの start メソッドを呼び出しています。
これにより、Car クラスは Engine クラスの機能を再利用しています。
もしCar クラスの中に Engine クラスの処理内容が書かれていると、他でエンジンの機能を使用したい場合に再利用ができなくなってしまいますね。

コンポジションの利点には、柔軟性、メンテナンスしやすさ、オブジェクトの疎結合性などがあります。
継承よりも強力であるとされ、適切に使用されると、コードの再利用性や拡張性が向上します。

継承とコンポジションのどちらを使うべきか

継承(Inheritance)とコンポジション(Composition)は、オブジェクト指向プログラミングにおいて異なるデザインアプローチを提供します。
どちらを使用するべきかは、具体的なケースやプログラムの要件に依存します。
以下は、一般的なガイドラインですが、状況により異なることを考慮する必要があります。

継承を使用するべき場合:

1. 「is-a」の関係が成り立つ場合:

  • サブクラス(子クラス、派生クラス)がスーパークラス(親クラス、基底クラス)の特性や動作を引き継ぐ場合。
  • サブクラスがスーパークラスの特定の機能や性質を拡張する場合。

2. コードの再利用が容易である場合:

  • スーパークラスのメソッドや属性をサブクラスで再利用する必要がある場合。

3. ポリモーフィズムを利用する場合:

  • 同じ名前のメソッドを異なるサブクラスでオーバーライドし、ポリモーフィズムを実現する場合。

※ポリモーフィズムについては後述

コンポジションを使用するべき場合:

1. 「has-a」の関係が適している場合:

  • オブジェクトが他のオブジェクトを持っているが、直接の継承関係が成り立たない場合。

2. 柔軟性と変更の容易さが求められる場合:

  • コンポジションは静的な継承よりも柔軟で、クラスの変更が他のクラスに与える影響が少ない。

3. 複数の実装から選択できる場合:

  • インターフェースや抽象クラスを使用し、異なる実装をコンポジションによって組み合わせることができる。

注意点:

1. 過度な継承の回避:

  • 過度な継承は、複雑なクラス階層や理解困難なコードを引き起こす可能性があります。

適切な範囲で使うことが重要です。

2. 状況に応じた選択:

  • プログラムの要件や将来の変更によって、継承とコンポジションの組み合わせが最適な場合もあります。

総じて、適切な設計を行うためには、各ケースにおいて継承とコンポジションの利点と欠点を考慮し、プログラムの特定の要件に基づいて選択することが重要です。
継承もコンポジションも、プロジェクト特性に基づいて事前に適切な設計をしなければ統一性のない実装が乱立してしまうので、状況に応じて使い分けましょう。

ポリモーフィズムとは

ポリモーフィズム(Polymorphism)は、同じインターフェースや抽象クラスを使用して異なるクラスのオブジェクトを扱える能力を指します。
これは主に「多様性」や「多態性」と訳されます。

ポリモーフィズムには主に二つの種類があります:

1. コンパイル時のポリモーフィズム(Compile-time Polymorphism):

  • メソッドのオーバロードやジェネリクスを利用したポリモーフィズム。

これはコンパイル時にメソッドやクラスがどのように呼び出されるかが決まります。

  • 例えば、メソッドのオーバロードでは同じ名前のメソッドが複数存在し、引数の型や個数によってコンパイラが適切なメソッドを選択します。

メソッドのオーバロードの実装例

public class PolymorphismExample {

    // メソッドオーバーローディングの例
    public void print(int i) {
        System.out.println("整数: " + i);
    }

    public void print(double d) {
        System.out.println("浮動小数点数: " + d);
    }

    public void print(String s) {
        System.out.println("文字列: " + s);
    }

    public static void main(String[] args) {
        PolymorphismExample example = new PolymorphismExample();
        example.print(10);       // 整数バージョンが呼び出される
        example.print(3.14);     // 浮動小数点数バージョンが呼び出される
        example.print("Hello");  // 文字列バージョンが呼び出される
    }
}

2. 実行時のポリモーフィズム(Runtime Polymorphism):

  • インターフェースや抽象クラス、メソッドのオーバーライドを利用したポリモーフィズム。

これは実際のプログラムが実行される際に、どのメソッドが呼び出されるかが動的に決まります。

  • 例えば、Javaにおいて、メソッドがインターフェースで定義され、異なるクラスがそのインターフェースを実装する場合、同じメソッド名を使ってもそれぞれのクラスの特定の実装が実行時に選択されます。

メソッドオーバーライドの実装例

// ベースクラス(スーパークラス)
class TestBase {
    public void make() {
        System.out.println("Base make");
    }
}

// Subクラス1(サブクラス)
class TestSub1 extends TestBase {
    @Override
    public void make() {
        System.out.println("Sub1 make");
    }
}

// Subクラス2(サブクラス)
class TestSub2 extends TestBase {
    @Override
    public void make() {
        System.out.println("Sub2 make");
    }
}

public class RuntimePolymorphismExample {
    public static void main(String[] args) {
        TestBase myTestSub1 = new TestSub1();  // 実行時にTestSub1のmakeが呼び出される
        TestBase myTestSub2 = new TestSub2();  // 実行時にTestSub2のmakeが呼び出される

        myTestSub1.make();  // Sub1 make
        myTestSub2.make();  // Sub2 make
    }
}

実行時のポリモーフィズムは、プログラムの柔軟性と拡張性を向上させ、コードの再利用性を高めるのに役立ちます。
この概念により、同じインターフェースを持つ複数のクラスを使って、一貫性のあるインターフェースを提供できるため、コードの保守性や拡張が容易になります。