Skip to content
Playground

演習: サービスクラスと単一責任

各問題ごとに .java ファイルを作り、Main.java で動作確認します。

挨拶メッセージを返すサービスクラス Greeting を定義してください。次のとおり定義します。

  • フィールドなし
  • sayHello(String name): "こんにちは、" + name + "さん" の形式の文字列を返す
  • sayGoodbye(String name): "さようなら、" + name + "さん" の形式の文字列を返す
Main.java
Greeting greeting = new Greeting();
System.out.println(greeting.sayHello("田中"));
System.out.println(greeting.sayGoodbye("鈴木"));

出力例:

こんにちは、田中さん
さようなら、鈴木さん
ヒント

サービスクラスはフィールドを持たず、処理を提供するメソッドだけを定義します。本文 サービスクラスGradeJudge と同じ構造です。各メソッドは引数の name と固定の文字列を + で連結して返します。

本文 サービスクラス で示した Student データクラスをそのまま使います。

Student を引数に取るサービスクラス StudentService を定義してください。次のとおり定義します。

  • フィールドなし
  • isPassed(Student student): 点数が 60 以上なら true、そうでなければ false を返す
  • formatResult(Student student): 合格なら "田中さんは合格です"、不合格なら "田中さんは不合格です" の形式の文字列を返す
Main.java
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
田中さんは合格です
鈴木さんは不合格です
ヒント

StudentServiceStudent を引数で受け取る形は、本文 サービスクラスGradeJudge.isPassed(Student student) と同じパターンです。formatResult の中では isPassed の結果に応じて返す文字列を変えます。Student の名前は student.getName() で取り出せます。

商品の注文金額を計算するサービスクラスを書きます。データクラス OrderItem は次のとおり用意されています。

OrderItem.java
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): すべての商品の小計を合算した値を返す
Main.java
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));

出力例:

450
1250
ヒント

calcSubtotalOrderItemgetPrice()getQuantity() を掛け合わせます。calcTotal は拡張 for 文 for (OrderItem item : items) で各商品の小計を順に加算します。クラス内で calcSubtotal を呼び出して使うこともできます。

次の UserHelper は、性質の異なる処理を 1 つのクラスに詰め込んだ反例です。これを責務ごとに 3 つのクラスに分割してください。

UserHelper.java(反例)
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) を提供
Main.java
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("田中"));

出力例:

true
false
true
田中(25歳)
こんにちは、田中さん
ヒント

本文 責務の分割GradeHelperScoreCalculatorGradeJudgeGradeMessageFormatter に分割した例と同じパターンです。UserHelper のメソッドは「メールの検査」「年齢の判定」「メッセージの整形」の 3 つの性質に分けられます。Helper のような曖昧な名前ではなく、各クラスの役割を表す名詞を選びます。

簡単な数学計算を提供する MathUtil を定義してください。次のとおり定義します。

  • フィールドなし
  • すべて static メソッド
  • square(int n): n の 2 乗を返す
  • cube(int n): n の 3 乗を返す
  • abs(int n): n の絶対値を返す(n が負なら符号を反転)
Main.java
System.out.println(MathUtil.square(5));
System.out.println(MathUtil.cube(3));
System.out.println(MathUtil.abs(-7));
System.out.println(MathUtil.abs(7));

出力例:

25
27
7
7
ヒント

本文 static メソッドMathUtil.square と同じパターンです。static メソッドは クラス名.メソッド名(...) の形で呼び出すため、利用側で new MathUtil() を書く必要はありません。absif (n < 0) で符号を反転します。

Java 標準ライブラリの Math クラスの static メソッドを使って、直角三角形の斜辺の長さを求めてください。斜辺は √(a² + b²) で計算できます。

Main.java で 2 つの辺の長さから斜辺を計算します。

  • Math.sqrt(double): 平方根を計算する
  • Math.pow(double, double): 累乗を計算する(Math.pow(a, 2)a の 2 乗)
Main.java
double a = 3;
double b = 4;
double hypotenuse = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
System.out.println("斜辺の長さ: " + hypotenuse);

出力例:

斜辺の長さ: 5.0

ab5.012.0 などに変えて、13.0 が出力されることも確認してください。

ヒント

Math クラスのメソッドはすべて static なので、インスタンスを new せずに Math.sqrt(...) の形で呼び出せます。Math.pow(a, 2) の代わりに a * a と書いても同じ結果が得られます。

アプリケーション全体で共有する設定値を Config クラスに集約してください。次のとおり定義します。

  • すべて public static final の定数
  • MAX_RETRY_COUNT: int、3
  • DEFAULT_PAGE_SIZE: int、20
  • APP_NAME: String、"BookStore"
  • TIMEOUT_SECONDS: int、30
Main.java
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
最大リトライ回数: 3
1 ページの件数: 20
タイムアウト: 30 秒
ヒント

本文 static フィールドと定数PASSING_SCORE と同じパターンです。public static final は「クラス全体で共有」「外から参照可能」「書き換え不可」の組み合わせです。定数の名前は大文字とアンダースコアで書きます。

4-H. マジックナンバーのリファクタリング

Section titled “4-H. マジックナンバーのリファクタリング”

次の ShippingCalculator には、コードに直接書かれた数値(マジックナンバー)が複数あります。それぞれを名前付きの定数に置き換えてください。

ShippingCalculator.java(書き換え前)
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: 通常送料
Main.java
ShippingCalculator calculator = new ShippingCalculator();
System.out.println(calculator.calcShippingFee(6000));
System.out.println(calculator.calcShippingFee(4000));
System.out.println(calculator.calcShippingFee(1500));

出力例:

0
300
600
ヒント

public static final int FREE_SHIPPING_THRESHOLD = 5000; のように定数を定義し、if (totalPrice >= FREE_SHIPPING_THRESHOLD) のように使います。マジックナンバーを定数に置き換えると、値の意味がコードから読み取れ、変更も 1 か所で済みます。

日付処理のユーティリティを提供する 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 を返す
Main.java
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));

出力例:

true
false
true
false
29
28
30
true
false
ヒント

isLeapYear の判定式は (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 です。daysInMonthswitch または if-else で月ごとに返す日数を分けます。isValidDatedaysInMonth を内部で呼び出すと簡潔に書けます。static メソッドの中から同じクラスの別の static メソッドを呼ぶときは、クラス名を省略して daysInMonth(year, month) のように書けます。

4-J. プロフィール表示の責務分割

Section titled “4-J. プロフィール表示の責務分割”

次の ProfileHelper には、プロフィール画面を構成する処理が混在しています。責務ごとに分割してください。

ProfileHelper.java(反例)
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 で各クラスのインスタンスを作成し、次のような呼び出しで動作確認します。

Main.java(例)
// 各自で分割したクラスを使って呼び出します
// 例:
// formatter.formatName("太郎", "田中") → "田中 太郎"
// formatter.formatBio("Java を学んでいます") → "Java を学んでいます"
// formatter.formatBio("") → "(自己紹介なし)"
// calculator.calcAge(2000, 2025) → 25
// checker.isAdult(20) → true
ヒント

formatNameformatBio は「表示用の整形」、calcAge は「計算」、isAdult は「判定」の責務です。たとえば ProfileFormatter(整形)、AgeCalculator(計算)、AgeChecker(判定)のような名前にすると、責務が読み取れます。年齢関連のメソッドが 2 つに分かれるのは「計算」と「判定」が別の責務だからです。