markdown
はじめに (対象読者・この記事でわかること)
この記事は、Javaの基礎文法は理解しているが、インタフェースの概念と実装方法に戸惑っている方、または「インタフェースを使った方がいい」とは聞くものの、いつ・どう使えばよいか判断できない方を対象としています。
この記事を読むことで、Javaのインタフェースが解決しようとしている課題、具体的な実装方法、実務で使える設計パターンまで一気に理解できます。最後には簡単なECサイト風の決済処理を例に、インタフェースを使った保守性の高いコードの書き方を身につけられます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Javaのクラス・メソッドの基本的な書き方
- 継承(extends)の概念
- 静的型付けの概念(変数には「型」があること)
なぜインタフェースが必要なのか:多重継承の欠点を補う
Javaはクラスによる多重継承を許可していません。理由はシンプルで、複数の親クラスが同じメソッドを持っていた際に、どちらを優先すべきかが曖昧になるためです。しかし現実の開発では「複数の振る舞いを組み合わせたい」場面が頻繁に起こります。そこで登場するのが「インタフェース」です。インタフェースは「メソッドの型(シグネチャ)のみを定義した契約書」のようなもので、クラスがこの契約書を実装(implements)することで、多重継承のような柔軟性を安全に実現します。
ステップバイステップで学ぶインタフェースの実装と活用法
ここでは、実際のECサイトの決済処理を例に、インタフェースを使った設計を学びます。従来の「if分岐地獄」を脱却し、新しい決済方法が追加されても修正が最小限で済む構成を目指します。
ステップ1:インタフェースを定義する
まず、決済処理のインタフェースPaymentを定義します。
Java// Payment.java public interface Payment { /** * 決済実行 * @param amount 決済金額(円) * @return 成功時はtrue */ boolean pay(int amount); /** * 与信確保(クレジットカードなどで必須) * @param amount 決済予定金額 * @return 与信ID(確保できない場合はnull) */ default String authorize(int amount) { // デフォルト実装:必要ない決済方法はオーバーライドしなくてよい return null; } }
ポイントは3つです。
1. メソッドに実装を書かない(defaultやstaticを除く)
2. すべてのメソッドは暗黙のうちにpublic abstract
3. 定数(public static final)以外のフィールドは持てない
ステップ2:インタフェースを実装したクラスを作る
次に、具体的な決済方法であるCreditCardPaymentとBankTransferPaymentを実装します。
Java// CreditCardPayment.java public class CreditCardPayment implements Payment { private final String cardNumber; private final String expiry; public CreditCardPayment(String cardNumber, String expiry) { this.cardNumber = cardNumber; this.expiry = expiry; } @Override public boolean pay(int amount) { String authId = authorize(amount); if (authId == null) return false; // 実際のカード決済APIをコール System.out.println("カード決済 " + amount + "円 与信ID=" + authId); return true; } @Override public String authorize(int amount) { // 与信サーバへリクエスト(仮実装) return "auth_" + System.nanoTime(); } } // BankTransferPayment.java public class BankTransferPayment implements Payment { private final String bankCode; private final String accountNumber; public BankTransferPayment(String bankCode, String accountNumber) { this.bankCode = bankCode; this.accountNumber = accountNumber; } @Override public boolean pay(int amount) { // 銀行振込は与信不要 System.out.println("振込依頼 " + amount + "円 口座=" + accountNumber); return true; } }
ステップ3:ポリモーフィズムを活かした呼び出し側を実装
Paymentを受け取る処理は、具体的なクラスを意識せずに決済を実行できます。
Java// CheckoutService.java public class CheckoutService { public Receipt checkout(Cart cart, Payment payment) { int total = cart.getTotal(); if (!payment.pay(total)) { throw new PaymentException("決済に失敗しました"); } return new Receipt(total); } } // Main.java public class Main { public static void main(String[] args) { CheckoutService service = new CheckoutService(); Cart cart = new Cart(); cart.add(new Item("Java本", 3000)); // クレカ決済 Payment credit = new CreditCardPayment("1234567890123456", "12/25"); service.checkout(cart, credit); // 銀行振込 Payment bank = new BankTransferPayment("1234", "1234567"); service.checkout(cart, bank); } }
ハマった点:デフォルトメソッドの衝突
Java 8以降、インタフェースにもdefault実装を持たせられますが、複数のインタフェースを同時に実装した際に、同じシグネチャのデフォルトメソッドが衝突することがあります。
Javainterface A { default void hello() { System.out.println("A"); } } interface B { default void hello() { System.out.println("B"); } } class C implements A, B { // コンパイルエラー:継承されたメソッドhello()に対するデフォルト実装が重複している }
解決策:衝突時はオーバーライドが必須
衝突した場合、クラス側でオーバーライドしてあげることで解決します。どちらのデフォルト実装を使いたいかを明示すればOKです。
Javaclass C implements A, B { @Override public void hello() { A.super.hello(); // Aのデフォルト実装を呼び出す // または独自実装 System.out.println("C"); } }
発展的なテクニック:Strategyパターンと組み合わせる
インタフェースは「振る舞い」を切り替えるStrategyパターンと相性が抜群です。例えば、決済手数料を外だしにして、実行時にルールを切り替えられます。
Java@FunctionalInterface interface FeeRule { int calc(int amount); } class CheckoutService { private final FeeRule feeRule; public CheckoutService(FeeRule feeRule) { this.feeRule = feeRule; } public Receipt checkout(Cart cart, Payment payment) { int subtotal = cart.getTotal(); int fee = feeRule.calc(subtotal); int total = subtotal + fee; payment.pay(total); return new Receipt(total, fee); } } // 呼び出し側 FeeRule free = amount -> 0; // 無料 FeeRule flat = amount -> 300; // 定額 FeeRule percent = amount -> amount / 10; // 10% CheckoutService service = new CheckoutService(flat);
まとめ
本記事では、Javaのインタフェースが解決しようとしている「多重継承の問題」から、実装方法、そして実務で使える設計パターンまでを一気に学びました。
- インタフェースは「契約書」であり、クラスがその契約を実装する
defaultメソッドで後方互換性を保ちつつ、多重継承的な柔軟性を実現- ポリモーフィズムにより、呼び出し側は具象クラスを意識せずに処理を実行
この記事を通して、if文で決済方法を分岐する代わりに、インタフェースを使った設計にすることで、新しい決済方法が追加されても既存コードを修正せずに済む「開放閉鎖原則」に則ったコードが書けるようになりました。次回は、もう一歩進んで「DI(依存性注入)」を使ったテスタビリティの高い設計について掘り下げていきます。
参考資料
