4-22:型を定義するためだけにインターフェースを使う
要点
インターフェースは「何ができるか(振る舞い/型)」を表すために使い、API の柔軟性と将来の拡張性を高める。実装の細部や単なる定数集のためにインターフェースを使うのは誤用。
なぜインターフェースを型定義に使うべきか
- 実装から契約を分離できる → 実装を差し替えやすく、複数のクラスが同じ「役割」を持てる。
- 多重実装が可能(implements を複数使える)なので、柔軟な設計ができる。
- API の利用者は具象型に依存しないで済み、将来の変更に強い。
- Java の標準ライブラリもこの方針(List, Set, Map など)で設計されている。
悪い例
1) 定数だけを置く「定数インターフェース」
// NG: Constant interface pattern
public interface Constants {
int MAX = 1024;
String DEFAULT_NAME = "foo";
}
問題点:
- implements Constants すると実装クラスの公開 API に定数が入り込み、実装の意味を変えてしまう。
- 定数を使いたいだけなら static import か public final class Constants { public static final ... } を使うべき。
2) インターフェースを実装のためだけに設計し、型として使わない
public interface ImplA { void a(); } // でも API は ImplA を使わず具体クラスを返す
public class ImplAImpl implements ImplA { ... }
public ImplAImpl getImpl() { return new ImplAImpl(); } // API が具体型を返す(柔軟性低い)
問題点:
- API が具象クラスに結びつくため実装差し替えやテストがしにくくなる。
良い例
インターフェースを型として使う
public interface ItemStore {
int size();
Item get(int i);
void add(Item item);
}
// 実装は自由に差し替えられる
public class ArrayItemStore implements ItemStore { ... }
public class DbItemStore implements ItemStore { ... }
// API はインターフェース型で返す/受け取る
public ItemStore createStore() { return new ArrayItemStore(); }
public void process(ItemStore store) { /* store の実装に依存しない */ }
利点:
- process は ItemStore の振る舞いだけに依存する。
- 実装を変更しても process 側は変えなくて良い。
補助パターン:骨格実装(Skeletal implementation)
- インターフェースだけで契約を定義し、共通の(複雑な)実装を再利用したければ抽象クラスで骨格(AbstractList のような)を提供する。これが標準ライブラリでも使われているパターン。
- 例:List(インターフェース)+AbstractList(骨格実装)+ArrayList(具象)。
マーカーインターフェースについて
- Serializable や Cloneable のような「マーカー(タグ)」としてのインターフェースは「型」としての意味を持つ(ある種の契約を表す)ため妥当な場合がある。
- ただし、振る舞いを表さない単なる定数置き場としてのインターフェースは避ける。
まとめ
- インターフェースは「型(契約)」を定義するために使うのが本筋。
- 実装共有や状態保持が必要なら抽象クラスを用意するが、まずはインターフェースを使って API を柔軟・拡張可能に設計するのが良い実務習慣です。
- 定数だけのインターフェースは避け、代わりに定数クラスを使う。