9.64: インターフェースでオブジェクトを参照する
結論
可能な限り 型(参照)には具象クラスではなくインターフェースを使え。
こうすることで実装からの分離(疎結合)、差し替えやテストのしやすさ、API の安定性・汎用性が得られる。ただし「必要な機能(順序保証やランダムアクセスなど)」を要求するなら、適切に具体度の高いインターフェース(List vs Collection)や具象型を選ぶこと。
良い例
引数・フィールド・戻り値にインターフェースを使っている例。
実装は簡単に差し替え可能。
import java.util.*;
public class UserService {
// フィールドはインターフェースで参照
private final Map<String, User> users;
public UserService(Map<String, User> users) {
// 呼び出し側で HashMap, ConcurrentHashMap, LinkedHashMap 等を渡せる
this.users = users;
}
// API もインターフェースで宣言(利用者に実装詳細を押し付けない)
public Collection<User> getAllUsers() {
return Collections.unmodifiableCollection(users.values());
}
// 内部処理でもインターフェースを受ける
public void addUsers(Collection<User> newUsers) {
for (User u : newUsers) {
users.put(u.getId(), u);
}
}
}
呼び出し側:
UserService svc = new UserService(new ConcurrentHashMap<>()); // スレッドセーフ実装に差し替え
利点:
-
UserServiceのソースを変えずに、実装(HashMap→ConcurrentHashMap)を差し替えられる - テストでは
new HashMap<>()や Mockito のモックで容易に置き換えられる
悪い例
具象クラスで参照してしまい、差し替えやテストが困難になる例。
public class BadUserService {
private final HashMap<String, User> users; // NG: 具象型で固定
public BadUserService() {
this.users = new HashMap<>();
}
public ArrayList<User> getAllUsers() { // NG: 具象を返す
return new ArrayList<>(users.values());
}
}
問題点:
-
BadUserServiceをConcurrentHashMapに差し替えたくてもコードを書き換える必要がある - 戻り値が
ArrayListだと、利用者がtrimToSize()等の具象メソッドに依存してしまう可能性がある(API の脆弱性) - 単体テストでモックやスタブと差し替えにくい
補足・注意点(いつインターフェースではなく具体型を使うか)
-
必要な特性を要求するとき:
順序(List)やキー順序(SortedMap)、ランダムアクセス性能(RandomAccessマーカー)などが必要なら、適切に具体度の高いインターフェース・型を使う。
例:
void process(List<T> items)は順序を前提とするAPIの正しい選択。
-
ローカル変数:
ローカル変数では実装のメソッドを直接使うことが多く、具象型を使っても問題ない。
だが API(フィールド・引数・戻り値)は原則インターフェースで。 -
特殊機能が必要なとき:
ensureCapacityやtrimToSizeのように具象クラス特有の操作が必要なら具象クラスを採用するか、その操作を抽象化した別インターフェースを作る。 -
過度な抽象化は逆効果:
あまりにも一般的すぎるインターフェース(Objectや極端に抽象的な自作インターフェース)では型情報が失われ、使い勝手が落ちる。適切な粒度で。
チェックリスト(ルール)
-
パラメータ・フィールド・戻り値はまずインターフェースで宣言する(例:
List,Map,Collection,Stream,Function)
-
実装の差し替えが考えられる場所はインターフェースで(並行処理、永続化、キャッシュ等)
-
戻り値はできるだけ抽象にして副作用を避ける(
Collections.unmodifiableList(...))
-
API ドキュメントで必要な性質(順序や重複の許容)を明示する(ただ型だけ返しても意味が伝わらないことがある)
-
テストしやすさを優先: インターフェースにしておけば Mockito 等でモック可能。
- ただし、必要な具体機能がある場合は具象を選ぶ(性能チューニングや特別操作が必要なとき)。
主要インターフェース
-
List/Set/Map -
Predicate/Function/Consumer/Supplier -
Runnable/Callable StreamComparatorIterable
まとめ
具象に依存すると変更で苦労する。
インターフェースに依存すると差し替え・拡張・テストが楽になる。
ただし『必要な機能』に応じて適切な抽象度を選ぶ。