5.31:APIの柔軟性向上
要点
公開 API は具象実装や不必要な制約を露出せず、できるだけ抽象(インターフェース・ジェネリクス・ワイルドカード)で表現して、将来の実装変更や利用法の拡張を妨げないように設計する。
なぜ重要か
- 利用者が多様な型を渡せる・受け取れるようにすると API の再利用性が上がる。
- 実装を変えても API を壊さずに内部改善(効率化・バグ修正)できる。
- 過度に具体的な型を要求すると呼び出し側のコードが不便になり、将来の互換性コストが増える。
具体的設計ルールと理由
1. 受け取る型は「最も抽象的な意味のあるインターフェース」で受ける
// NG: 実装の詳細を強制している
public void process(ArrayList<String> list) { ... }
// OK: 呼び出し側は List の任意の実装を渡せる
public void process(List<String> list) { ... }
理由:List にしておくと ArrayList LinkedList ImmutableList など何でも渡せる。
2. 返す型もインターフェースで返し、内部表現を露出しない
// NG: 内部配列や内部可変リストを直接返すと状態が壊される
public String[] getNames() { return names; }
// OK: インターフェースで返し、必要なら不変コピーを返す
public List<String> getNames() { return List.copyOf(namesList); }
理由:内部実装を変えられる、安全にイミュータブルなビューを提供できる。
3. ジェネリクス&ワイルドカードで型の互換性を高める(PECS等)
// 汎用的で安全(Producer-extends, Consumer-super を活用)
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }
理由:具体型に縛られず、幅広い呼び出しを許す。
4) 引数の数が多い/オプションが多い場合は Builder やファクトリを用意
悪い:長いコンストラクタ/多数のオーバーロード
良い:Builder パターン、static factory(引数名で選べる)で読みやすく将来の拡張に強くなる。
5. 配列よりコレクションを使う(特に公開 API)
公開 API は List を返す/受け取る方が安全。配列は共変性や防御的コピーの問題がある。
6. Optional を使って「値がない」ことを明示する(null を返さない)
Optional を返すことで、呼び出し側に欠如処理を強制できる(null を返す API は拡張しにくい)。
// OK: シグネチャで「ないかもしれない」を明示
public Optional<User> findUser(String id) { ... }
// 呼び出し側の例
Optional<User> userOpt = repo.findUser("alice");
userOpt.ifPresent(user -> sendWelcome(user));
User u = userOpt.orElse(defaultUser);
User u2 = userOpt.orElseThrow(() -> new NoSuchElementException("not found"));
7. 便利オーバーロードを用意する(だが乱発しない)
たとえば addAll(Collection< T >) と addAll(T... items) の二つを用意すると使い勝手は良くなるが、varargs × ジェネリクスの安全性には注意。
悪い例:
public class Bag<T> {
private final List<T> items = new ArrayList<>();
// 危険: ジェネリック varargs を直接内部で配列ベースのビューに渡したり露出させると危険
public void addAll(T... items) {
// Arrays.asList(items) や直接配列を何かに保持すると配列が露出する可能性がある
Collections.addAll(this.items, items); // 一見OKでも呼び出し側が配列を渡していると危険性がある
}
public void addAll(Collection<T> coll) {
items.addAll(coll);
}
}
良い例:
public class Bag<T> {
private final List<T> items = new ArrayList<>();
public void addAll(Collection<? extends T> coll) {
items.addAll(coll);
}
// 使い勝手をよくする varargs オーバーロード(安全実装)
@SafeVarargs
public final void addAll(T... elems) {
// elems の配列自体を外に渡さない(要素を新しい ArrayList にコピーしてから使う)
for (T e : elems) items.add(e);
}
}
8. パラメータに具象例外や実装依存型を使わない
実装固有の例外型や内部例外を API(公開メソッドのシグネチャ)に露出させると利用者が依存してしまう。可能なら標準の例外や専用の高レベル例外を使う。
悪い例:
// Repository が JDBC を内部で使っていると仮定
public class UserRepository {
// NG: SQLException をそのまま宣言している(実装依存を公開)
public User findById(String id) throws SQLException {
// ... JDBC code ...
}
}
良い例:
// API 層でラップしたり、より高レベルな例外を投げる
public class DataAccessException extends RuntimeException {
public DataAccessException(String msg, Throwable cause) { super(msg, cause); }
}
public class UserRepository {
public User findById(String id) {
try {
// ... JDBC code ...
} catch (SQLException ex) {
// 実装依存の例外を API 用の例外でラップして投げる
throw new DataAccessException("Failed to find user " + id, ex);
}
}
}
まとめ
API の柔軟性を上げる基本は「抽象化して公開すること」。具体的にはインターフェースやジェネリクスで表す、内部表現を露出しない(不変ビュー/防御的コピー)、Builder/Factory を用意する、といった実務テクニックを使うことで、利用者にとって使いやすく、実装者にとって将来の変更に強い API がつくれる。