株式会社LITALICOでアプリエンジニア(iOS/Rails)を担当しています、shuyuheyです。
『LITALICO Advent Calendar 2016』7日目の記事です。
まえがき
このエントリでは、iOSアプリをリファクタリングしながら、徐々にClean Architectureを適用していく過程で気づいたことをまとめます。
まだまだリファクタリングは途中ですし、アーキテクチャ適用の中でも手探りな部分がありますので、ご意見を頂けますと大変嬉しいです!
当初のアプリの状態
当初のアプリでは、次のようにロジックがまとめられていました。
- Managers
- サーバAPIへアクセスする
- レスポンスをModelへパースする
- パースしたModelの一部を、インスタンス変数で管理する
- シングルトンとして実装
- Model
- JSONオブジェクトの変換先
- Himotokiで実装
- ViewControllers
- 表示の際のロジックはすべてViewControllerにまとめてある
例えば、ユーザの情報を表示する画面の処理の流れは次のとおりです。
-
UserViewController
がUserManager.shared
のユーザ情報取得メソッドを呼び出す -
UserManager
はサーバにAPIリクエストし、コールバックでUserModel
を返す -
UserViewController
はUserModel
を使って、画面にユーザ情報を表示する
一見、流れとしては良いように見えます。しかし、この時の実装では、ViewController
にビジネスロジックが集中していたほか、データはManager
が管理していたりと、オブジェクトの責務がはっきりしていませんでした。そのため、機能を追加するときにどこにロジックを書くべきか、都度悩んでいました。
また、ゆくゆくはテストを書いていきたいので、オブジェクト間のインタフェースは出来る限りシンプルにしたい。
そこで、その2つの要求を満たす、Clean Architectureを選択しました。
Clean Architecture については、以下の記事を参考にしています。このエントリでは、Clean Architectureの説明はあまりしませんので、気になる方はこの記事を読んでみてください。
- まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて
- 持続可能な開発を目指す ~ ドメイン・ユースケース駆動(クリーンアーキテクチャ) + 単方向に制限した処理 + FRP
リファクタリングから始める
さあやってみようと思いたちましたが、前述の課題以外にも細かい問題が当初のアプリにはありました。経緯は割愛しますが、とにかくViewController
に継ぎ足し継ぎ足しでロジックが追加されていたり、1つのManager
に依存しているViewController
の数が多いことも有り、一気に適用するとおそらくかなり多くの機能がデグレするだろうな、という感覚がありました。
そこで、リファクタリングでロジックを分割していきながら、できる範囲でちょっとずつClean Architectureを適用してく戦略を取りました。
適用していく順番としては次のような順序です。
-
Model
をEntity
にリネーム -
ViewController
から表示用ロジックをPresenter
に分離 -
ViewController
およびPresenter
からビジネスロジックをUseCase
に分離- この時点で
ViewController
とPresenter
はManager
に依存していない状態を目指す
- この時点で
-
Translator
を実装し、Entity
をModel
に変換する- この時点で
ViewController
はEntity
に依存していない状態を目指す - また、
Model
固有の表示/ビジネスロジックはModel
に移す
- この時点で
-
Repository
を実装し、UseCase
とManager
の結合を疎にする -
Manager
をDataStore
に変更、DataStore
をEntity
ごとに分離
次節からは、それぞれの適用タイミングで感じたこと、気づきをまとめていきます。なお、現時点では4の途中まで手を付けていますので、今回は3までの示唆を書いていきます。
Presenter
の実装
はじめは、とにかく愚直に分離をしていきました。しかし、そのうちかなり冗長な記述をしてしまっているのではないか?と感じるようになりました。これなんかすごいやり過ぎ感が溢れていますね。これをPresenter
に実装しようとしていました。
func numberOfSectionsInCollectionView() -> Int {
if haveForYouContent {
return 2
}
return 1
}
また、TableViewController
で、表示するEntity
によって使うCell
を変更する、というロジックは表示ロジックではありますが、Presenter
に分離する程のものかな?など、どんどん疑問が増えはじめ早くもリファクタリングがしんどくなり始めていました。そこで、再度考え直し、Presenter
は実装しないことにしてみました。
Clean ArchitectureにおけるPresenter
は、Controller
とセットで考えられます。Controller
は、View
からのイベント(ボタンのタップやテキストの入力)をうけとり、UseCase
へと通知します。UseCase
は、その結果をPresenter
を通じて、View
に反映します。
iOSにおいては、その両方の役目をそもそもViewController
というクラスが担当しており、そこから更にロジックを分離しようとしてしまうと、自信を持って責務を切り分けることが出来ず、冗長な記述をしてしまったり、結局ViewController
からきれいにロジックが分離されなくなると思いました。
そこで、ViewController
の中で、Presenter
に当たるロジックとController
に当たるロジックを分離するためにextension
を使うことにしました。
後述のUseCase
の実装で詳しく説明をしますが、Controller
の責務に当たるビジネスロジックの呼び出しは、UseCaseInputProtocol
を実装したインスタンスのメソッド呼び出しで実現し、Presenter
の責務に当たる結果の受取と描画はUseCase
からUseCaseOutputProtocol
を実装したViewController
のメソッドを呼び出してもらうことにしました。
つまり、ViewController
は、次のように記述されることになります。
class ViewController: UIViewController {
var useCase: UserUseCaseInputProtocol = UseCase()
override func viewDidLoad() {
useCase.output = self
useCase.getUser()
}
}
extension ViewController: UserUseCaseOutputProtocol {
func onSuccess(user: UserModel) {
// 表示の処理
}
}
これで、Controller
の責務に当たる処理と、Presenter
の責務に当たる処理を切り分けることができるので、どこにロジックを書くべきか、という点については迷いがなくなります。
3. UseCase
の実装
前述したように、Presenter
は、クラスとして実装するのはやめたので、ViewController
からロジックを分離していきます。
UseCase
は、次のように実装していきました。
protocol UserUseCaseInputProtocol {
var output: UserUseCaseOutputProtocol? { get set }
func getUser()
}
protocol UserUseCaseOutputProtocol {
func onSuccess(user: UserModel)
}
class UserUseCase: UserUseCaseInputProtocol {
var output: UserUseCaseOutputProtocol?
func getUser() {
// 取得処理
output.onSuccess(user: user)
}
}
最初のうちはこれで良さそうだったのですが、リソースごとにUseCase
をまとめようとしてしまうとViewController
によっては、使用しないメソッドを実装する必要がでてくるようになりました。
例えば、UserUseCase
が、ユーザ情報の取得・更新などをするとします。すると、取得だけをすればいい画面でもUserUseCase
を利用しようとすると、更新用のメソッドも実装する必要が出てきてしまいます。
そこで、UseCase
は、アクションごとにprotocol
を分けることにしました。
protocol GetUserUseCaseInputProtocol {
var getOutput: GetUserUseCaseOutputProtocol? { get set }
func getUser()
}
protocol GetUserUseCaseOutputProtocol {
func onSuccess(updated user: UserModel)
}
protocol UpdateUserUseCaseInputProtocol {
var updateOutput: UpdateUserUseCaseOutputProtocol? { get set }
func updateUser(user: UserModel)
}
protocol UpdateUseCaseOutputProtocol {
func onSuccess(updated user: UserModel)
}
class UserUseCase {
var getOutput: GetUseCaseOutputProtocol?
var updateOutput: UpdateUseCaseOutputProtocol?
}
extension UserUseCase: GetUserUseCaseInputPtorocol {
func getUser() {
// 取得処理
output.onSuccess(user: user)
}
}
extension UserUseCase: UpdateUserUseCaseInputPtorocol {
func updateUser(user: UserModel) {
// 更新処理
output.onSuccess(updated: user)
}
}
これで、利用するViewController
は必要なアクションに応じたUseCase
を利用できるようになりました。
ViewController
で使うときは次のようになります。
class ViewController: UIViewController {
var useCase: GetUserUseCaseInputProtocol = UseCase()
override func viewDidLoad() {
useCase.getOutput = self
useCase.getUser()
}
}
extension ViewController: GetUserUseCaseOutputProtocol {
func onSuccess(user: UserModel) {
// 表示の処理
}
}
また、更新処理も合わせて利用する場合は、次のように書くことが出来ます。使うアクションをProtocol
で分けることによって、useCaseの宣言時に、このViewController
がどのような操作をリソースに対して行うのかを宣言することができるので、見通しが良くなったと感じています。
class ViewController: UIViewController {
var useCase: protocol<GetUserUseCaseInputProtocol, UpdateUserUseCaseInputProtocol> = UseCase()
override func viewDidLoad() {
useCase.getOutput = self
useCase.updateOutput = self
useCase.getUser()
}
}
extension ViewController: GetUserUseCaseOutputProtocol {
func onSuccess(user: UserModel) {
// 表示の処理
}
}
extension ViewController: UpdateUserUseCaseOutputProtocol {
func onSuccess(updated user: UserModel) {
// 表示の処理
}
}
今回は触れませんでしたが、テストを書く際に、Protocol
を分けたことでモックの実装がしやすかったという効果もありました。テストについてはまた別の機会に書きたいと思います。
まとめ
適用の目標としては、まだ半分の手順も出来ていませんが、ここまでの示唆をまとめてみました。
リファクタリングをちょっとずつやり始めてから4ヶ月ほどで、ほとんどのViewController
からManager
が取り除かれつつありますが、まだまだ先は長いなという印象です。
ただし、徐々にアーキテクチャを適用していくことで、コードのロジックが分散しにくくなっているな、という感覚があります。新しい機能を追加するときに、アーキテクチャに沿って実装していくためとっちらかりにくいのだと思います。
アプリのアーキテクチャを変更するのは、結構コストが大きいですが、リファクタリングの過程で、順序を決めてじわじわ適用していくと、比較的低コストでやっていけるのではないかと思います。
次回は、litalico-takamasa-mizukamiがKotlinについて書くみたいです!よろしくお願いします!