Swiftのprotocol extensionでmixin的なものを実現する


この記事は Goodpatch Advent Calendar 2015 3日目の記事です。昨日は @daichi_ito今日からはじめる命名戦略でした。

GoodpatchでもSwiftを使うことが増えてきました。僕自身、今年5月に入社して以来ずっとSwiftを書いてます。今日はSwift2で新しく追加されたprotocol extensionを使って、mixin的なものを実現する方法を紹介します。これを上手く使うと、コードの再利用が柔軟にできて便利です!

protocol extensionとは?

protocol(インタフェースの定義)を拡張し、メソッドの実装を追加できる機能です。

protocol MyProtocol {
    func hoge()
}

extension MyProtocol {
    func hoge() {
        print("hoge")
    }
}

class MyClass: MyProtocol {
}

let myInstance = MyClass()
myInstance.hoge() // hoge と出力される

mixinとは?

あるクラスに対して、外側からメソッドを追加します。共通の機能を1つのコードにまとめて、複数のクラスで再利用できます。

継承じゃだめ?

継承でもコードの再利用・共通化はできますが、柔軟性に欠けます。

例えば、何かしら記事のリストを表示するアプリがあったとします。新着順、人気順、検索結果を表示する3つのViewControllerがあり、どれも見た目や機能はだいたい同じです。

ですが、新着順・人気順には運営からのお知らせを出したい(検索結果には出したくない)など、各画面で微妙に要件が異なります。(実際のアプリ開発でもよくあると思います)

これらをすべてBaseViewControllerみたいな親クラスを使って吸収すると、次のような問題が発生します。

  • BaseViewControllerの肥大化
    • 各画面の違いを吸収するため、BaseViewControllerにすべて突っ込むことになる
  • 影響範囲が無駄にでかくなる
    • BaseViewControllerを修正すると、それらを継承しているすべてのViewControllerが影響範囲になる。お知らせに関わるコードのみ修正しても、意図せず検索画面に影響でる可能性がある(複数人開発だと特にこわい)。
  • 仕様変更に弱い
    • 人気順だけお知らせの文言を変えたい場合、BaseViewControllerの中で分岐する必要があり、コードの見通しが悪くなる
    • 記事のリストとは全く異なる画面(自分のプロフィール画面とか)にもお知らせを出したくなった場合、コードをコピペすることになる。将来的にお知らせのロジックを変更する場合、修正箇所が増える。

参考:iOSアプリの設計でBaseViewControllerのようなのは作りたくない - Qiita

protocol extensionでmixin的なものを実現する

継承ではなく、mixinを使うことで上記の問題は解決します。

TableViewControllerに対して、headerViewにお知らせを表示する機能を追加してみましょう。

Noticeable.swift
protocol Noticeable {
    func getNotice()
    func openNotice(url: NSURL)
    func closeNotice()
}
extension Noticeable where Self: UITableViewController {
    func getNotice() {
        NoticeAPIClient.find()
        .success { (notice: Notice) -> Void in
            let noticeView = NoticeView.view()
            noticeView.notice = notice
            self.tableView.tableHeaderView = noticeView
        }
    }

    func openNotice(url: NSURL) {
        let webVC = //WebViewでお知らせを表示
        self.navigationController?.presentViewController(webVC, animated: true, completion: nil)
    }

    func closeNotice() {
        guard let noticeView = self.tableView.tableHeaderView as? NoticeView else { return }

        self.tableView.tableHeaderView = nil
    }
}
NewTopicsTableViewController.swift
class NewTopicsTableViewController: UITableViewController, Noticeable {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.getNotice()
    }
}

細かい部分は省略しましたがだいたいこのような感じです。

ポイントは where Self: UITableViewController です。これでprotocol extensionの対象をUITableViewControllerに絞ることができます。つまりはUITableViewControllerに対してmixinできます。self.tableViewとか書いても普通に補完してくれます。

参考:プロトコル拡張の話? #WWDC21cafe

注意事項

便利なprotocol extensionですが、格納型プロパティを持つことができません。そのため実現したい機能によってはうまく実装できないかもです。1

また、格納型プロパティをもてないので、厳密にはmixinではなく、traitと呼ぶようです。mixinの方が馴染みがあってわかりやすいと思ったので今回はmixin的なものと書きました。

参考:Mixins and Traits in Swift 2.0

おわりに

明日はスクーにも出たことがある @migi がJS初学者に向けた話をするみたいです。お楽しみに!


  1. 一応無理やり格納型プロパティをもたせる方法はあります。Swiftのextensionでstored propertyを追加する?(黒魔術は閉じ込める) - TOKOROM BLOG