Posted at

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

More than 3 years have passed since last update.

image

この記事は 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があり、どれも見た目や機能はだいたい同じです。

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

image

これらをすべて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