0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【リファクタリング】Extract Interface(インターフェースの抽出)

Posted at

1. 概要(Overview)

Extract Interface は、複数のクラスに見られる 共通のふるまい(メソッド群)
インターフェースとして切り出し、クライアントをそのインターフェースに依存させるリファクタリングです。

継承(スーパークラス抽出)と違い、状態(フィールド)の共有は行わず、契約(API)だけ を共通化します。
実装を差し替えやすくなり、モジュール間の結合を弱められます。

目的:

  • 依存関係の逆転(DIP)/抽象化による疎結合化
  • テスト容易性の向上(モック・スタブ差し替えが容易)
  • 実装の入れ替え・並存を可能にする(戦略の切替)

2. 適用シーン(When to Use)

  • 複数クラスが 同じメソッドセット を(名前・概念的に)持っている
  • クライアントが 具体クラス に依存しており、差し替えが難しい
  • 将来、別実装(DB→API、同期→非同期等)を導入する見込みがある
  • 単体テストで 代替実装に置き換えたい(モックしたい)

よくある匂い:

  • Duplicate Code(重複)(ふるまいの重複だが継承は不自然)
  • Inappropriate Intimacy(不適切な親密さ)(具体に強く依存)
  • Shotgun Surgery(差し替え時の影響範囲が広い)

3. 手順(Mechanics / Steps)

  1. 共有したいメソッド群を抽出(引数・戻り値を可能な限り共通化)
  2. インターフェース を定義
  3. 対象クラスにそのインターフェースを implements(Kotlin: :)させる
  4. クライアントの依存先を 具体型 → インターフェース型 に置換
  5. コンパイル&テスト(モック置換・差し替えも確認)

4. Kotlin 例(Before → After)

Before:クライアントが具体クラスに依存

class LocalUserRepository {
    fun getUser(id: String): User? { /* ... */ return null }
    fun save(user: User) { /* ... */ }
}

class RemoteUserRepository {
    fun getUser(id: String): User? { /* ... */ return null }
    fun save(user: User) { /* ... */ }
}

class UserService(
    // 具体クラスに依存してしまっている
    private val repo: LocalUserRepository
) {
    fun load(id: String): User? = repo.getUser(id)
}
  • LocalRemote同名ふるまい を持っているのに、共通の契約がない
  • UserServiceLocalUserRepository に固定依存で差し替え不能

After:インターフェースを抽出して依存逆転

interface UserRepository {
    fun getUser(id: String): User?
    fun save(user: User)
}

class LocalUserRepository : UserRepository {
    override fun getUser(id: String): User? { /* ... */ return null }
    override fun save(user: User) { /* ... */ }
}

class RemoteUserRepository : UserRepository {
    override fun getUser(id: String): User? { /* ... */ return null }
    override fun save(user: User) { /* ... */ }
}

class UserService(
    // 抽象に依存(DIP)
    private val repo: UserRepository
) {
    fun load(id: String): User? = repo.getUser(id)
}
  • UserRepository を抽出し、Local/Remote が実装
  • UserService は抽象に依存するので、実装の差し替え・モック化が容易

5. 効果(Benefits)

  • 疎結合化:クライアントが具象から独立
  • テスト容易性:インターフェースをモック・フェイクで差し替え
  • 拡張性:新しい実装(例えばキャッシュつき、レート制限つき等)を無停止で追加可能
  • 並存運用:段階移行(Local→Remote)や A/B 切替がやりやすい

6. 注意点(Pitfalls)

  • インターフェースの 過剰細分化(I/F が増えすぎると可読性低下)
  • 契約の早すぎる固定化:検証前に固めると将来の変更に硬直化
  • 状態を共有したい場合は Extract Superclass のほうが自然(I/F は振るまい契約のみ)
  • デフォルト実装を多用するなら、Strategy/委譲 も検討(I/F+実装注入)

7. 実務Tips

  • 最小限のメソッド から始める(YAGNI)
  • I/F 名は 役割名 に(Repository, Encoder, Clock など)
  • 戻り値・例外契約 を明確にし、呼び出し側の責務(リトライ/リカバリ)を設計
  • Kotlin では I/F に デフォルト実装 を置けるが、複雑ロジックは避ける(肥大化の元)
  • DI コンテナ(Koin/Hilt)と相性良し:抽象にバインド して注入

8. もう一例:決済戦略の切替(Strategy と好相性)

interface PaymentGateway {
    fun pay(amount: Money): PaymentResult
}

class StripeGateway : PaymentGateway {
    override fun pay(amount: Money): PaymentResult { /* ... */ }
}

class PayPalGateway : PaymentGateway {
    override fun pay(amount: Money): PaymentResult { /* ... */ }
}

class CheckoutService(private val gateway: PaymentGateway) {
    fun checkout(amount: Money): PaymentResult = gateway.pay(amount)
}
  • インターフェース抽出で ランタイム差し替え(戦略切替)が容易
  • テストでは FakeGateway を注入して 決済外部依存を排除

まとめ

  • Extract Interface は、共通ふるまいを契約として切り出し、
    具体への依存を抽象へ置き換える リファクタリング
  • 判断基準:共通メソッドがあり、差し替え・テスト・拡張の必要があるか?
  • 基本思想:DIP/ISP を満たし、疎結合・可テスト・拡張容易な設計へ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?