iOS

Swiftでのプロトコル指向のプログラミングを考えてみる

概要

Swiftのプロトコル指向プログラミングのベストプラクティスがわからなかったので、海外で販売されている本を読みながらXcodeのplaygroundで挙動を見ながら勉強していきました。

これはその備忘録です。

Introducing protocol extensions (protocol extensionの導入について)

アプリ開発でよく見るプロトコル拡張の使い方はUIColorStringといった既存の型に新しいメソッドを追加するやり方ではないでしょうか。

ProtocolPlayground.swift
extension String {
    func shout(){
        print(uppercased())
    }
}
"Protocol extension is pretty cool".shout() // PROTOCOL EXTENSION IS PRETTY COOL といった感じに大文字に変換される

こんな感じですね。

それに対して下のコードはプロトコルに新しいインターフェースを定義してそれをextensionで実装していく流れになります。

ProtocolPlayground.swift
protocol TeamRecord {
    var wins: Int { get }  // 勝ち数
    var losses: Int { get } // 負け数
    var winningPercentage: Double { get } // 勝率
}

extension TeamRecord {
    var gamesPlayed: Int { // 試合数
        return wins + losses
    }
}

struct BaseballRecord: TeamRecord {
    var wins: Int
    var losses: Int
    var winningPercentage: Double {
        return Double(wins) / Double(wins + losses)
    }
}

let bayStars = BaseballRecord(wins: 10, losses: 5)
print(bayStars.gamesPlayed) // 15 回
print(bayStars.winningPercentage) // 0.666666 ~

Default Implementations (デフォルト実装)

ProtocolPlayground.swift
// Before extensionで拡張前

struct BasketBallRecord: TeamRecord {
    var wins: Int
    var losses: Int
    let seasonLength = 82  // デフォルト実装が持てる

    var winningPercentage: Double {
        return Double(wins) / Double(wins + losses)
    }
}

extensionで拡張させることで

ProtocolPlayground.swift
extension TeamRecord {
    var winningPercentage: Double {
        return Double(wins) / Double(wins + losses)
    }
}

// After extensionで拡張後

struct BasketBallRecord: TeamRecord {
    var wins: Int
    var losses: Int
    let seasonLength = 82 // シーズンの長さ
}

let minneapolisFunctors = BasketBallRecord(wins: 60, losses: 22)
print(minneapolisFunctors.winningPercentage) // winningPercentage が使えるようになる // 0.7317

structの中の実装は少なくなるよね、といった話し。
さらにstructのインスタンスはprotocol extensionで実装したものが使えるようになっています。

ProtocolPlayground.swift
struct HockeyRecord: TeamRecord {
    var wins: Int
    var losses: Int
    var ties: Int // 引き分けのプロパティ、 追加

    // Hockey のレコードは引き分けのプロパティを導入したので「勝率」の計算方法が変わる
    var winningPercentage: Double {
        return Double(wins) / Double(wins + losses + ties)
    }
}

let chicagoOptionals = BasketBallRecord(wins: 10, losses: 6)
let phoenixStridables = HockeyRecord(wins: 8, losses: 7, ties: 1)

print(chicagoOptionals.winningPercentage) // 10 / (10 + 6) = 0.625
print(phoenixStridables.winningPercentage) // 8 / (8 + 7 + 1) = 0.5

Understanding protocol extension dispatching

ProtocolPlayground.swift
protocol WinLoss {
    var wins: Int { get }
    var losses: Int { get }
}

extension WinLoss {
    var winningPercentage: Double {
        return Double(wins) / Double(wins + losses)
    }
}

struct CricketRecord: WinLoss {
    var wins: Int
    var losses: Int
    var draws: Int

    var winningPercentage: Double {
        return Double(wins) / Double(wins + losses + draws)
    }
}

let miamiTuples = CricketRecord(wins: 8, losses: 7, draws: 1)
let winLoss: WinLoss = miamiTuples

print(miamiTuples.winningPercentage) // 0.5
print(winLoss.winningPercentage) // 0.53 drawsがカウントされないため

WinLossには引き分けの draws が存在しないので、winLossはprotocol-extensionの方のメソッドwinningPercentageを出力する

Type constraints

ProtocolPlayground.swift
// 概念
protocol PostSeasonEligible {
    var minimumWinsForPlayoffs: Int { get }
}

// PostSeasonEligible と TeamRecord に準拠している時だけ適用される
extension TeamRecord where Self: PostSeasonEligible {
    var isPlayoffEligible: Bool {
        return wins > minimumWinsForPlayoffs
    }
}
ProtocolPlayground.swift
// 具体例
protocol Tieable {
    var ties: Int { get }
}

extension TeamRecord where Self : Tieable {
    var winningPercentage: Double {
        return Double(wins) / Double(wins + losses + ties)
    }
}

struct RugbyRecord: TeamRecord, Tieable {
    var wins: Int
    var losses: Int
    var ties: Int
}

//struct HockeyRecord: TeamRecord {
//    var wins: Int
//    var losses: Int
//    var ties: Int
//
//    var winningPercentage: Double {
//        return Double(wins) / Double(wins + losses + ties)
//    }
//}

let rugbyRecord = RugbyRecord(wins: 8, losses: 7, ties: 1)
print(rugbyRecord.winningPercentage) // 0.5

Protocol-oriented benefits (プロトコル指向のメリット)

プロトコル指向のプログラミングをやるメリットとして

  1. Programming to interfaces, not implementations
  2. Traits, mixins and multiple inheritance
  3. Simplicity

のメリットを享受出来る。

Programming to interfaces, not implementations

プロトコルは実装にフォーカスするのではなくインターフェースにフォーカスする
protocol にinterfaceを持たせるのでprotocolを見ればどういった機能なのかわかるので最終的にはFatClassが無くなりそうだよね。

Traits, mixins and multiple inheritance

多重継承 の問題を解決する
interfaceをprotocolに持たせてextensionで実装を行い、それをstruct やclass に準拠させるのでクラスの共通実装が必要なくなる。そのため、BaseClassといった共通クラスを継承したサブクラスにする必要がないので A is B 問題を解決する糸口になる。

Simplicity

単純かつ、簡潔、簡易なものにする

あとがき

海外のプログラミング本は最初はクオリティとか本当に理解できるのかと疑ってましたが、日本で販売されているプログラミング本よりも大変わかりやすかったです。最初は英語の苦手意識がありました。ですが、それを乗り越えると英語の解説書の方が回りくどい表現がない分理解が早くなることがわかる。

delegateとかprotocolなどの専門用語を日本語に翻訳する方が難しい気もします。

英語と聞くとアレルギーを起こすエンジニアさんも多数いるとは思いますが、

海外の本は日本の受験英語みたいに難しい英文はそこまでないように思います。

だったら最初から英語の本を読みながら英語でプログラミングを理解する方がいい気がします。

ですが、これは個々人の好みの問題ではあるところだと思います。

ABOUT ME
tamappe
都内で働くiOSアプリエンジニアのTamappeです。 当ブログではモバイルアプリの開発手法について紹介しています。メインはiOS、サブでFlutter, Android も対応できます。 執筆・講演のご相談は tamapppe@gmail.com までお問い合わせください。