Javaでの継承と構成(Is-aとHas-aの関係)
1. 概要
継承と構成は、抽象化、カプセル化、およびポリモーフィズムとともに、オブジェクト指向プログラミング(OOP)の基礎です。
このチュートリアルでは、継承と構成の基本について説明し、2つのタイプの関係の違いを見つけることに重点を置きます。
2. 継承の基本
継承は、強力でありながら使いすぎで誤用されているメカニズムです。
簡単に言えば、継承では、基本クラス(別名 基本タイプ)は、特定のタイプに共通の状態と動作を定義し、サブクラス(別名 サブタイプ)は、その状態と動作の特殊なバージョンを提供します。
継承の操作方法を明確にするために、単純な例を作成しましょう。基本クラス Person は人の共通のフィールドとメソッドを定義し、サブクラスはWaitressと Acctress は、追加のきめ細かいメソッド実装を提供します。
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とサブタイプの間に明確に定義された「is-a」関係を作成しましたウェイトレスおよび女優。 ウェイトレスと女優は、事実上、人です。
これにより、次のように質問される可能性があります。どのユースケースで継承が正しいアプローチを取るか?
サブタイプが「is-a」条件を満たす場合、主にクラス階層のさらに下の追加機能を提供します。 次に、継承が進むべき道です。
もちろん、オーバーライドされたメソッドが 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」関係を定義できます。
さらに、構成は最も強力な関連付けの形式です。つまり、 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オブジェクトは、オブジェクトが別の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の特定の実装に強く依存させるため、堅固で緊密に結合された設計になります。
インターフェイスと依存性注入によって提供される抽象化のレベルを利用することはできません。
インターフェイスに基づく初期設計では、緩く結合された設計が得られ、これもテストが容易です。
6. 結論
この記事では、Javaでの継承と構成の基礎を学び、2つのタイプの関係(「is-a」と「is-a」)の違いを詳しく調べました。 “があります”)。
いつものように、このチュートリアルに示されているすべてのコードサンプルは、GitHubからで入手できます。