5.コンポジション
学習目標
- あるクラスが別のクラスを部品として利用する構造を説明できる
- 依存をコンストラクターで受け取り、
private finalフィールドに保持する書き方ができる - 依存を内部で
newする場合と外から受け取る場合の違いを説明できる
1. コンポジションと依存
Section titled “1. コンポジションと依存”クラスのフィールドの型には、int や String のような基本型だけでなく、自分で定義したクラスも指定できます。あるクラスが別のクラスをフィールドとして持つことで、複数のクラスを組み合わせたプログラムが作れます。
例として、車とエンジンの関係を 2 つのクラスで表現します。
public class Engine { public void start() { System.out.println("エンジン始動"); }}Engine はフィールドを持たず、start メソッドだけを提供します。これを部品として使う Car を定義します。
public class Car { private final Engine engine;
public Car() { this.engine = new Engine(); }
public void start() { engine.start(); System.out.println("車が発進"); }}Car は Engine 型のフィールド engine を持ち、コンストラクターの中で new Engine() を実行してフィールドに代入しています。Car.start() の中で engine.start() が呼ばれ、エンジン始動の処理は Engine 側で実行されます。
Car car = new Car();car.start();エンジン始動車が発進このように、あるクラスが別のクラスを部品として利用してプログラムを構成する構造を コンポジション(クラスの組み合わせ)と呼びます。Car は Engine を必要とするため、Engine に 依存 しています。
2. 依存の渡し方
Section titled “2. 依存の渡し方”ScoreCalculatorで平均点を計算し、GradeJudgeでランクを判定する処理を組み合わせると、学生のリストから成績レポートを出力できます。この処理を GradeReportService というサービスクラスにまとめます。
Car が Engine を 1 つ持つように、GradeReportService は ScoreCalculator と GradeJudge の 2 つの依存を持ちます。依存の渡し方には、依存を自分のクラスの中で new する書き方と、コンストラクターの引数で外から受け取る書き方があります。
2-1. 依存の内部生成
Section titled “2-1. 依存の内部生成”依存を自分で new する書き方です。GradeReportService の report メソッドの中で、必要なインスタンスを生成します。
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 自身では計算や判定のロジックを持たず、ScoreCalculator の calcAverage と GradeJudge の rank に処理を委ねた結果を組み合わせて出力しています。
// レポート対象となる学生 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);Car は Engine をコンストラクターで一度だけ生成してフィールドに保持していましたが、この GradeReportService は report を呼ぶたびに ScoreCalculator と GradeJudge を新しく生成しています。どちらも依存を自分で new する書き方ですが、保持するか毎回作り直すかが違います。
2-2. コンストラクター注入
Section titled “2-2. コンストラクター注入”GradeReportService を、コンストラクターの引数で依存を受け取る書き方に変更します。
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)); } }}依存をコンストラクターの引数で受け取り、フィールドに保持するこの設計を コンストラクター注入 と呼びます。
2-3. private final フィールド
Section titled “2-3. private final フィールド”依存を保持するフィールドは private final で宣言します。
| 修飾子 | 役割 |
|---|---|
private | クラス外部からの直接アクセスを禁止 |
final | 初期化後の書き換えを禁止 |
final を付けると、フィールドへの代入はコンストラクターでの初期化 1 回だけに限定されます。this.calculator = calculator で代入されたあと、calculator フィールドは別のインスタンスに書き換わりません。
2-4. GradeReportService の組み立て
Section titled “2-4. GradeReportService の組み立て”Main.java で ScoreCalculator と GradeJudge を作り、GradeReportService のコンストラクターに渡します。
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佐藤: Anew GradeReportService(calculator, judge) で、GradeReportService を呼び出すコードが依存を組み立てて渡しています。service.report を何度呼んでも、フィールドに保持された同じ calculator と judge が再利用されます。
3. コンストラクター注入の利点
Section titled “3. コンストラクター注入の利点”コンストラクター注入には、GradeReportService の中で直接 new する書き方にはない利点があります。
3-1. 依存の差し替え
Section titled “3-1. 依存の差し替え”内部で new すると、生成するインスタンスが GradeReportService の中で固定されます。テスト用に状態を持たせた GradeJudge のインスタンスや、初期値を変えた GradeJudge のインスタンスを使いたいときは、GradeReportService 自身のコードを書き換える必要があります。
コンストラクター注入の書き方なら、GradeReportService を呼び出すコードで渡す GradeJudge のインスタンスを差し替えるだけで挙動を切り替えられます。GradeReportService のコードは変える必要がありません。
3-2. 依存の可視化
Section titled “3-2. 依存の可視化”コンストラクター注入では、コンストラクターのシグネチャを見るだけで GradeReportService が何に依存しているかが外から読み取れます。
public GradeReportService(ScoreCalculator calculator, GradeJudge judge)このシグネチャから、GradeReportService を使うには ScoreCalculator と GradeJudge が必要だと一目で分かります。内部で new する書き方では、メソッドの中身を読まないと依存関係が分かりません。
3-3. 責務の明確化
Section titled “3-3. 責務の明確化”内部で new する書き方では、GradeReportService は「依存を作る」と「依存を使う」の両方を担っています。コンストラクター注入では、依存を作るのは GradeReportService を呼び出すコード、依存を使うのは GradeReportService という形で役割が分かれます。
GradeReportService 自身は「与えられた依存を使う」ことに集中でき、依存をどう用意するかは呼び出すコードに任せられます。クラスごとの責務が明確になり、設計が整理されます。