結論
- 小手先で楽をするためのボトムアップな設計は後々苦労する
- 継承を使った差分プログラミングは長年運用していくと大変だ
- 人は楽な方に流れるので、Baseクラスで解決すべきでない問題をBaseクラスで解決して後で困る
はじめに
この文章は2015年1月のpotatotips13で発表するネタ用のメモに書いてました。
実際に発表した内容を含む様子は下記のページにまとめています。
http://curiosity.co.jp/potatotips13/
会場で質問されたりツイートの様子を見てて気づいたのですが、BaseViewControllerを使いたくないという"この文章"と同意の意見は、比較的経験のあるおじさんたちの意見であって、若い人からするとなぜBaseViewControllerを使ってはいけないように言われるのかについて具体例を聞きたがる傾向が強いです。
また、不必要に自分が気に入ってるやり方を否定されたような気にもなるのかもしれないという事も追記しておきます。
本題
iOSアプリを設計する際、BaseViewControllerのようなアプリ共通のViewControllerを作り、それを継承した各画面ごとのViewControllerを作っていくのはメリットが多いように思えます。
しかし、自分の経験上、他の方法でいくらでも効率的な設計はあり、BaseViewControllerを作りたくなった際には検討をしたほうが良いというのがこの文章の結論です。
もちろん、設計というのはそのアプリごとの要件があり正解のないケースバイケースな物事なのですが、共通のノウハウというものは存在しています。そのため、今回は自分自身が長年モヤモヤしていたBaseViewControllerは作りたくないという文章にして公開することにしました。
もし、「BaseViewControllerサイコーだよ」または「BaseViewControllerはここがダメだと思う」という意見があれば是非コメント欄にお願いします。この場でその意見に対して否定する気も議論する気もなく、新しくアプリを設計する際の参考にもなるんじゃないかと思っています。
何を言いたいか
- iOSアプリ開発ではBaseViewControllerみたいなやつをつくってそれを各画面用のViewControllerが継承して使うことがある
- 嫌な理由の1つ目はメリットに比べてデメリットが大きいと思う
- 嫌な理由の2つ目は実装をまとめるために継承を利用するのはボトムアップな設計で自分には歪に見える
以下メリット・デメリットを述べながらそれっぽい事を書く
BaseViewControllerをつくるメリット
- 各画面共通の処理をまとめる事ができる
- 具体的にはアクセス解析的なお決まりの処理をまとめられる
- 画面サイズに応じた処理もまとめられる
- OSのバージョンに応じた処理もまとめられる
BaseViewControllerをつくるデメリット
- 各画面事の処理をまとめるとBaseViewControllerが肥大化する
- ほとんどの画面では共通だが例外の画面が要件によって出てきて困る
- BaseViewControllerで条件分岐する?
- 一度まとめた処理を各ViewControllerに書く?
- そもそもUIViewController、UITableViewController、UICollectionViewControllerがある
- BaseTableViewControllerが必要になってくる
- BaseCollectionViewControllerが必要になってくる
- ViewControllerはContainer埋込みされることもある
- Containerに保持されるパターンでは同じBaseViewControllerを継承すべきじゃないなど条件があれば複雑化していく
- オレオレフレームワークのようになると新規開発メンバーへの学習コストが増す
メリットに対する経験上の感想
共通の処理でViewControllerごとに可変のパラメータが必要な場合
そもそも各画面お決まりの処理にもパラメータは必要なはず、そのパラメータが自動で取得できる要件(例えばクラス名+メソッド名など)ならば問題ないが、この画面ならこのパラメータなど細かいパラメータがあるのなら、もう各ViewControllerで呼び出せばいいんじゃない?実装後の確認も楽じゃない?
共通の処理でBaseViewControllerがパターンの例外に対応しないといけなくなる場合
仕様の追加変更では特に画面系の例外のパターンは起きるので、そういうパターンの例外に対する処理を書かなきゃいけなくなりそう。具体的には「このサブクラスならこのメソッドを実行しない」のような処理をBaseViewControlelrに記述しないといけなくなるのではないか。
実際の処理はUtilityクラスのメソッドやHelperクラスのメソッドにコーディングすることになるんだけど、それをViewControllerのライフサイクルに応じて動かしたいからBaseViewControllerにしたいのかなという気持ちは分かる。
単純にコードの見通しを良くしたい場合
コードの見通しを良くしたいのであれば、他の手段を組み合わせることでどうにか出来ないか。
まずViewControllerの責任の見直してみると大きく分けるとこんな感じ
- (必要があれば)Viewの生成
- 何らかのステータスによって表示するViewを切り替え
- イベント処理
- ライフサイクルに応じてAnalyticsにデータ送信
- 次の画面への遷移
- ユーザーのデータ入力時の処理
- 固定のボタンなどのイベント
- 表示用データに関する処理
- (通信やDBから)表示用データ取得開始と完了まで
- 表示用データの保持
- (必要があれば)表示用データをKVOで監視し反映
- Cell/Viewへのデータアサイン
- (必要があれば)状態による表示の切替
- Cell/Viewをタップされた際のイベント
- (通信やDBから)表示用データ取得開始と完了まで
これらの処理を責任ごとに分けていく
ステータスに応じたViewの切り替えはViewControllerでやる
ViewControllerがステータスを保持していてViewを切り替える場合、というのはよくある話で、View自体の共通性がないのであれば別途作る方法が良いと思う。
また、頻繁に状態が変わる場合とViewController生成時にしか状態が変わらない場合の2通りあり、前者はKVOで監視したりしてステートマシンでの処理記述を検討する(https://github.com/ReactKit/SwiftState)。
イベントの処理もViewControllerでやる
ジェスチャーやタッチ系のイベントの処理もViewControllerでやればいいと思う。
その他
- データ入力ではReactiveCocoa/ReactKitを検討する
- イベントはPromiseを検討する
表示用データに関する処理はDataSourceを別クラスに移動する
表示用のデータ取得と表示に関しては、過去自分がまとめたUIViewControllerからUICollectionViewのDataSourceを分離し肥大化を防ぐ具体例のようにすれば責任を分割できてコードの見通しはぐっとよくなる。
元ネタになるViewControllerの軽量化については、ネット上でかなり前から検討されていて、2010年くらいの記事ではUITableViewController を使わない設計や最近ではobjc.io #1-1 軽量なView Controller(日本語訳)などにも記載がある。
また、自分のやり方とは違うが、データ取得と保持はViewControllerの責任とする方法はこのスライドが参考になるかも。自分としてはViewControllerはある状態の時にその画面のどんなViewを表示するかを知ってれば良いと思っていて、そのためのどんなデータを保持するかは(極論かもしれないけど)どうでもいいので、DataSourceなクラスが表示用のデータを取得/保持していれば良いと思うことが多い。
おわりに
ケースバイケースって言えばその通りだし、一生一人でやるようなプロジェクトなら一人でやれる規模なんだろうからBaseViewControllerなどを使うのは良い方法なのかもしれない。
処理が分割されていないViewControllerに対してリファクタリングしてDataSourceで処理を分割すべきかというと、そうとも言い切れない。例えばViewControllerの処理が2か3000行以上あって処理を分割しないとバグが出そうとかそういう意図で問題を解決したいなら検討したほうが良いだろうと思う。