Skip to content
Playground

4.サービスクラスと単一責任

学習目標

  • データを持つクラスとサービスクラスを区別して設計できる
  • 単一責任の原則の意義を説明できる
  • 役割が混ざったクラスを、複数のクラスに分けて再構成できる
  • インスタンスメソッドと static メソッド、static フィールドと定数の違いを説明できる

学生の名前と点数を保持する Student データクラスを共通の題材とします。

データクラス は主にデータの保持を担います。実際のプログラムでは、保持したデータを使って何かを判定したり、整形したりする処理も必要になります。

たとえば、点数から合否やランクを判定する処理を考えます。判定ロジックを Student クラス自体に追加すると、次のようになります。

Student.java
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 はデータ保持の役割だけを持ち、判定ルールの変更は切り出した判定クラスの中で完結します。

GradeJudge.java
// 判定処理だけを提供するサービスクラス
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";
}
}

Main.java から両方のクラスを使います。

Main.java
Student tanaka = new Student("田中", 75);
GradeJudge judge = new GradeJudge();
System.out.println(judge.isPassed(tanaka)); // true
System.out.println(judge.rank(tanaka)); // B

GradeJudge はフィールドを持たず、合否とランクという「成績の判定」に関する処理をまとめて提供します。データの保持よりも処理の提供を主な役割とするクラスを サービスクラス と呼びます。

データクラスとサービスクラスを並べると、役割の違いがはっきりします。

種類主な役割
データクラスデータを保持するPersonStudent
サービスクラス処理を提供するGradeJudge

実際のクラスはデータと処理の両方を持つことも多いですが、設計するときに「このクラスの主な役割はどちらか」を意識すると、責務が明確になります。

GradeJudge は 1 つの役割に絞ったクラスです。一方で、関連するメソッドを次々と 1 つのクラスに追加していく書き方もあり、その場合は別の問題が生じます。

次の GradeHelper は、性質の異なる処理を 1 つのクラスに詰め込んだ例です。

GradeHelper.java
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 つの修正が他の処理に影響する可能性も高まります。

性質ごとにメソッドをまとめて、3 つのクラスに分割します。

ScoreCalculator.java
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;
}
}
GradeJudge.java
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";
}
}
GradeMessageFormatter.java
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 には、インスタンスを生成せずにクラス自体に紐づける形でメソッドやフィールドを定義する仕組みもあります。

メソッド定義に static を付けると、クラスに紐づくメソッドになります。

class クラス名 {
static 戻り値の型 メソッド名(引数) {
処理
}
}
MathUtil.java
public class MathUtil {
// static: インスタンスを作らずクラス名で呼び出せる
public static int square(int n) {
return n * n;
}
}

呼び出すときは クラス名.メソッド名(...) の形で書き、インスタンスを new する必要はありません。

Main.java
int result = MathUtil.square(5); // 25

Java の標準ライブラリにも、static メソッドが多数用意されています。たとえば Math.max は次のように呼び出せます。

Main.java
int max = Math.max(10, 20); // 20

MathUtil.squareMath.max のように、クラスに紐づいて呼び出すメソッドを static メソッド と呼びます。インスタンスメソッドが「インスタンスに紐づく」のに対し、static メソッドは「クラスに紐づく」点が異なります。

static はメソッドだけでなくフィールドにも付けられます。Math.PI は円周率を表すフィールドで、これも Math のインスタンスを new せずに参照できます。

Main.java
System.out.println(Math.PI); // 3.141592653589793

Math.PI のように、クラスに紐づいて持つ値を static フィールド と呼びます。

static final の組み合わせは「クラス全体で共有する書き換え不可の値」を表し、定数 と呼ばれます。たとえば GradeJudge の合格点 60 を、定数として外に出せます。

GradeJudge.java
public class GradeJudge {
// 定数(クラスで共有、書き換え不可)
public static final int PASSING_SCORE = 60;
public boolean isPassed(Student student) {
return student.getScore() >= PASSING_SCORE;
}
// rank メソッドは省略
}

定数の名前は大文字とアンダースコアで書く(PASSING_SCORE)のが Java の慣習です。