Skip to content
Playground

6.インターフェイス

学習目標

  • インターフェイスを定義し、複数のクラスでそれを実装できる
  • インターフェイス型の変数を宣言して、複数の実装を差し替えられる
  • 具体的な実装ではなく抽象(インターフェイス)に依存する設計を書ける

1. インターフェイスとその実装

Section titled “1. インターフェイスとその実装”

intString のような基本型、自分で定義した Person のようなクラス型に加えて、Java には インターフェイス という型もあります。インターフェイスは「クラスが持つメソッドの宣言」だけを定めた型で、処理は書きません。

たとえば、円と長方形では計算方法が異なる図形でも、「面積を返すメソッドを持つ」という共通点はあります。この共通点を Shape というインターフェイスとして定義します。

Shape.java
public interface Shape {
double area();
}

area() メソッドが宣言されているだけで、処理({ ... } の中身)は書きません。Shape インターフェイスは「double を返す area() メソッドを持つ」ことだけを定義しています。処理を記述するのは、Shape インターフェイスを 実装 するクラスです。

インターフェイスは class ではなく interface キーワードで定義します。

interface インターフェイス名 {
戻り値の型 メソッド名(引数);
...
}

メソッドはセミコロン ; で終わり、中括弧 { ... } の処理本体は書きません。

クラスがインターフェイスを実装するには implements キーワードを使います。

class クラス名 implements インターフェイス名 {
// インターフェイスのメソッドの処理を書く
}

Shape を実装する Circle(円)と Rectangle(長方形)を作ります。Circle は半径から面積を計算し、Rectangle は幅と高さから面積を計算します。

Circle.java
public class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return radius * radius * Math.PI;
}
}
Rectangle.java
public class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}

implements Shape と書いたクラスは、Shape インターフェイスで宣言されたメソッドをすべて実装する必要があります。area() の実装が欠けていると、コンパイルエラーになります。

1-3. インターフェイス型での宣言

Section titled “1-3. インターフェイス型での宣言”

インターフェイスを実装したクラスのインスタンスは、インターフェイス型の変数にも代入できますCircle のインスタンスは、Circle 型の変数にも Shape 型の変数にも代入できます。

Main.java
Shape circle = new Circle(5.0);
Shape rectangle = new Rectangle(4.0, 6.0);
System.out.println(circle.area()); // 78.53981633974483
System.out.println(rectangle.area()); // 24.0

circle の型は Shape ですが、実体は Circle のインスタンスです。circle.area() を呼ぶと、実体のクラスである Circlearea() が実行され、円の面積が返ります。

Shape 型の変数から呼び出せるのは、Shape インターフェイスで宣言されたメソッドだけです。Circle 独自のメソッドが定義されていても、Shape 型の変数からは呼び出せません。

コンポジション では GradeReportServiceGradeJudge を依存に持ちました。コンストラクター注入を使うと依存を外から渡せますが、GradeJudge 型のフィールドに渡せるのは GradeJudge クラスのインスタンスだけです。判定ルールが違う別のクラスを渡せるようにするには、GradeJudge をインターフェイスとして再定義します。

サービスクラスと単一責任 で書いた GradeJudge は、isPassedrank の処理を 1 つのクラスにまとめていました。点数 60 以上で合格、80 以上で A、というルールが GradeJudge のコードに直接書かれています。

このルールを別のもの(たとえば点数 70 以上で合格、90 以上で A という厳しいルール)に変えるには、GradeJudge 自身のコードを書き換える必要があります。標準ルールと厳しいルールを使い分ける場合、両方のロジックを GradeJudge に書き込んで if で切り替える方法は、ルールが増えるたびに GradeJudge が肥大化し、変更の影響範囲が広がります。

メソッドの宣言(isPassedrank)だけを GradeJudge として残し、判定ロジックは別のクラスに分けます。GradeJudge をインターフェイスに変更し、ルールごとに実装クラスを定義します。

GradeJudge.java
public interface GradeJudge {
boolean isPassed(Student student);
String rank(Student student);
}

GradeJudge インターフェイスを実装する 2 つのクラスを定義します。StandardGradeJudge は標準ルール、StrictGradeJudge は合格点と A 評価をそれぞれ 10 点引き上げた厳しいルールです。

StandardGradeJudge.java
public class StandardGradeJudge implements GradeJudge {
@Override
public boolean isPassed(Student student) {
return student.getScore() >= 60;
}
@Override
public String rank(Student student) {
int score = student.getScore();
if (score >= 80) return "A";
if (score >= 60) return "B";
return "C";
}
}
StrictGradeJudge.java
public class StrictGradeJudge implements GradeJudge {
@Override
public boolean isPassed(Student student) {
return student.getScore() >= 70;
}
@Override
public String rank(Student student) {
int score = student.getScore();
if (score >= 90) return "A";
if (score >= 70) return "B";
return "C";
}
}

判定ロジックがルールごとに別のクラスに分かれました。新しいルールが必要になっても、新しい実装クラスを作るだけで済みます。

GradeReportService のフィールドの型は GradeJudge のまま変わりません。GradeJudge がインターフェイスになったことで、StandardGradeJudge でも StrictGradeJudge でも GradeJudge 型として受け取れます。

GradeReportService.java
import java.util.List;
public class GradeReportService {
private final ScoreCalculator calculator;
private final GradeJudge judge;
public GradeReportService(ScoreCalculator calculator, GradeJudge judge) {
this.calculator = calculator;
this.judge = judge;
}
public void report(List<Student> students) {
int average = calculator.calcAverage(students);
System.out.println("平均: " + average);
for (Student s : students) {
System.out.println(s.getName() + ": " + judge.rank(s));
}
}
}

Main.java で同じ学生リストを 2 つの GradeReportService で処理します。違いは渡す GradeJudge の実装だけです。

Main.java
import java.util.ArrayList;
import java.util.List;
List<Student> students = new ArrayList<>();
students.add(new Student("田中", 85));
students.add(new Student("鈴木", 60));
students.add(new Student("佐藤", 95));
ScoreCalculator calculator = new ScoreCalculator();
// 標準ルールでレポートを出力
GradeReportService standardService = new GradeReportService(calculator, new StandardGradeJudge());
standardService.report(students);
System.out.println("---");
// 厳しいルールでレポートを出力
GradeReportService strictService = new GradeReportService(calculator, new StrictGradeJudge());
strictService.report(students);
平均: 80
田中: A
鈴木: B
佐藤: A
---
平均: 80
田中: B
鈴木: C
佐藤: A

同じ学生リストでも、渡した GradeJudge の実装によってランクが変わります。点数 85 の田中は標準ルールで A、厳しいルールで B。点数 60 の鈴木は標準ルールで B、厳しいルールで C。点数 95 の佐藤は両方とも A です。

GradeReportService のコードは一切変えていません。Main.java で渡すインスタンスを切り替えるだけで、挙動が変わります。新しいルールが必要になったら、GradeJudge を実装する新しいクラスを作って渡すだけで済みます。

GradeReportServicejudge フィールドの型は GradeJudge インターフェイスです。具体的な実装クラス(StandardGradeJudgeStrictGradeJudge)の名前は GradeReportService の中に登場しません。

private final GradeJudge judge; // インターフェイス型

このように、具体的な実装クラスではなくインターフェイス(抽象 とも呼ばれます)を介して依存する設計を 抽象に依存する と呼びます。GradeReportService のコードは「isPassedrank を持つ型」という抽象的な情報だけを参照し、具体的な実装には依存しません。

抽象に依存することで、次のことが可能になります。

  • 判定ルールを増やすときは、GradeJudge を実装する新しいクラスを追加するだけで済み、GradeReportService の変更は不要
  • テスト時には固定の値を返す GradeJudge の実装を渡すことで、GradeReportService の動作を意図した条件で検証できる
  • 実装側は GradeJudge インターフェイスで宣言されたメソッドを満たすことに集中できる