9.65: リフレクションよりもインターフェースを選ぶ
結論
リフレクションは強力だが最後の手段にするべき。
可能ならインターフェース(または関数型インターフェース/抽象化)を使って型安全に振る舞いを差し替える。
インターフェースはコンパイル時チェック・可読性・テスト容易性・セキュリティ・保守性を与える一方、リフレクションは実行時エラー・性能低下・モジュール境界の破壊・脆弱で理解しづらいコードを招きやすい。
良い例
依存性注入/ポリモーフィズムで振る舞いを切り替える典型パターン。
// インターフェース
public interface PaymentProcessor {
Receipt process(PaymentRequest req);
}
// 実装A(本番)
public class StripePaymentProcessor implements PaymentProcessor {
public Receipt process(PaymentRequest req) {
// Stripe API を呼ぶ実装
}
}
// 実装B(テスト用スタブ)
public class StubPaymentProcessor implements PaymentProcessor {
public Receipt process(PaymentRequest req) {
return new Receipt("stub", true);
}
}
// 利用側(UseCase / Service)
public class CheckoutService {
private final PaymentProcessor processor;
public CheckoutService(PaymentProcessor processor) {
this.processor = processor;
}
public Receipt checkout(PaymentRequest req) {
return processor.process(req);
}
}
// 起動時に差し替え(Composition Root)
PaymentProcessor p = new StripePaymentProcessor(); // 本番
// PaymentProcessor p = new StubPaymentProcessor(); // テスト
CheckoutService svc = new CheckoutService(p);
利点:
コンパイル時にメソッド存在が保証され、IDE補完・リファクタが効き、簡単にスタブ差し替えできる。
悪い例
メソッド名やシグネチャを文字列で扱い、実行時に呼び出すパターン。
// 呼び出し側(リフレクション利用)
Object impl = Class.forName("com.example.StripePaymentProcessor").getConstructor().newInstance();
Method m = impl.getClass().getMethod("process", PaymentRequest.class);
Receipt r = (Receipt) m.invoke(impl, req);
問題点:
-
processの名前やシグネチャを文字列で保持 → リファクタ時に壊れる -
例外が増える(ClassNotFoundException, NoSuchMethodException, InvocationTargetException…)
-
メソッドが存在してもアクセス制御に引っかかる場合がある(モジュールシステム)
-
実行時オーバーヘッド(性能低下)
-
テストやモックが難しい
いつリフレクションを許容するか
- フレームワークやライブラリ(DI・シリアライズ・ORM・テストフレームワーク等)がユーザーのコードを自動的に扱う場合は 仕方なく使うことがある
ただしその責務はフレームワーク層に閉じるべき。
- ランタイムプラグインロードや動的モジュール検出が真に必要な場合のみ
- 代替手段(
ServiceLoader、DIコンテナ、ファクトリ、Strategy/Commandパターン、注入されたSupplier/Function)が使えないときのみ
- Java 9+ のモジュールシステムを使っている場合、リフレクションでアクセスするには
--add-exports等の特別な設定が必要になり、運用負担が増す
代替テクニック(リフレクションを使わず安全に実現する方法)
-
インターフェース + DI(コンストラクタ注入 / ServiceProvider)
-
ServiceLoader(プラグイン検出) -
ファクトリ / Provider / Supplier を注入
-
関数型インターフェース(
Function/Supplier)を引数で受ける -
コンパイル時注釈処理(Annotation Processor)でコード生成(リフレクションのコストと脆弱性を排除)
チェックリスト
- API を設計するときはまず インターフェース を作る
- 動的な振る舞い変更が必要か再検討:不要ならリフレクションを使わない
- フレームワークコードでリフレクションを使うなら、その箇所を明確に分離しテストする
- リフレクション使用時は例外処理・型チェックを丁寧に行い、ドキュメントに理由を書く
- Java モジュールを使っている場合、リフレクションでアクセスする API はエクスポートポリシーに注意
- 性能が重要なら事前にベンチ(Profiler/JMH)を取り、MethodHandle 等の高速手段を検討する