演習: サービスクラスと単一責任
各問題ごとに .java ファイルを作り、Main.java で動作確認します。
4-A. Greeting サービス
Section titled “4-A. Greeting サービス”挨拶メッセージを返すサービスクラス Greeting を定義してください。次のとおり定義します。
- フィールドなし
sayHello(String name):"こんにちは、" + name + "さん"の形式の文字列を返すsayGoodbye(String name):"さようなら、" + name + "さん"の形式の文字列を返す
Greeting greeting = new Greeting();System.out.println(greeting.sayHello("田中"));System.out.println(greeting.sayGoodbye("鈴木"));出力例:
こんにちは、田中さんさようなら、鈴木さんヒント
サービスクラスはフィールドを持たず、処理を提供するメソッドだけを定義します。本文 サービスクラス の GradeJudge と同じ構造です。各メソッドは引数の name と固定の文字列を + で連結して返します。
4-B. StudentService
Section titled “4-B. StudentService”本文 サービスクラス で示した Student データクラスをそのまま使います。
Student を引数に取るサービスクラス StudentService を定義してください。次のとおり定義します。
- フィールドなし
isPassed(Student student): 点数が60以上ならtrue、そうでなければfalseを返すformatResult(Student student): 合格なら"田中さんは合格です"、不合格なら"田中さんは不合格です"の形式の文字列を返す
Student tanaka = new Student("田中", 75);Student suzuki = new Student("鈴木", 40);
StudentService service = new StudentService();System.out.println(service.isPassed(tanaka));System.out.println(service.formatResult(tanaka));System.out.println(service.formatResult(suzuki));出力例:
true田中さんは合格です鈴木さんは不合格ですヒント
StudentService が Student を引数で受け取る形は、本文 サービスクラス の GradeJudge.isPassed(Student student) と同じパターンです。formatResult の中では isPassed の結果に応じて返す文字列を変えます。Student の名前は student.getName() で取り出せます。
4-C. OrderCalculator
Section titled “4-C. OrderCalculator”商品の注文金額を計算するサービスクラスを書きます。データクラス OrderItem は次のとおり用意されています。
public class OrderItem { private final String name; private final int price; private final int quantity;
public OrderItem(String name, int price, int quantity) { this.name = name; this.price = price; this.quantity = quantity; }
public String getName() { return name; }
public int getPrice() { return price; }
public int getQuantity() { return quantity; }}サービスクラス OrderCalculator を定義してください。次のとおり定義します。
- フィールドなし
calcSubtotal(OrderItem item):price * quantityを返すcalcTotal(List<OrderItem> 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));items.add(new OrderItem("バナナ", 200, 2));
OrderCalculator calculator = new OrderCalculator();System.out.println(calculator.calcSubtotal(items.get(0)));System.out.println(calculator.calcTotal(items));出力例:
4501250ヒント
calcSubtotal は OrderItem の getPrice() と getQuantity() を掛け合わせます。calcTotal は拡張 for 文 for (OrderItem item : items) で各商品の小計を順に加算します。クラス内で calcSubtotal を呼び出して使うこともできます。
4-D. UserHelper の責務分割
Section titled “4-D. UserHelper の責務分割”次の UserHelper は、性質の異なる処理を 1 つのクラスに詰め込んだ反例です。これを責務ごとに 3 つのクラスに分割してください。
public class UserHelper { // メールアドレスの形式を検査 public boolean isValidEmail(String email) { return email != null && email.contains("@"); }
// 年齢が成人かを判定 public boolean isAdult(int age) { return age >= 18; }
// ユーザー情報のメッセージを整形 public String formatProfile(String name, int age) { return name + "(" + age + "歳)"; }
// 挨拶メッセージを整形 public String formatGreeting(String name) { return "こんにちは、" + name + "さん"; }}3 つのクラスに分割します。
EmailValidator:isValidEmail(String email)を提供AgeChecker:isAdult(int age)を提供UserMessageFormatter:formatProfile(String name, int age)とformatGreeting(String name)を提供
EmailValidator emailValidator = new EmailValidator();AgeChecker ageChecker = new AgeChecker();UserMessageFormatter formatter = new UserMessageFormatter();
System.out.println(emailValidator.isValidEmail("tanaka@example.com"));System.out.println(emailValidator.isValidEmail("invalid-email"));System.out.println(ageChecker.isAdult(20));System.out.println(formatter.formatProfile("田中", 25));System.out.println(formatter.formatGreeting("田中"));出力例:
truefalsetrue田中(25歳)こんにちは、田中さんヒント
本文 責務の分割 で GradeHelper を ScoreCalculator、GradeJudge、GradeMessageFormatter に分割した例と同じパターンです。UserHelper のメソッドは「メールの検査」「年齢の判定」「メッセージの整形」の 3 つの性質に分けられます。Helper のような曖昧な名前ではなく、各クラスの役割を表す名詞を選びます。
4-E. MathUtil の定義
Section titled “4-E. MathUtil の定義”簡単な数学計算を提供する MathUtil を定義してください。次のとおり定義します。
- フィールドなし
- すべて
staticメソッド square(int n):nの 2 乗を返すcube(int n):nの 3 乗を返すabs(int n):nの絶対値を返す(nが負なら符号を反転)
System.out.println(MathUtil.square(5));System.out.println(MathUtil.cube(3));System.out.println(MathUtil.abs(-7));System.out.println(MathUtil.abs(7));出力例:
252777ヒント
本文 static メソッド の MathUtil.square と同じパターンです。static メソッドは クラス名.メソッド名(...) の形で呼び出すため、利用側で new MathUtil() を書く必要はありません。abs は if (n < 0) で符号を反転します。
4-F. Math 標準ライブラリの活用
Section titled “4-F. Math 標準ライブラリの活用”Java 標準ライブラリの Math クラスの static メソッドを使って、直角三角形の斜辺の長さを求めてください。斜辺は √(a² + b²) で計算できます。
Main.java で 2 つの辺の長さから斜辺を計算します。
Math.sqrt(double): 平方根を計算するMath.pow(double, double): 累乗を計算する(Math.pow(a, 2)でaの 2 乗)
double a = 3;double b = 4;
double hypotenuse = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));System.out.println("斜辺の長さ: " + hypotenuse);出力例:
斜辺の長さ: 5.0a と b を 5.0 と 12.0 などに変えて、13.0 が出力されることも確認してください。
ヒント
Math クラスのメソッドはすべて static なので、インスタンスを new せずに Math.sqrt(...) の形で呼び出せます。Math.pow(a, 2) の代わりに a * a と書いても同じ結果が得られます。
4-G. Config の定義
Section titled “4-G. Config の定義”アプリケーション全体で共有する設定値を Config クラスに集約してください。次のとおり定義します。
- すべて
public static finalの定数 MAX_RETRY_COUNT: int、3DEFAULT_PAGE_SIZE: int、20APP_NAME: String、"BookStore"TIMEOUT_SECONDS: int、30
System.out.println("アプリ名: " + Config.APP_NAME);System.out.println("最大リトライ回数: " + Config.MAX_RETRY_COUNT);System.out.println("1 ページの件数: " + Config.DEFAULT_PAGE_SIZE);System.out.println("タイムアウト: " + Config.TIMEOUT_SECONDS + " 秒");出力例:
アプリ名: BookStore最大リトライ回数: 31 ページの件数: 20タイムアウト: 30 秒ヒント
本文 static フィールドと定数 の PASSING_SCORE と同じパターンです。public static final は「クラス全体で共有」「外から参照可能」「書き換え不可」の組み合わせです。定数の名前は大文字とアンダースコアで書きます。
4-H. マジックナンバーのリファクタリング
Section titled “4-H. マジックナンバーのリファクタリング”次の ShippingCalculator には、コードに直接書かれた数値(マジックナンバー)が複数あります。それぞれを名前付きの定数に置き換えてください。
public class ShippingCalculator { public int calcShippingFee(int totalPrice) { if (totalPrice >= 5000) { return 0; } if (totalPrice >= 3000) { return 300; } return 600; }}4 つの定数を導入します。
FREE_SHIPPING_THRESHOLD: 送料無料になる金額DISCOUNT_SHIPPING_THRESHOLD: 送料割引になる金額DISCOUNTED_SHIPPING_FEE: 割引送料STANDARD_SHIPPING_FEE: 通常送料
ShippingCalculator calculator = new ShippingCalculator();System.out.println(calculator.calcShippingFee(6000));System.out.println(calculator.calcShippingFee(4000));System.out.println(calculator.calcShippingFee(1500));出力例:
0300600ヒント
public static final int FREE_SHIPPING_THRESHOLD = 5000; のように定数を定義し、if (totalPrice >= FREE_SHIPPING_THRESHOLD) のように使います。マジックナンバーを定数に置き換えると、値の意味がコードから読み取れ、変更も 1 か所で済みます。
4-I. DateUtil の設計
Section titled “4-I. DateUtil の設計”日付処理のユーティリティを提供する DateUtil を設計してください。次の機能を持たせます。
- すべて
staticメソッド isLeapYear(int year): 与えられた年が閏年ならtrueを返す。条件は「4 で割り切れる」かつ「100 で割り切れない」、または「400 で割り切れる」daysInMonth(int year, int month): 指定された年月の日数を返す(1, 3, 5, 7, 8, 10, 12 月は 31、4, 6, 9, 11 月は 30、2 月は閏年なら 29、それ以外は 28)isValidDate(int year, int month, int day): 月が 1〜12 の範囲内、かつ日が 1 以上その月の日数以下ならtrueを返す
System.out.println(DateUtil.isLeapYear(2024));System.out.println(DateUtil.isLeapYear(2023));System.out.println(DateUtil.isLeapYear(2000));System.out.println(DateUtil.isLeapYear(1900));
System.out.println(DateUtil.daysInMonth(2024, 2));System.out.println(DateUtil.daysInMonth(2023, 2));System.out.println(DateUtil.daysInMonth(2024, 4));
System.out.println(DateUtil.isValidDate(2024, 2, 29));System.out.println(DateUtil.isValidDate(2023, 2, 29));出力例:
truefalsetruefalse292830truefalseヒント
isLeapYear の判定式は (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 です。daysInMonth は switch または if-else で月ごとに返す日数を分けます。isValidDate は daysInMonth を内部で呼び出すと簡潔に書けます。static メソッドの中から同じクラスの別の static メソッドを呼ぶときは、クラス名を省略して daysInMonth(year, month) のように書けます。
4-J. プロフィール表示の責務分割
Section titled “4-J. プロフィール表示の責務分割”次の ProfileHelper には、プロフィール画面を構成する処理が混在しています。責務ごとに分割してください。
public class ProfileHelper { // 表示用に整形 public String formatName(String firstName, String lastName) { return lastName + " " + firstName; }
// 自己紹介文を整形 public String formatBio(String bio) { if (bio == null || bio.isEmpty()) { return "(自己紹介なし)"; } return bio; }
// 年齢を計算 public int calcAge(int birthYear, int currentYear) { return currentYear - birthYear; }
// 成人かを判定 public boolean isAdult(int age) { return age >= 18; }}責務ごとのクラス名と役割を自分で決めて、3 つのクラスに分割してください。クラス名は曖昧な Helper を避け、何をするクラスかが名前から読み取れる名詞を選びます。
Main.java で各クラスのインスタンスを作成し、次のような呼び出しで動作確認します。
// 各自で分割したクラスを使って呼び出します// 例:// formatter.formatName("太郎", "田中") → "田中 太郎"// formatter.formatBio("Java を学んでいます") → "Java を学んでいます"// formatter.formatBio("") → "(自己紹介なし)"// calculator.calcAge(2000, 2025) → 25// checker.isAdult(20) → trueヒント
formatName と formatBio は「表示用の整形」、calcAge は「計算」、isAdult は「判定」の責務です。たとえば ProfileFormatter(整形)、AgeCalculator(計算)、AgeChecker(判定)のような名前にすると、責務が読み取れます。年齢関連のメソッドが 2 つに分かれるのは「計算」と「判定」が別の責務だからです。