4.サービスクラスと単一責任
学習目標
- データを持つクラスとサービスクラスを区別して設計できる
- 単一責任の原則の意義を説明できる
- 役割が混ざったクラスを、複数のクラスに分けて再構成できる
- インスタンスメソッドと
staticメソッド、staticフィールドと定数の違いを説明できる
学生の名前と点数を保持する Student データクラスを共通の題材とします。
1. サービスクラス
Section titled “1. サービスクラス”データクラス は主にデータの保持を担います。実際のプログラムでは、保持したデータを使って何かを判定したり、整形したりする処理も必要になります。
たとえば、点数から合否やランクを判定する処理を考えます。判定ロジックを Student クラス自体に追加すると、次のようになります。
public class Student { private final String name; private final int score;
// コンストラクター、getter は省略
// 判定ロジックを Student に追加 public boolean isPassed() { return score >= 60; }
public String rank() { if (score >= 80) return "A"; if (score >= 60) return "B"; return "C"; }}簡単な判定ならこのままでも問題ありません。ただし、判定ルールが複雑になったり、別の判定が増えたりすると、Student の中にデータ保持と判定ロジックが共存し、肥大化します。
そこで、判定ロジックを別のクラスに切り出します。Student はデータ保持の役割だけを持ち、判定ルールの変更は切り出した判定クラスの中で完結します。
// 判定処理だけを提供するサービスクラスpublic class GradeJudge { public boolean isPassed(Student student) { return student.getScore() >= 60; }
public String rank(Student student) { int score = student.getScore(); if (score >= 80) return "A"; if (score >= 60) return "B"; return "C"; }}public class Student { private final String name; private final int score;
public Student(String name, int score) { this.name = name; this.score = score; }
public String getName() { return name; }
public int getScore() { return score; }}Main.java から両方のクラスを使います。
Student tanaka = new Student("田中", 75);GradeJudge judge = new GradeJudge();System.out.println(judge.isPassed(tanaka)); // trueSystem.out.println(judge.rank(tanaka)); // BGradeJudge はフィールドを持たず、合否とランクという「成績の判定」に関する処理をまとめて提供します。データの保持よりも処理の提供を主な役割とするクラスを サービスクラス と呼びます。
データクラスとサービスクラスを並べると、役割の違いがはっきりします。
| 種類 | 主な役割 | 例 |
|---|---|---|
| データクラス | データを保持する | Person、Student |
| サービスクラス | 処理を提供する | GradeJudge |
実際のクラスはデータと処理の両方を持つことも多いですが、設計するときに「このクラスの主な役割はどちらか」を意識すると、責務が明確になります。
2. 単一責任の原則
Section titled “2. 単一責任の原則”GradeJudge は 1 つの役割に絞ったクラスです。一方で、関連するメソッドを次々と 1 つのクラスに追加していく書き方もあり、その場合は別の問題が生じます。
2-1. 責務の混在
Section titled “2-1. 責務の混在”次の GradeHelper は、性質の異なる処理を 1 つのクラスに詰め込んだ例です。
import java.util.List;
public class GradeHelper { // 平均点を計算 public int calcAverage(List<Student> students) { int sum = 0; for (Student s : students) sum += s.getScore(); return sum / students.size(); }
// 合計点を計算 public int calcTotal(List<Student> students) { int sum = 0; for (Student s : students) sum += s.getScore(); return sum; }
// 合否を判定 public boolean isPassed(Student student) { return student.getScore() >= 60; }
// ランクを判定 public String rank(Student student) { int score = student.getScore(); if (score >= 80) return "A"; if (score >= 60) return "B"; return "C"; }
// 成績メッセージを整形 public String formatGradeMessage(Student student, String rank) { return student.getName() + " さんの成績: " + rank; }
// 合否メッセージを整形 public String formatResultMessage(Student student, boolean passed) { return student.getName() + " さんは " + (passed ? "合格" : "不合格") + " です"; }}GradeHelper には「算術計算」「業務ルールの判定」「文字列の整形」という性質の異なる処理が混ざっています。これらは変更が起きる理由がそれぞれ独立しています。
- 計算式を変えたい(重み付けの導入など)
- ランクの境界や合格基準を変えたい(評価制度の変更)
- メッセージのフォーマットを変えたい(表記の変更)
異なる理由の変更が、すべて同じクラスに集中することになります。1 つの修正が他の処理に影響する可能性も高まります。
2-2. 責務の分割
Section titled “2-2. 責務の分割”性質ごとにメソッドをまとめて、3 つのクラスに分割します。
import java.util.List;
public class ScoreCalculator { public int calcAverage(List<Student> students) { int sum = 0; for (Student s : students) sum += s.getScore(); return sum / students.size(); }
public int calcTotal(List<Student> students) { int sum = 0; for (Student s : students) sum += s.getScore(); return sum; }}public class GradeJudge { public boolean isPassed(Student student) { return student.getScore() >= 60; }
public String rank(Student student) { int score = student.getScore(); if (score >= 80) return "A"; if (score >= 60) return "B"; return "C"; }}public class GradeMessageFormatter { public String formatGradeMessage(Student student, String rank) { return student.getName() + " さんの成績: " + rank; }
public String formatResultMessage(Student student, boolean passed) { return student.getName() + " さんは " + (passed ? "合格" : "不合格") + " です"; }}ScoreCalculator には算術計算のメソッドが、GradeJudge には成績の判定が、GradeMessageFormatter には文字列整形が、それぞれまとまっています。各クラスは関連するメソッドの集まりとして 1 つの責務を担います。
クラスごとの変更の影響範囲が局所化され、後から仕様が変わっても他のクラスに波及しにくくなります。
このように、1 つのクラスに 1 つの責務を持たせる考え方を 単一責任の原則 と呼びます。役割を分けたクラスを 1 つのプログラムでまとめて使うには、それぞれを連携させる仕組みが必要になります。
3. クラスに紐づくメソッドとフィールド
Section titled “3. クラスに紐づくメソッドとフィールド”GradeJudge のようなサービスクラスは、フィールドを持たず処理を提供するだけです。それでも利用するときは new GradeJudge() でインスタンスを生成する必要があります。Java には、インスタンスを生成せずにクラス自体に紐づける形でメソッドやフィールドを定義する仕組みもあります。
3-1. static メソッド
Section titled “3-1. static メソッド”メソッド定義に static を付けると、クラスに紐づくメソッドになります。
class クラス名 { static 戻り値の型 メソッド名(引数) { 処理 }}public class MathUtil { // static: インスタンスを作らずクラス名で呼び出せる public static int square(int n) { return n * n; }}呼び出すときは クラス名.メソッド名(...) の形で書き、インスタンスを new する必要はありません。
int result = MathUtil.square(5); // 25Java の標準ライブラリにも、static メソッドが多数用意されています。たとえば Math.max は次のように呼び出せます。
int max = Math.max(10, 20); // 20MathUtil.square や Math.max のように、クラスに紐づいて呼び出すメソッドを static メソッド と呼びます。インスタンスメソッドが「インスタンスに紐づく」のに対し、static メソッドは「クラスに紐づく」点が異なります。
3-2. static フィールドと定数
Section titled “3-2. static フィールドと定数”static はメソッドだけでなくフィールドにも付けられます。Math.PI は円周率を表すフィールドで、これも Math のインスタンスを new せずに参照できます。
System.out.println(Math.PI); // 3.141592653589793Math.PI のように、クラスに紐づいて持つ値を static フィールド と呼びます。
static final の組み合わせは「クラス全体で共有する書き換え不可の値」を表し、定数 と呼ばれます。たとえば GradeJudge の合格点 60 を、定数として外に出せます。
public class GradeJudge { // 定数(クラスで共有、書き換え不可) public static final int PASSING_SCORE = 60;
public boolean isPassed(Student student) { return student.getScore() >= PASSING_SCORE; }
// rank メソッドは省略}定数の名前は大文字とアンダースコアで書く(PASSING_SCORE)のが Java の慣習です。