6.インターフェイス
学習目標
- インターフェイスを定義し、複数のクラスでそれを実装できる
- インターフェイス型の変数を宣言して、複数の実装を差し替えられる
- 具体的な実装ではなく抽象(インターフェイス)に依存する設計を書ける
1. インターフェイスとその実装
Section titled “1. インターフェイスとその実装”int や String のような基本型、自分で定義した Person のようなクラス型に加えて、Java には インターフェイス という型もあります。インターフェイスは「クラスが持つメソッドの宣言」だけを定めた型で、処理は書きません。
たとえば、円と長方形では計算方法が異なる図形でも、「面積を返すメソッドを持つ」という共通点はあります。この共通点を Shape というインターフェイスとして定義します。
public interface Shape { double area();}area() メソッドが宣言されているだけで、処理({ ... } の中身)は書きません。Shape インターフェイスは「double を返す area() メソッドを持つ」ことだけを定義しています。処理を記述するのは、Shape インターフェイスを 実装 するクラスです。
1-1. interface キーワード
Section titled “1-1. interface キーワード”インターフェイスは class ではなく interface キーワードで定義します。
interface インターフェイス名 { 戻り値の型 メソッド名(引数); ...}メソッドはセミコロン ; で終わり、中括弧 { ... } の処理本体は書きません。
1-2. implements キーワード
Section titled “1-2. implements キーワード”クラスがインターフェイスを実装するには implements キーワードを使います。
class クラス名 implements インターフェイス名 { // インターフェイスのメソッドの処理を書く}Shape を実装する Circle(円)と Rectangle(長方形)を作ります。Circle は半径から面積を計算し、Rectangle は幅と高さから面積を計算します。
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; }}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 型の変数にも代入できます。
Shape circle = new Circle(5.0);Shape rectangle = new Rectangle(4.0, 6.0);
System.out.println(circle.area()); // 78.53981633974483System.out.println(rectangle.area()); // 24.0circle の型は Shape ですが、実体は Circle のインスタンスです。circle.area() を呼ぶと、実体のクラスである Circle の area() が実行され、円の面積が返ります。
Shape 型の変数から呼び出せるのは、Shape インターフェイスで宣言されたメソッドだけです。Circle 独自のメソッドが定義されていても、Shape 型の変数からは呼び出せません。
2. 実装の差し替え
Section titled “2. 実装の差し替え”コンポジション では GradeReportService が GradeJudge を依存に持ちました。コンストラクター注入を使うと依存を外から渡せますが、GradeJudge 型のフィールドに渡せるのは GradeJudge クラスのインスタンスだけです。判定ルールが違う別のクラスを渡せるようにするには、GradeJudge をインターフェイスとして再定義します。
2-1. 判定ロジックの分離
Section titled “2-1. 判定ロジックの分離”サービスクラスと単一責任 で書いた GradeJudge は、isPassed と rank の処理を 1 つのクラスにまとめていました。点数 60 以上で合格、80 以上で A、というルールが GradeJudge のコードに直接書かれています。
このルールを別のもの(たとえば点数 70 以上で合格、90 以上で A という厳しいルール)に変えるには、GradeJudge 自身のコードを書き換える必要があります。標準ルールと厳しいルールを使い分ける場合、両方のロジックを GradeJudge に書き込んで if で切り替える方法は、ルールが増えるたびに GradeJudge が肥大化し、変更の影響範囲が広がります。
メソッドの宣言(isPassed と rank)だけを GradeJudge として残し、判定ロジックは別のクラスに分けます。GradeJudge をインターフェイスに変更し、ルールごとに実装クラスを定義します。
public interface GradeJudge { boolean isPassed(Student student); String rank(Student student);}GradeJudge インターフェイスを実装する 2 つのクラスを定義します。StandardGradeJudge は標準ルール、StrictGradeJudge は合格点と A 評価をそれぞれ 10 点引き上げた厳しいルールです。
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"; }}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"; }}判定ロジックがルールごとに別のクラスに分かれました。新しいルールが必要になっても、新しい実装クラスを作るだけで済みます。
2-2. 実装の切り替え
Section titled “2-2. 実装の切り替え”GradeReportService のフィールドの型は GradeJudge のまま変わりません。GradeJudge がインターフェイスになったことで、StandardGradeJudge でも StrictGradeJudge でも GradeJudge 型として受け取れます。
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 の実装だけです。
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 を実装する新しいクラスを作って渡すだけで済みます。
3. 抽象への依存
Section titled “3. 抽象への依存”GradeReportService の judge フィールドの型は GradeJudge インターフェイスです。具体的な実装クラス(StandardGradeJudge や StrictGradeJudge)の名前は GradeReportService の中に登場しません。
private final GradeJudge judge; // インターフェイス型このように、具体的な実装クラスではなくインターフェイス(抽象 とも呼ばれます)を介して依存する設計を 抽象に依存する と呼びます。GradeReportService のコードは「isPassed と rank を持つ型」という抽象的な情報だけを参照し、具体的な実装には依存しません。
抽象に依存することで、次のことが可能になります。
- 判定ルールを増やすときは、
GradeJudgeを実装する新しいクラスを追加するだけで済み、GradeReportServiceの変更は不要 - テスト時には固定の値を返す
GradeJudgeの実装を渡すことで、GradeReportServiceの動作を意図した条件で検証できる - 実装側は
GradeJudgeインターフェイスで宣言されたメソッドを満たすことに集中できる