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

概要

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

これはその備忘録です。

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

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

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

こんな感じですね。

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

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 (デフォルト実装)

// Before extensionで拡張前

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

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

extensionで拡張させることで

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で実装したものが使えるようになっています。

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

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

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

// PostSeasonEligible と TeamRecord に準拠している時だけ適用される
extension TeamRecord where Self: PostSeasonEligible {
    var isPlayoffEligible: Bool {
        return wins > minimumWinsForPlayoffs
    }
}
// 具体例
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などの専門用語を日本語に翻訳する方が難しい気もします。

英語と聞くとアレルギーを起こすエンジニアさんも多数いるとは思いますが、
海外の本は日本の受験英語みたいに難しい英文はそこまでないように思います。

だったら最初から英語の本を読みながら英語でプログラミングを理解する方がいい気がします。
ですが、これは個々人の好みの問題ではあるところだと思います。