ソフトウェア設計

SwiftのAdapterパターン:レガシーAPIをプロトコルでラップする方法

Adapterパターンは、レガシーAPIのインターフェースを新しいコードが期待するプロトコルへ変換します。Swiftで変換の境界を作り、置き換えとテストを容易にする実装手順や、他のラッピングパターンとの違いを整理します。

読了 5 分
SwiftのAdapterパターン:レガシーAPIをプロトコルでラップする方法のカバー画像

削除もできず、そのまま使い続けるのもつらいレガシーAPIコードに困ったことはありませんか?

古いネットワークモジュールを新しい画面に接続するとき、誰もがぶつかる壁です。

結論から言うと、レガシーAPIをプロトコル(interface)でラップし、その間にアダプターを置くのが最もすっきりした解決策です。

Adapterパターンは、互換性のない2つのインターフェースの間に「変換器」を挟む方法です。

似たようにオブジェクトをラップするFacade・Proxy・Decoratorと混同するなら、ラッピングパターン4種類の比較でまず目的の違いを整理できます。

今日はSwiftでこれをどう適用するか、私が実際に経験した流れに沿って整理します。


この記事で得られる3つのこと

忙しい方向けに、先に要点をまとめます。

  1. 新しいコードが求める形を、まずプロトコルとして定義する
  2. レガシーAPIをそのプロトコルに合わせて変換するアダプター型を作る
  3. 画面とビューモデルはレガシーAPIではなく、プロトコルだけに依存させる

この3点を守るだけで、後からAPI全体を置き換えるときの修正範囲を大幅に減らせます。

この構造に変えてから、テストコードを書きやすくなりました。


SwiftでAdapterパターンが必要な理由

レガシーAPIは、たいてい私たちが望む形になっていません。

コールバックベースだったり、パラメーターが複雑だったり、戻り値の型が曖昧だったりします。

新しいコードが古いAPIの事情に合わせて曲がると、そのAPIがなくなった日にコード全体が揺らぎます。

そこで、間にアダプターを置きます。

たとえば、以前のモジュールが次のような形だとします。

// 手を入れにくいレガシーコード API (コールバックベース)
class LegacyUserAPI {
    func fetch(id: Int,
               done: @escaping (NSDictionary?) -> Void) {
        // 古いネットワーク呼び出し...
    }
}

NSDictionaryをそのまま画面まで持ち込むと、後でこのAPIを変更するときに画面コードまで全面的に直すことになります。

見るからに直接つなぎたくない形ですよね。


レガシーAPIをプロトコルでラップする方法(3ステップ)

実際のラッピング手順は、思ったより単純です。

ステップ1:望む形をプロトコルとして定義します。

新しいコードが「こう呼べたらいい」と思う形を、先に書き出します。

// 新しいコードが求めるすっきりしたインターフェース
protocol UserRepository {
    func user(id: Int) async throws -> User
}

コールバックの代わりにasync/awaitNSDictionaryの代わりにUser型へ変更しました。

ステップ2:アダプターでレガシーAPIをこのプロトコルに合わせます。

複雑な変換はすべてアダプターの中に閉じ込めます。

// レガシーAPIを新しいプロトコルに変換するアダプター
struct LegacyUserAdapter: UserRepository {
    let legacy = LegacyUserAPI()
    func user(id: Int) async throws -> User {
        try await withCheckedThrowingContinuation { cont in
            legacy.fetch(id: id) { dict in
                cont.resume(returning: User(dict))
            }
        }
    }
}

コールバックをasyncに変えるwithCheckedThrowingContinuationが、ここで重要な役割を果たします。

ステップ3:画面はプロトコルだけに依存させます。

ビューモデルはLegacyUserAPIを知る必要がありません。UserRepositoryだけ知っていれば十分です。

こうすると、レガシーコードが登場する場所をアダプター1か所だけにできます。

レガシーAPIはアダプターの後ろに隠れ、画面はプロトコルだけを見ます
レガシーAPIはアダプターの後ろに隠れ、画面はプロトコルだけを見ます
画面コードは、このすっきりしたプロトコルだけ知っていれば十分です
画面コードは、このすっきりしたプロトコルだけ知っていれば十分です

アダプターを使うと何が変わる?(直接呼び出しとの比較)

直接呼び出しとアダプター方式を表で比較しました。

項目 レガシーAPIを直接呼び出す アダプターでラップする
API交換時の修正範囲 画面全体 アダプター1か所
ユニットテスト 難しい モックで簡単
新しいコードの可読性 低い 高い
初期作業量 少ない 少し増える

初期作業量が少し増えるのは事実です。

しかし、私はこのコストをまったく惜しいとは思いませんでした。

テスト時にUserRepositoryを実装した偽オブジェクトを1つ渡すだけで、ネットワークなしに画面ロジックを検証できます。

実際のAPIがまだ完成していない状況でも、先に開発を始められました。

この3つの区画構造だけ覚えれば、半分は終わったも同然です
この3つの区画構造だけ覚えれば、半分は終わったも同然です

よくある質問(Q&A)

Q. アダプターはstructとclassのどちらで作るべきですか?

状態を持たないなら、structで十分です。

レガシーオブジェクトを保持し続ける必要がある、または参照共有が必要なら、classを使います。

Q. AdapterパターンとFacadeパターンはどう違いますか?

Adapterパターンの目的は「インターフェースを合わせる」ことです。

Facadeパターンは、複雑な複数の要素を「1つの単純なものとして見せる」ことが目的なので、方向性が少し異なります。

Q. プロトコル名はどう付ければよいですか?

レガシーの名前に合わせず、新しいコードから見て必要な役割に基づいて名付けてください。

LegacyUserAPIではなく、UserRepositoryのようにします。


レガシーコードを無理に消そうとせず、まずはプロトコルとアダプターで静かに隔離してみてください。

古いコードを1か所に閉じ込めるだけで、次のリファクタリングがずっと楽になります。皆さんのプロジェクトでも、最も複雑なAPIを1つ選んでラップしてみてください。

あわせて読みたい