Javaの継承と合成(Is-aとHas-aの関係)

1概要

継承 と合成は、抽象化、カプセル化、そしてポリモーフィズムと共に、https://en.wikipedia.org/wiki/Object-oriented programming[object-oriented programming[オブジェクト指向プログラミング](OOP)の礎石です。

このチュートリアルでは、継承と合成の基本について説明し、2つのタイプの関係の違いを見つけることに重点を置きます。

2継承の基本

  • 継承は強力でありながらも使いすぎで誤用されているメカニズムです。

簡単に言うと、継承と共に、基本クラス(別名ベースタイプ)は特定のタイプに共通の状態と振る舞いを定義し、サブクラス(別名サブタイプ)にその状態と振る舞いの特殊バージョンを提供させます。

継承の扱い方を明確にするために、単純な例を作成しましょう。個人用の共通フィールドとメソッドを定義する基本クラス Person 、サブクラス Waitress Actress は追加のきめ細かいメソッド実装を提供します。

これが Person クラスです。

public class Person {
    private final String name;

   //other fields, standard constructors, getters
}

そしてこれらはサブクラスです:

public class Waitress extends Person {

    public String serveStarter(String starter) {
        return "Serving a " + starter;
    }

   //additional methods/constructors
}
public class Actress extends Person {

    public String readScript(String movie) {
        return "Reading the script of " + movie;
    }

   //additional methods/constructors
}

さらに、 Waitress および Actress クラスのインスタンスが Person のインスタンスでもあることを確認するための単体テストを作成しましょう。これにより、「is-a」条件が型レベルで満たされていることがわかります。

@Test
public void givenWaitressInstance__whenCheckedType__thenIsInstanceOfPerson() {
    assertThat(new Waitress("Mary", "[email protected]", 22))
      .isInstanceOf(Person.class);
}

@Test
public void givenActressInstance__whenCheckedType__thenIsInstanceOfPerson() {
    assertThat(new Actress("Susan", "[email protected]", 30))
      .isInstanceOf(Person.class);
}
  • ここで継承の意味論的側面を強調することが重要です。 Personクラス の実装を再利用する以外に、 基本型 Person とサブタイプ Waitress および Actress の間に 明確な "is-a"関係** を作成しました。ウェイトレスと女優は、事実上、人です。

これは私達に尋ねるかもしれません: どのユースケースで継承が取るべき正しいアプローチですか?

サブタイプが「is-a」条件を満たし、主にクラス階層の下に追加的な機能を提供する場合は、継承が最善の方法です。

もちろん、オーバーライドされたメソッドがhttps://en.wikipedia.org/wiki/Liskov substitution principle[Liskov Substitution Principle]で推奨されている基本型/サブタイプの代用性を保持している限り、メソッドのオーバーライドは許可されています。

さらに、 サブタイプは基本タイプのAPI を継承していることにも注意してください。

それ以外の場合は、代わりにコンポジションを使用してください。

3デザインパターンにおける継承

可能な限り、継承よりも合成を優先することがコンセンサスですが、継承にその場所がある典型的なユースケースがいくつかあります。

3.1. レイヤスーパータイプパターン

この場合、私たちは継承を使って** 共通のコードを基本クラス(スーパータイプ)にレイヤごとに移動します。

ドメイン層におけるこのパターンの基本的な実装は次のとおりです。

public class Entity {

    protected long id;

   //setters
}
public class User extends Entity {

   //additional fields and methods
}

サービス層や持続層など、システム内の他の層にも同じ方法を適用できます。

3.2. テンプレートメソッドパターン

テンプレートメソッドパターンでは、基本クラスを使用してアルゴリズムの不変部分を定義してから、その部分クラスをサブクラスに実装することができます。

public abstract class ComputerBuilder {

    public final Computer buildComputer() {
        addProcessor();
        addMemory();
    }

    public abstract void addProcessor();

    public abstract void addMemory();
}
public class StandardComputerBuilder extends ComputerBuilder {

    @Override
    public void addProcessor() {
       //method implementation
    }

    @Override
    public void addMemory() {
       //method implementation
    }
}

4コンポジションの基本

コンポジションは、実装を再利用するためにOOPによって提供されるもう1つのメカニズムです。

一言で言えば、 合成によって、他のオブジェクト で構成されているオブジェクトをモデル化することができます。したがって、それらの間の「has-a」関係を定義できます。

さらに、 構成はhttps://en.wikipedia.org/wiki/Association (object-oriented programming)[association] の最強形式です。つまり、 1つのオブジェクトを構成するオブジェクト、または1つのオブジェクトに含まれるオブジェクトです。そのオブジェクトが破壊された時にも破壊されます

コンポジションがどのように機能するかをよりよく理解するために、コンピューターを表すオブジェクトを操作する必要があるとしましょう _. _

コンピュータは、マイクロプロセッサ、メモリ、サウンドカードなどを含むさまざまな部分で構成されているので、コンピュータとその各部分を個別のクラスとしてモデル化できます。

これが、 Computer クラスの単純な実装の外観です。

public class Computer {

    private Processor processor;
    private Memory memory;
    private SoundCard soundCard;

   //standard getters/setters/constructors

    public Optional<SoundCard> getSoundCard() {
        return Optional.ofNullable(soundCard);
    }
}

次のクラスは、マイクロプロセッサ、メモリ、サウンドカードをモデル化したものです(インターフェースは簡潔にするために省略されています)。

public class StandardProcessor implements Processor {

    private String model;

   //standard getters/setters
}
public class StandardMemory implements Memory {

    private String brand;
    private String size;

   //standard constructors, getters, toString
}
public class StandardSoundCard implements SoundCard {

    private String brand;

   //standard constructors, getters, toString
}

継承よりも構成をプッシュすることの動機を理解するのは簡単です。特定のクラスと他のクラスとの間に意味的に正しい「has-a」関係を確立することが可能なすべてのシナリオで、構成は正しい選択です。**

上記の例では、 Computer は、その部分をモデル化するクラスとの「has-a」条件を満たしています。

この場合、包含 Computer オブジェクトは包含オブジェクト ifの所有権を持ち、if オブジェクトのみが他の Computer オブジェクト内で再利用できないことにも注意する必要があります。所有権が暗示されていないコンポジション。

5抽象化なしの構図

別の方法として、コンストラクタで宣言するのではなく、 Computer クラスの依存関係をハードコーディングして構成関係を定義することもできます。

public class Computer {

    private StandardProcessor processor
      = new StandardProcessor("Intel I3");
    private StandardMemory memory
      = new StandardMemory("Kingston", "1TB");

   //additional fields/methods
}
  • もちろん、 Computer Processor および Memory の特定の実装に強く依存させるため、これは厳密で密結合な設計になります。

私たちはインターフェースとhttps://en.wikipedia.org/wiki/Dependency__injection[依存性注入]によって提供される抽象化のレベルを利用しないでしょう。

インターフェイスに基づく初期設計では、疎結合設計が得られます。これもテストが簡単です。

6. 結論

この記事では、Javaにおける継承と合成の基礎を学び、2つのタイプの関係(「is-a」と「has-a」)の違いを詳しく調べました。

いつものように、このチュートリアルで示されているすべてのコードサンプルはhttps://github.com/eugenp/tutorials/tree/master/core-java-lang-oop[GitHubで利用可能]です。