Help us understand the problem. What is going on with this article?

リファクタリングから始めるiOS Clean Architecture (Presentation層&Usecase)

More than 3 years have passed since last update.

株式会社LITALICOでアプリエンジニア(iOS/Rails)を担当しています、shuyuheyです。
LITALICO Advent Calendar 2016』7日目の記事です。

まえがき

このエントリでは、iOSアプリをリファクタリングしながら、徐々にClean Architectureを適用していく過程で気づいたことをまとめます。
まだまだリファクタリングは途中ですし、アーキテクチャ適用の中でも手探りな部分がありますので、ご意見を頂けますと大変嬉しいです!

当初のアプリの状態

当初のアプリでは、次のようにロジックがまとめられていました。

  • Managers
    • サーバAPIへアクセスする
    • レスポンスをModelへパースする
    • パースしたModelの一部を、インスタンス変数で管理する
    • シングルトンとして実装
  • Model
    • JSONオブジェクトの変換先
    • Himotokiで実装
  • ViewControllers
    • 表示の際のロジックはすべてViewControllerにまとめてある

例えば、ユーザの情報を表示する画面の処理の流れは次のとおりです。

  1. UserViewControllerUserManager.sharedのユーザ情報取得メソッドを呼び出す
  2. UserManagerはサーバにAPIリクエストし、コールバックでUserModelを返す
  3. UserViewControllerUserModelを使って、画面にユーザ情報を表示する

image

一見、流れとしては良いように見えます。しかし、この時の実装では、ViewControllerにビジネスロジックが集中していたほか、データはManagerが管理していたりと、オブジェクトの責務がはっきりしていませんでした。そのため、機能を追加するときにどこにロジックを書くべきか、都度悩んでいました。
また、ゆくゆくはテストを書いていきたいので、オブジェクト間のインタフェースは出来る限りシンプルにしたい。
そこで、その2つの要求を満たす、Clean Architectureを選択しました。

Clean Architecture については、以下の記事を参考にしています。このエントリでは、Clean Architectureの説明はあまりしませんので、気になる方はこの記事を読んでみてください。

リファクタリングから始める

さあやってみようと思いたちましたが、前述の課題以外にも細かい問題が当初のアプリにはありました。経緯は割愛しますが、とにかくViewControllerに継ぎ足し継ぎ足しでロジックが追加されていたり、1つのManagerに依存しているViewControllerの数が多いことも有り、一気に適用するとおそらくかなり多くの機能がデグレするだろうな、という感覚がありました。

そこで、リファクタリングでロジックを分割していきながら、できる範囲でちょっとずつClean Architectureを適用してく戦略を取りました。

適用していく順番としては次のような順序です。

  1. ModelEntityにリネーム
  2. ViewControllerから表示用ロジックをPresenterに分離
  3. ViewControllerおよびPresenterからビジネスロジックをUseCaseに分離
    • この時点でViewControllerPresenterManagerに依存していない状態を目指す
  4. Translatorを実装し、EntityModelに変換する
    • この時点でViewControllerEntityに依存していない状態を目指す
    • また、Model固有の表示/ビジネスロジックはModelに移す
  5. Repositoryを実装し、UseCaseManagerの結合を疎にする
  6. ManagerDataStoreに変更、DataStoreEntityごとに分離

次節からは、それぞれの適用タイミングで感じたこと、気づきをまとめていきます。なお、現時点では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に反映します。 

image

iOSにおいては、その両方の役目をそもそもViewControllerというクラスが担当しており、そこから更にロジックを分離しようとしてしまうと、自信を持って責務を切り分けることが出来ず、冗長な記述をしてしまったり、結局ViewControllerからきれいにロジックが分離されなくなると思いました。

image

そこで、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について書くみたいです!よろしくお願いします!

shuyuhey
アプリエンジニアやってます。iOS (Swift)とRailsとかが今は中心。
litalico
「障害のない社会をつくる」というビジョンに向けて、社会の側にある障害をテクノロジーの力で取り除くことを目指す
http://litalico.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away