Skip to content
Playground

5.コンポジション

学習目標

  • あるクラスが別のクラスを部品として利用する構造を説明できる
  • 依存をコンストラクターで受け取り、private final フィールドに保持する書き方ができる
  • 依存を内部で new する場合と外から受け取る場合の違いを説明できる

クラスのフィールドの型には、intString のような基本型だけでなく、自分で定義したクラスも指定できます。あるクラスが別のクラスをフィールドとして持つことで、複数のクラスを組み合わせたプログラムが作れます。

例として、車とエンジンの関係を 2 つのクラスで表現します。

Engine.java
public class Engine {
public void start() {
System.out.println("エンジン始動");
}
}

Engine はフィールドを持たず、start メソッドだけを提供します。これを部品として使う Car を定義します。

Car.java
public class Car {
private final Engine engine;
public Car() {
this.engine = new Engine();
}
public void start() {
engine.start();
System.out.println("車が発進");
}
}

CarEngine 型のフィールド engine を持ち、コンストラクターの中で new Engine() を実行してフィールドに代入しています。Car.start() の中で engine.start() が呼ばれ、エンジン始動の処理は Engine 側で実行されます。

Main.java
Car car = new Car();
car.start();
エンジン始動
車が発進

このように、あるクラスが別のクラスを部品として利用してプログラムを構成する構造を コンポジション(クラスの組み合わせ)と呼びます。CarEngine を必要とするため、Engine依存 しています。

ScoreCalculatorで平均点を計算し、GradeJudgeでランクを判定する処理を組み合わせると、学生のリストから成績レポートを出力できます。この処理を GradeReportService というサービスクラスにまとめます。

CarEngine を 1 つ持つように、GradeReportServiceScoreCalculatorGradeJudge の 2 つの依存を持ちます。依存の渡し方には、依存を自分のクラスの中で new する書き方と、コンストラクターの引数で外から受け取る書き方があります。

GradeReportService が ScoreCalculator と GradeJudge に依存する関係

依存を自分で new する書き方です。GradeReportServicereport メソッドの中で、必要なインスタンスを生成します。

GradeReportService.java
import java.util.List;
public class GradeReportService {
public void report(List<Student> students) {
// 計算と判定に使うサービスクラスのインスタンスを用意
ScoreCalculator calculator = new ScoreCalculator();
GradeJudge judge = new GradeJudge();
// クラス全体の平均点を算出
int average = calculator.calcAverage(students);
System.out.println("平均: " + average);
// 学生ごとに A/B/C のランクを判定
for (Student s : students) {
System.out.println(s.getName() + ": " + judge.rank(s));
}
}
}

report メソッドは GradeReportService 自身では計算や判定のロジックを持たず、ScoreCalculatorcalcAverageGradeJudgerank に処理を委ねた結果を組み合わせて出力しています。

Main.java
// レポート対象となる学生 3 人のリストを作成
List<Student> students = new ArrayList<>();
students.add(new Student("田中", 75));
students.add(new Student("鈴木", 50));
students.add(new Student("佐藤", 90));
// GradeReportService を呼び出して成績レポートを出力
GradeReportService service = new GradeReportService();
service.report(students);

CarEngine をコンストラクターで一度だけ生成してフィールドに保持していましたが、この GradeReportServicereport を呼ぶたびに ScoreCalculatorGradeJudge を新しく生成しています。どちらも依存を自分で new する書き方ですが、保持するか毎回作り直すかが違います。

GradeReportService を、コンストラクターの引数で依存を受け取る書き方に変更します。

GradeReportService.java
import java.util.List;
public class GradeReportService {
// 外から受け取った ScoreCalculator と GradeJudge を保持するフィールド
private final ScoreCalculator calculator;
private final GradeJudge judge;
// インスタンス生成時に依存を受け取り、フィールドに代入
public GradeReportService(ScoreCalculator calculator, GradeJudge judge) {
this.calculator = calculator;
this.judge = judge;
}
public void report(List<Student> students) {
// 内部で new せず、フィールドの calculator と judge を使う
int average = calculator.calcAverage(students);
System.out.println("平均: " + average);
for (Student s : students) {
System.out.println(s.getName() + ": " + judge.rank(s));
}
}
}

依存をコンストラクターの引数で受け取り、フィールドに保持するこの設計を コンストラクター注入 と呼びます。

依存を保持するフィールドは private final で宣言します。

修飾子役割
privateクラス外部からの直接アクセスを禁止
final初期化後の書き換えを禁止

final を付けると、フィールドへの代入はコンストラクターでの初期化 1 回だけに限定されます。this.calculator = calculator で代入されたあと、calculator フィールドは別のインスタンスに書き換わりません。

Main.javaScoreCalculatorGradeJudge を作り、GradeReportService のコンストラクターに渡します。

Main.java
import java.util.ArrayList;
import java.util.List;
// レポート対象となる学生 3 人のリストを作成
List<Student> students = new ArrayList<>();
students.add(new Student("田中", 75));
students.add(new Student("鈴木", 50));
students.add(new Student("佐藤", 90));
// GradeReportService に渡すための依存を生成
ScoreCalculator calculator = new ScoreCalculator();
GradeJudge judge = new GradeJudge();
// 生成した依存をコンストラクターに渡して GradeReportService を組み立てる
GradeReportService service = new GradeReportService(calculator, judge);
// GradeReportService を呼び出して成績レポートを出力
service.report(students);
平均: 71
田中: B
鈴木: C
佐藤: A

new GradeReportService(calculator, judge) で、GradeReportService を呼び出すコードが依存を組み立てて渡しています。service.report を何度呼んでも、フィールドに保持された同じ calculatorjudge が再利用されます。

3. コンストラクター注入の利点

Section titled “3. コンストラクター注入の利点”

コンストラクター注入には、GradeReportService の中で直接 new する書き方にはない利点があります。

内部で new すると、生成するインスタンスが GradeReportService の中で固定されます。テスト用に状態を持たせた GradeJudge のインスタンスや、初期値を変えた GradeJudge のインスタンスを使いたいときは、GradeReportService 自身のコードを書き換える必要があります。

コンストラクター注入の書き方なら、GradeReportService を呼び出すコードで渡す GradeJudge のインスタンスを差し替えるだけで挙動を切り替えられます。GradeReportService のコードは変える必要がありません。

コンストラクター注入では、コンストラクターのシグネチャを見るだけで GradeReportService が何に依存しているかが外から読み取れます。

public GradeReportService(ScoreCalculator calculator, GradeJudge judge)

このシグネチャから、GradeReportService を使うには ScoreCalculatorGradeJudge が必要だと一目で分かります。内部で new する書き方では、メソッドの中身を読まないと依存関係が分かりません。

内部で new する書き方では、GradeReportService は「依存を作る」と「依存を使う」の両方を担っています。コンストラクター注入では、依存を作るのは GradeReportService を呼び出すコード、依存を使うのは GradeReportService という形で役割が分かれます。

GradeReportService 自身は「与えられた依存を使う」ことに集中でき、依存をどう用意するかは呼び出すコードに任せられます。クラスごとの責務が明確になり、設計が整理されます。