LoginSignup
52
53

More than 5 years have passed since last update.

SwiftのProtocolExtensionでBaseViewControllerの肥大化を解決!そして、必ず抑えておきたいデメリット

Last updated at Posted at 2015-12-24

BaseViewControllerの肥大化

ある特定の画面への遷移など、共通の処理を書きたいことはしばしばあります。

下の例はMyViewControllerA及びMyViewControllerBのどちらからもMyViewControllerCへ遷移したい時のコードです。

class MyViewControllerA: UIViewController {
    // ...省略...
    func showViewControllerC() {
        navigationController?.pushViewController(MyViewControllerC(), animated: true)
    }
    // ...省略...
}

class MyViewControllerB: UIViewController {
    // ...省略...
    func showViewControllerC() {
        navigationController?.pushViewController(MyViewControllerC(), animated: true)
    }
    // ...省略...
}

class MyViewControllerC: UIViewController {
    // ...省略...
}

MyViewControllerA及びMyViewControllerBのどちらにも実装されているshowViewControllerCメソッドを共通化したい時、一つの選択肢としてBaseViewController(共通のスーパークラス)を作る方法があります。

class MyBaseViewController: UIViewController {
    // ...省略...
    func showViewControllerC() {
        navigationController?.pushViewController(MyViewControllerC(), animated: true)
    }
    // ...省略...
}

class MyViewControllerA: MyBaseViewController {
    // ...省略...
}

class MyViewControllerB: MyBaseViewController {
    // ...省略...
}

class MyViewControllerC: UIViewController {
    // ...省略...
}

これでこのコードはすっきりして悪くはないのですが、共通の処理をMyBaseViewControllerに記述しすぎて肥大化するということがしばしば起こります。

また、意外とはまりがちなのが、UITableViewControllerUICollectionViewControllerのサブクラスにも同じ処理を実装したくなった場合です。
BaseViewController方式には、こういったところでいつか限界点が訪れることになるでしょう。

もう一つの方法としては、

extension UIViewController {
    // ...ここをどんどん拡張していく...
    func showViewControllerC() {
        navigationController?.pushViewController(MyViewControllerC(), animated: true)
    }
}

のような実装方法も考えられます。が、アプリ固有のコードをextensionでどんどん拡張していくのもあまり良い経験がありません。

ProtocolExtensionを使う

上記の問題はProtocolExtensionでMixinを実装することで解決できます。

一般的にMixinというと、インターフェースを定義してそれぞれのクラスが実装を担保するという形が多いと思いますが、ProtocolExtensionで行えるMixinはもっともっと強力です。

プロトコルに実装を記述することができるので、あるプロトコルを採用するだけで実装を組み込むことができてしまうのです。

下記のコードを見てみましょう。

protocol TransisionProtocol {
    func showViewControllerC()
}

extension TransisionProtocol where Self: UIViewController {
    func showViewControllerC() {
        navigationController?.pushViewController(MyViewControllerC(), animated: true)
    }
}

class MyViewControllerA: UIViewController {
    // ...省略...
}

class MyViewControllerB: UIViewController {
    // ...省略...
}

class MyViewControllerC: UIViewController {
    // ...省略...
}

// Mixin
extension MyViewControllerA: TransisionProtocol {}
extension MyViewControllerB: TransisionProtocol {}

解説

TransisionProtocolインターフェースを宣言します。

protocol TransisionProtocol {
    func showViewControllerC()
}

TransisionProtocolに実装を定義します。

extension TransisionProtocol where Self: UIViewController {
    func showViewControllerC() {
        self.navigationController?.pushViewController(MyViewControllerC(), animated: true)
    }
}

ここで重要なのが、where Self: UIViewControllerの部分です。
SelfがUIViewControllerを継承していることが保証されるので、実装側で、selfがあたかもUIViewControllerであるとして記述することができます。

あとは、各ViewControllerはTransisionProtocolを採用するだけです。

// Mixin
extension MyViewControllerA: TransisionProtocol {}
extension MyViewControllerB: TransisionProtocol {}

これで各ViewControllerにshowViewControllerCの実装を組み込むことができます。

コツ

目的ごとにプロトコルを宣言することで特定のクラスやプロトコルが肥大化するのを抑えることができるでしょう。
そうすれば、どのViewControllerがどのProtocolを採用しているかを見ればやりたいことが一目瞭然になります。

デメリット

ここまで読めばメリットはなんとなくわかると思います。
ただ、この方法にはメリットだけではなく、必ず抑えて置かなければいけないデメリットも存在します。
変なところではまらないために、このデメリットの方を強調したいです。

  • クラス側でプロトコルの実装を上書きしても気づけない
  • 名前が被らないように気を配る必要がある

などなどありますが、前者が一番大きなデメリットだと思います。
なぜかというと、コンパイルエラーにならないからです!

下記の例を見てみましょう。

protocol TransisionProtocol {
    func showViewControllerC()
    func pop() // 追加
}

extension TransisionProtocol where Self: UIViewController {
    func showViewControllerC() {
        navigationController?.pushViewController(MyViewControllerC(), animated: true)
    }

    // 戻る画面遷移
    func pop() {
        navigationController?.popViewControllerAnimated(true)
        print("TransisionProtocol pop")
    }
}

class MyViewControllerA: UIViewController {
    // ...省略...

    // 実装を上書き
    func pop() {
        // なにかpop的な別の処理
        print("MyViewControllerA pop")
    }
}

class MyViewControllerB: UIViewController {
    // ...省略...
}

class MyViewControllerC: UIViewController {
    // ...省略...
}

// Mixin
extension MyViewControllerA: TransisionProtocol {}
extension MyViewControllerB: TransisionProtocol {}

TransisionProtocolpopという、画面を戻すためのメソッドを追加してみました。

この状態でもし仮に、MyViewControllerApopというpop的動作の何かを実行するメソッドを追加したとします。
継承の時のようにoverrideをつけなくてもpopの実装を上書きしてしまうことができるため、この状態でpopを実行すると上書きされた方が実行されることになります。

myViewControllerA.pop()  // 出力: MyViewControllerA pop
myViewControllerB.pop()  // 出力: TransisionProtocol pop

このように無意識のうちに実装を上書きしてしまい、予期せぬ動作になることはバグの原因です。

プロトコル側のメソッドにあまりにもシンプルな名前を付け過ぎるとこのようなことが起きやすくなるでしょう。

最後に

この方法を実際のプロダクトに採用し始めてから日が浅いので、もっと別のデメリットに遭遇することがあるかもしれません。
また、他にもデメリットがあるという方がいらっしゃいましたらご意見いただけるとありがたいです!

52
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
53