5.

インターフェースとは? Java / PHP のサンプルで学ぶ目的・抽象クラスとの違い・DIP

編集
この記事の要点
  • インターフェースはクラスが実装すべき 「契約 (メソッドの宣言だけ)」
  • 実装は強制される (実装漏れはコンパイルエラー)
  • 抽象クラスとの違い: 多重実装可 / フィールド持てない (Java 8 までは default 実装も不可)
  • メリット: 差し替え可能 (DIP, モック化, 戦略パターン) / 多態性 / API としての契約
  • リスト要素を List で受ければ犬でも猫でも同じループで処理できる (ポリモーフィズム)

インターフェースとは

クラスが「こういうメソッドを持っていますよ」と外部に約束するための仕組み。実装は持たず、メソッドのシグネチャ (名前 + 引数 + 戻り値) だけを宣言する。実装は implements 側のクラスが書く。

// インターフェース定義
public interface Drawable {
    void draw();              // 抽象メソッド (public abstract)
    int getArea();
}

// 実装クラス1
public class Circle implements Drawable {
    private int radius;
    public Circle(int r) { this.radius = r; }

    @Override
    public void draw() {
        System.out.println("○ 円を描画");
    }

    @Override
    public int getArea() {
        return (int) (Math.PI * radius * radius);
    }
}

// 実装クラス2
public class Square implements Drawable {
    private int side;
    public Square(int s) { this.side = s; }

    @Override
    public void draw() {
        System.out.println("□ 四角を描画");
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

なぜインターフェースが必要か(4 つのメリット)

1. 差し替え可能 (DIP: 依存性逆転原則)

呼び出し側は実装クラスではなくインターフェースに依存することで、後から実装を差し替えられる:

// ❌ 直接 MySQLRepository に依存 (実装に依存)
public class UserService {
    private MySQLUserRepository repo = new MySQLUserRepository();
    // → DB を Postgres に変えたら UserService も修正
}

// ✅ インターフェースに依存
public interface UserRepository {
    User findById(long id);
    void save(User u);
}

public class MySQLUserRepository implements UserRepository { ... }
public class PostgresUserRepository implements UserRepository { ... }

public class UserService {
    private final UserRepository repo;  // 抽象に依存
    public UserService(UserRepository repo) {
        this.repo = repo;   // コンストラクタ注入
    }
}

// 利用側で実装を選ぶ
UserService svc = new UserService(new MySQLUserRepository());
UserService svc = new UserService(new PostgresUserRepository());

2. テスト時にモックに差し替え

// テスト用のフェイク実装
public class FakeUserRepository implements UserRepository {
    private Map store = new HashMap<>();
    public User findById(long id) { return store.get(id); }
    public void save(User u) { store.put(u.getId(), u); }
}

// テストコード
@Test
void testRegisterUser() {
    UserRepository repo = new FakeUserRepository();
    UserService svc = new UserService(repo);

    svc.register(new User("taro"));
    assertEquals("taro", repo.findById(1L).getName());
    // DB なしでテストが回せる
}

3. ポリモーフィズム (多態性)

// 異なるクラスを同じ List で扱える
List shapes = new ArrayList<>();
shapes.add(new Circle(5));
shapes.add(new Square(3));
shapes.add(new Circle(10));

// instanceof やキャスト不要!
for (Drawable s : shapes) {
    s.draw();                       // ○ 円を描画 / □ 四角を描画
    System.out.println(s.getArea());
}

// 合計面積
int total = shapes.stream()
    .mapToInt(Drawable::getArea)
    .sum();

4. 戦略パターン (Strategy)

// 支払い方法のインターフェース
public interface PaymentStrategy {
    void pay(int amount);
}

// 各実装
public class CreditCardPayment implements PaymentStrategy {
    public void pay(int amount) { /* Stripe API 呼び出し */ }
}
public class BankTransferPayment implements PaymentStrategy {
    public void pay(int amount) { /* 銀行 API 呼び出し */ }
}
public class PayPalPayment implements PaymentStrategy {
    public void pay(int amount) { /* PayPal API 呼び出し */ }
}

// 利用側は実装を知らなくていい
public class CheckoutService {
    public void checkout(PaymentStrategy method, int total) {
        method.pay(total);  // ← 実装が何でも動く
    }
}

インターフェース vs 抽象クラス

項目interfaceabstract class
多重継承implements A, B, C× 単一継承のみ
フィールド× (定数のみ)
コンストラクタ×
実装持てるJava 8+ default メソッド
用途できること」(can-do)共通実装」(is-a)

判断基準: 共通実装を持ちたいなら抽象クラス、多種から同じ型として扱いたいならインターフェース。両者の組み合わせも有効 (テンプレートメソッドパターン)。

PHP のインターフェース

path, "[$level] $message\n", FILE_APPEND);
    }
}

class SlackLogger implements Logger {
    public function log(string $level, string $message): void {
        // Slack Webhook へ POST
    }
}

// 利用
function notify(Logger $logger, string $msg): void {
    $logger->log('ERROR', $msg);
}

notify(new FileLogger('/var/log/app.log'), '失敗');
notify(new SlackLogger(), '失敗');

Java 8+ の default メソッド

public interface Animal {
    String getName();

    // デフォルト実装を持てる
    default String greet() {
        return "I am " + getName();
    }
}

public class Dog implements Animal {
    public String getName() { return "Pochi"; }
    // greet() は実装不要
}

new Dog().greet();  // → "I am Pochi"

マーカーインターフェース

メソッドを持たない、「型のラベル」として使うインターフェース。java.io.Serializable など:

public interface Serializable {}  // メソッド無し

public class User implements Serializable {
    private String name;
}

// 利用側で型チェック
if (obj instanceof Serializable) {
    // シリアライズ可能と判断
}

FAQ

Q: メソッド 1 つしか無いならクラスでよい?
A: テストで差し替えたい / 戦略パターンで切り替えたい場合はインターフェースに。Java 8+ ならFunctionSupplier 等の関数型インターフェースで代用可。

Q: インターフェースに定数を書いていい?
A: 書けるが避けるべき (定数インターフェースアンチパターン)。定数は final class Constants や enum に。

Q: 実装クラスの方が便利ではないか?
A: コードの規模が大きくなると「修正の影響範囲が読めない」「テストが書きにくい」「実装を差し替えたい」が必ず出てきます。インターフェースは将来の自分への投資です。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. オブジェクト指向の概念
  2. 継承の概念と必要性
  3. ポリモーフィズム(多様性)の概念と必要性
  4. 抽象クラスの概念と必要性
  5. インターフェースの概念と必要性
  6. カプセル化の概念と必要性