削除もできず、そのまま使い続けるのもつらいレガシーAPIコードに困ったことはありませんか?
古いネットワークモジュールを新しい画面に接続するとき、誰もがぶつかる壁です。
結論から言うと、レガシーAPIをプロトコル(interface)でラップし、その間にアダプターを置くのが最もすっきりした解決策です。
Adapterパターンは、互換性のない2つのインターフェースの間に「変換器」を挟む方法です。
似たようにオブジェクトをラップするFacade・Proxy・Decoratorと混同するなら、ラッピングパターン4種類の比較でまず目的の違いを整理できます。
今日はSwiftでこれをどう適用するか、私が実際に経験した流れに沿って整理します。
この記事で得られる3つのこと
忙しい方向けに、先に要点をまとめます。
- 新しいコードが求める形を、まずプロトコルとして定義する
- レガシーAPIをそのプロトコルに合わせて変換するアダプター型を作る
- 画面とビューモデルはレガシー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/await、NSDictionaryの代わりに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交換時の修正範囲 | 画面全体 | アダプター1か所 |
| ユニットテスト | 難しい | モックで簡単 |
| 新しいコードの可読性 | 低い | 高い |
| 初期作業量 | 少ない | 少し増える |
初期作業量が少し増えるのは事実です。
しかし、私はこのコストをまったく惜しいとは思いませんでした。
テスト時にUserRepositoryを実装した偽オブジェクトを1つ渡すだけで、ネットワークなしに画面ロジックを検証できます。
実際のAPIがまだ完成していない状況でも、先に開発を始められました。
よくある質問(Q&A)
Q. アダプターはstructとclassのどちらで作るべきですか?
状態を持たないなら、structで十分です。
レガシーオブジェクトを保持し続ける必要がある、または参照共有が必要なら、classを使います。
Q. AdapterパターンとFacadeパターンはどう違いますか?
Adapterパターンの目的は「インターフェースを合わせる」ことです。
Facadeパターンは、複雑な複数の要素を「1つの単純なものとして見せる」ことが目的なので、方向性が少し異なります。
Q. プロトコル名はどう付ければよいですか?
レガシーの名前に合わせず、新しいコードから見て必要な役割に基づいて名付けてください。
LegacyUserAPIではなく、UserRepositoryのようにします。
レガシーコードを無理に消そうとせず、まずはプロトコルとアダプターで静かに隔離してみてください。
古いコードを1か所に閉じ込めるだけで、次のリファクタリングがずっと楽になります。皆さんのプロジェクトでも、最も複雑なAPIを1つ選んでラップしてみてください。

