演習: コンポジション
各問題ごとに .java ファイルを作り、Main.java で動作確認します。
5-A. Printer の定義
Section titled “5-A. Printer の定義”挨拶メッセージを表示する Printer クラスを定義してください。次のとおり定義します。
- フィールド:
greeting(演習 4-A で作ったGreeting型、private final) - コンストラクターで
Greetingのインスタンスを受け取り、フィールドに代入する printHello(String name):greeting.sayHello(name)の戻り値をSystem.out.printlnで出力する
Main.java で Greeting のインスタンスを生成し、Printer のコンストラクターに渡してから printHello を呼びます。
Greeting greeting = new Greeting();Printer printer = new Printer(greeting);printer.printHello("田中");出力例:
こんにちは、田中さんヒント
本文 コンストラクター注入 と同じ書き方です。フィールドは private final Greeting greeting; の形で宣言し、コンストラクターで this.greeting = greeting; のように受け取った引数を代入します。
5-B. Printer に Counter を追加
Section titled “5-B. Printer に Counter を追加”5-A で作った Printer に、挨拶した回数を数える機能を追加してください。Counter クラス(演習 1-A で作ったもの)を 2 つ目の依存として受け取ります。
- フィールド:
greeting(5-A と同じ)とcounter(Counter型、private final) - コンストラクターで
GreetingとCounterの両方を受け取る printHello(String name):greeting.sayHello(name)の戻り値を出力したあと、counter.increment()で回数を増やすprintCount():counter.getCount()で現在の回数を取得し、これまでに N 人に挨拶しましたの形式で出力する
Greeting greeting = new Greeting();Counter counter = new Counter(0);Printer printer = new Printer(greeting, counter);
printer.printHello("田中");printer.printHello("鈴木");printer.printHello("佐藤");printer.printCount();出力例:
こんにちは、田中さんこんにちは、鈴木さんこんにちは、佐藤さんこれまでに 3 人に挨拶しましたヒント
本文 コンストラクター注入 の GradeReportService と同じ構造です。コンストラクターの引数を 2 つに増やし、両方のフィールドに代入します。
5-C. final フィールドのコンパイルエラー
Section titled “5-C. final フィールドのコンパイルエラー”次のコードはコンパイルエラーになります。エラーの理由を説明し、コンパイルが通るように修正してください。
public class Printer { private final Greeting greeting; private final Counter counter;
public Printer(Greeting greeting) { this.greeting = greeting; }
public void printHello(String name) { System.out.println(greeting.sayHello(name)); counter.increment(); }}ヒント
private final フィールドはコンストラクターでの初期化が必須です。本文 private final フィールド を参照してください。counter フィールドが宣言されているのにコンストラクターで初期化されていないため、コンパイラーがエラーを出します。
修正方法は 2 通りあります。どちらの形が Printer の役割に合うかを考えて選んでください。
5-D. UserRegistration
Section titled “5-D. UserRegistration”ユーザー登録のサービスクラス UserRegistration を定義してください。EmailValidator と AgeChecker(演習 4-D で作ったもの)の 2 つを依存に持ちます。
- フィールド:
emailValidator(EmailValidator型、private final)、ageChecker(AgeChecker型、private final) - コンストラクターで
EmailValidatorとAgeCheckerの両方を受け取る register(String email, int age): 次の順で検査し、結果を出力するemailValidator.isValidEmail(email)がfalseならメールアドレスが不正です: メールを出力して終了ageChecker.isAdult(age)がfalseなら未成年は登録できません: 年齢を出力して終了- 両方の検査を通ったら
登録しました: メールを出力
Main.java で 3 件のユーザー登録を試します。
EmailValidator emailValidator = new EmailValidator();AgeChecker ageChecker = new AgeChecker();UserRegistration registration = new UserRegistration(emailValidator, ageChecker);
registration.register("tanaka@example.com", 25);registration.register("invalid-email", 30);registration.register("suzuki@example.com", 15);出力例:
登録しました: tanaka@example.comメールアドレスが不正です: invalid-email未成年は登録できません: 15ヒント
本文 GradeReportService の組み立て の Main.java と同じパターンです。2 つの依存を new で生成してから、UserRegistration のコンストラクターに渡します。
register メソッドの中では、検査に失敗したら return; で早期に処理を抜けます。
5-E. DiceRoller
Section titled “5-E. DiceRoller”サイコロを振るサービスクラス DiceRoller を定義してください。Java 標準ライブラリの java.util.Random を依存として受け取ります。
- フィールド:
random(java.util.Random型、private final) - コンストラクターで
Randomのインスタンスを受け取る roll():random.nextInt(6) + 1で 1〜6 の整数を返すrollMultiple(int times): 指定回数サイコロを振り、各結果をSystem.out.printlnで 1 行ずつ出力する
Main.java で Random のインスタンスを生成して DiceRoller に渡し、サイコロを 5 回振ります。
import java.util.Random;
Random random = new Random();DiceRoller dice = new DiceRoller(random);
dice.rollMultiple(5);出力例(実行ごとに変わります):
31642ヒント
java.util.Random の nextInt(6) は 0 以上 6 未満(つまり 0〜5)の整数を返します。サイコロの 1〜6 にするために + 1 を加えます。rollMultiple の中で for 文を使い、roll() を times 回呼び出します。
依存が標準ライブラリのクラスでも、コンストラクター注入の書き方は同じです。フィールドの型を Random にして、コンストラクターで受け取ります。
5-F. OrderService
Section titled “5-F. OrderService”注文を受け付けるサービスクラス OrderService を定義してください。OrderCalculator(演習 4-C で作ったもの)を依存に持ちます。
- フィールド:
calculator(OrderCalculator型、private final) - コンストラクターで
OrderCalculatorを受け取る placeOrder(List<OrderItem> items): 次の処理を順に行うcalculator.calcTotal(items)で合計金額を計算注文を受け付けました。合計金額: 値円を出力
import java.util.ArrayList;import java.util.List;
List<OrderItem> items = new ArrayList<>();items.add(new OrderItem("りんご", 150, 3));items.add(new OrderItem("みかん", 80, 5));
OrderCalculator calculator = new OrderCalculator();OrderService service = new OrderService(calculator);
service.placeOrder(items);出力例:
注文を受け付けました。合計金額: 850円ヒント
OrderCalculator.calcTotal(items) は合計金額を int で返します。OrderService 自身は金額計算のロジックを持たず、OrderCalculator に処理を任せます。
5-G. 内部 new からコンストラクター注入への書き換え
Section titled “5-G. 内部 new からコンストラクター注入への書き換え”次の GradeReportService は、依存しているクラスを report メソッドの中で new する書き方になっています。コンストラクター注入の書き方に変更してください。
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);
for (Student s : students) { System.out.println(s.getName() + ": " + judge.rank(s)); } }}書き換えたあと、Main.java で ScoreCalculator と GradeJudge のインスタンスを生成し、GradeReportService のコンストラクターに渡す形に変更します。
import java.util.ArrayList;import java.util.List;
List<Student> students = new ArrayList<>();students.add(new Student("田中", 75));students.add(new Student("鈴木", 50));students.add(new Student("佐藤", 90));
ScoreCalculator calculator = new ScoreCalculator();GradeJudge judge = new GradeJudge();GradeReportService service = new GradeReportService(calculator, judge);
service.report(students);出力例:
平均: 71田中: B鈴木: C佐藤: Aヒント
書き換え後の GradeReportService には次の 3 つを追加します。
private final ScoreCalculator calculator;とprivate final GradeJudge judge;のフィールド- 2 つの依存を受け取るコンストラクター
reportメソッドの中のnew ScoreCalculator()とnew GradeJudge()を削除し、フィールドのcalculatorとjudgeを使う
5-H. Counter の共有と差し替え
Section titled “5-H. Counter の共有と差し替え”Counter(演習 1-A で作ったもの)を依存として受け取る Visitor クラスを定義してください。
- フィールド:
counter(Counter型、private final) - コンストラクターで
Counterを受け取る visit(String name):counter.increment()を呼んだあと、name さんが訪問しました(合計 N 人)の形式で出力する。N はcounter.getCount()の値
Main.java で 2 つの Visitor インスタンスを作ります。最初は同じ Counter を共有して合計人数が積み上がることを確認し、その後で片方だけ別の Counter インスタンスに切り替えます。
// 1 つの Counter を 2 つの Visitor で共有Counter sharedCounter = new Counter(0);Visitor entranceA = new Visitor(sharedCounter);Visitor entranceB = new Visitor(sharedCounter);
entranceA.visit("田中");entranceB.visit("鈴木");entranceA.visit("佐藤");
// 別の Counter を渡して新しい Visitor を作るCounter separateCounter = new Counter(0);Visitor entranceC = new Visitor(separateCounter);
entranceC.visit("中村");entranceA.visit("山田");出力例:
田中さんが訪問しました(合計 1 人)鈴木さんが訪問しました(合計 2 人)佐藤さんが訪問しました(合計 3 人)中村さんが訪問しました(合計 1 人)山田さんが訪問しました(合計 4 人)entranceA と entranceB は sharedCounter を共有しているので、田中・鈴木・佐藤・山田の 4 人の訪問が同じカウントに積み上がります。entranceC は別の separateCounter を持つので、中村の訪問は独立してカウントされます。
ヒント
同じ Counter インスタンスを 2 つの Visitor のコンストラクターに渡すと、両方の Visitor が同じカウントを共有します。別の Counter インスタンスを渡せば、独立したカウントを持つ Visitor になります。
コンストラクター注入の利点は、Visitor のコードを変えずに、渡す Counter のインスタンスを変えるだけで挙動が切り替わる点にあります。
5-I. BookFormatter
Section titled “5-I. BookFormatter”Book(演習 1-H で作ったもの)を整形して文字列を返すサービスクラス BookFormatter を定義してください。
- フィールドなし
format(Book book):Bookの情報を『書名』(価格円、在庫 N 冊)の形式の文字列で返す
Book book = new Book("こころ", 650, 3);
BookFormatter formatter = new BookFormatter();System.out.println(formatter.format(book));出力例:
『こころ』(650円、在庫 3 冊)ヒント
BookFormatter 自身は依存を持ちません。引数で受け取った Book の getter(getTitle()、getPrice()、getStock())を呼び出して、文字列を組み立てて返します。
演習 1-I のヒントでは getTitle() と getStock() だけ追加していたかもしれません。本問では getPrice() も必要なので、まだ定義していなければ追加してください。
5-J. BookService
Section titled “5-J. BookService”書店の本を管理するサービスクラス BookService を定義してください。5-I で作った BookFormatter を依存に持ち、整形対象の Book のリストもフィールドに保持します。
- フィールド:
formatter(BookFormatter型、private final)、books(List<Book>型、private final) - コンストラクターで
BookFormatterとList<Book>の両方を受け取る showAll(): 全部の本をformatter.format(book)で整形して順に出力するshowOutOfStock(): 在庫が0の本だけをformatter.format(book)で整形して出力する
import java.util.ArrayList;import java.util.List;
List<Book> books = new ArrayList<>();books.add(new Book("こころ", 650, 3));books.add(new Book("吾輩は猫である", 780, 0));books.add(new Book("ノルウェイの森", 680, 5));books.add(new Book("人間失格", 590, 0));
BookFormatter formatter = new BookFormatter();BookService service = new BookService(formatter, books);
System.out.println("--- 全部の本 ---");service.showAll();
System.out.println("--- 在庫切れ ---");service.showOutOfStock();出力例:
--- 全部の本 ---『こころ』(650円、在庫 3 冊)『吾輩は猫である』(780円、在庫 0 冊)『ノルウェイの森』(680円、在庫 5 冊)『人間失格』(590円、在庫 0 冊)--- 在庫切れ ---『吾輩は猫である』(780円、在庫 0 冊)『人間失格』(590円、在庫 0 冊)ヒント
BookService は BookFormatter に整形処理を任せ、自分では出力の組み立てを書きません。showAll と showOutOfStock の中で、拡張 for 文 for (Book book : books) を使ってリストの各要素を順に処理します。
BookService が BookFormatter を依存に持ち、BookFormatter が Book を処理する、という 2 段階の構造です。コンストラクター注入の書き方は依存の数や種類が増えても変わりません。