ソフトウェア設計

Swift テンプレートメソッドパターン:プロトコル extension で骨格を作る

クラスごとに似たロジックをコピペして、「何かおかしい」と感じたことはありませんか? iOS アプリで画面ごとのデータ読み込みコードを3回目にコピーする頃には、手が止まってしまいますよね。

読了 6 分
Swift テンプレートメソッドパターン:プロトコル extension で骨格を作るのカバー画像

クラスごとに似たロジックをコピペして、「何かおかしい」と感じたことはありませんか? iOS アプリで画面ごとのデータ読み込みコードを3回目にコピーする頃には、手が止まってしまいますよね。

そんなときに役立つのが、テンプレートメソッドパターンです。

結論から言うと、全体の流れ(骨格)を1か所に固定し、異なるステップだけを各実装に埋めてもらうパターンです。Swiftでは、継承よりもプロトコル extension を使うと、よりすっきり構成できます。

この記事では、テンプレートメソッドパターンとは何か、Swiftではなぜプロトコル extension が理想に近いのか、そして実際のコードで骨格をどう作るのかを順に説明します。

まずは要点をまとめます

  1. テンプレートメソッドパターンは、「固定された手順」と「差し替え可能なステップ」を分離する方法です
  2. 従来は親クラスの継承で実装しますが、Swiftらしくするならプロトコル extension が適しています
  3. 骨格は extension のデフォルト実装に置き、異なる部分はプロトコルの要件として残します
  4. 単一継承や強い結合といった継承の制約から解放されます

テンプレートメソッドパターンとは?

名前は大げさですが、概念は単純です。

料理のレシピを思い浮かべてください。「材料の準備 → 下ごしらえ → 調理 → 盛り付け」という順序は固定です。

ただし、材料や調理方法は料理ごとに異なります。

ここで順序(骨格)がテンプレートメソッドで、料理ごとに異なる各ステップが差し替え部分です。

流れは1か所で制御し、詳細な実装だけを外部に委譲する。

この一文がテンプレートメソッドパターンのすべてだと言ってもよいでしょう。

コードで見ると、こういう状況です。データを読み込む手順はどの画面でも同じです。読み込み開始 → リクエスト → パース → 画面更新。

異なるのは、「どのURLにリクエストするか」と「受け取ったデータをどうパースするか」くらいです。

この共通手順を毎回書き直していたのが、無駄だったのです。


Swiftでは、なぜ継承ではなくプロトコル extension なのか?

元来の方法は、親クラスに骨格を置き、子クラスが特定のメソッドだけを override する構造です。Javaの教科書に出てくる方法ですね。

しかしSwiftでこれをクラス継承で実装すると、いくつもの制約があります。

  • クラスは単一継承のみです。すでに親クラスを1つ使っていれば、それで終わりです
  • structやenumではまったく使えません
  • 親子が強く結合し、後から差し替えるのが難しくなります

プロトコル extension は、これらの問題の大部分を解決します。

骨格となるメソッドは extension のデフォルト実装として提供し、異なるステップはプロトコルの要件として宣言だけしておきます。

すると、structでもclassでも、そのプロトコルを採用するだけで共通の骨格を利用できます。継承の制約もありません。

以下は、データ読み込み手順をプロトコルで定義した例です。load()が固定された骨格で、残り2つが差し替え部分です。

protocol DataLoadable {
    associatedtype Item
    var endpoint: URL { get }          // 画面ごとに異なる
    func parse(_ data: Data) -> [Item] // 画面ごとに異なる
}

extension DataLoadable {
    func load() async throws -> [Item] {   // 固定された骨格
        let (data, _) = try await URLSession.shared.data(from: endpoint)
        return parse(data)
    }
}

load()の順序は extension が制御します。

各画面はendpointとparseだけを、それぞれの方法で実装すればよいのです。

骨格はプロトコルに置き、空欄だけを採用側が埋めます
骨格はプロトコルに置き、空欄だけを採用側が埋めます
骨格を固定し、異なるステップだけを差し替えます
骨格を固定し、異なるステップだけを差し替えます

実際に骨格を作ってみましょう

では、このプロトコルを採用する側を見てみましょう。継承ではなく採用なので、structでも実装できます。

struct ArticleLoader: DataLoadable {
    let endpoint = URL(string: "https://api.example.com/articles")!
    func parse(_ data: Data) -> [Article] {
        (try? JSONDecoder().decode([Article].self, from: data)) ?? []
    }
}
// 使用: let articles = try await ArticleLoader().load()

ご覧のとおり、ArticleLoaderはload()を直接実装していません。

それでもload()を呼び出せます。extensionがデフォルト実装を提供しているからです。

これが、プロトコル extension で構成したテンプレートメソッドの力です。新しい画面が増えたら、endpointとparseだけを実装するstructをもう1つ作れば完了です。

この方法に変えてから、画面追加のコードは半分以下になりました。読み込みとエラー処理を1か所に集約できたので、バグ修正もずっと簡単になりました。

structを1つ追加するだけで画面を増やせるため、コードが大幅に減りました
structを1つ追加するだけで画面を増やせるため、コードが大幅に減りました

1つ注意点があります。プロトコル extension のデフォルト実装は、overrideとは少し動作が異なります。採用型が同名のメソッドを定義しても、プロトコルの要件として宣言されていなければ、静的ディスパッチによって意図しない実装が呼ばれることがあります。

そのため、差し替えるステップは必ずプロトコル本体の要件として宣言しておくのが安全です。


簡単なQ&Aでまとめます

Q. では、継承ベースのテンプレートメソッドはもう使わなくてよいのでしょうか?

UIKitのように、すでにクラス階層がある場所では、継承によるoverrideが自然な場合もあります。状況に応じて選んでください。

Q. associatedtypeが難しく感じます。

戻り値の型が固定なら、associatedtypeを使わず通常のプロトコルとして定義できます。ジェネリックが必要な場合だけ使いましょう。


プロトコル extension で骨格を作ってみると、継承で苦労していたことが少しむなしく感じるほど、すっきり整理できます。

いまコピペしているロジックがあるなら、今日ひとつだけプロトコルに切り出してみてください。きっと感覚がつかめます。応援しています!

あわせて読みたい記事