この記事は クラウドワークス Advent Calendar 2018 の8日目の記事です。
はじめに
こんにちはクラウドワークスでエンジニアをしている @tkoshida です。
つい最近、新規のiOSアプリを開発するプロジェクトが始まったのですが、そのプロジェクトのiOSアプリ開発でクリーンアーキテクチャを採用しようということになりました。
しかし開発メンバーの一人から「クリーンアーキテクチャは嫌だ」と言う声があがりまして、ここでは何でそんなことになったんだっけ、というのを回想してみたいと思います。
TL;DR
- クリーンアーキテクチャやってみたけどメンバーから実装が面倒でやりたくない的なことを言われた。
- 別にクリーンアーキテクチャが悪いわけではない。
- ただ確かに実装効率も検討してアーキテクチャを考えたほうが良いかもと思った。
反発の起きたポイント
冒頭の開発メンバーの意見をよくよく聞いてみると、以前作ったクライアントアプリでのことを気にしており、そのときのアプリのアーキテクチャ構成だと
- 一々プロトコルなどを用意したり登場するコンポーネントが多くて実装効率が悪い。
- 機能を実装してるというよりクリーンアーキテクチャを実装しているという感じになってしまう。
- 自分は素早く機能を実現したい。
- Fat ViewControllerにこだわる人多いが、そもそも登場人物が多くなると全体的にFatになることを気にしないのがよくわからない。
- クリーンアーキテクチャはそもそも実用的ではない「机上の空論」と思っている。
ということで、元も子もない発言もありましたが確かにそういう意見もあるだろうなと思う次第でした。
クライアントアプリというのは、クラウドワークスではメンバー(受注者)向けのアプリをメインに開発していますが、クライアント(発注者)向けのアプリも機能が少ないながら開発、リリースしています。
このクライアントアプリの開発にはクリーンアーキテクチャを採用していました。
メンバーアプリの振り返り
クライアントアプリについてふり返る前に、クライアントアプリに先立って開発したメンバーアプリの開発について振り返ってみます。
クラウドワークスはもともとメンバー向けのアプリを2015年の夏に最初のアプリとしてリリースしています。
これは私がクラウドワークスに2015年春にジョインしてサイト仕様も何もわからず、どういうアプリを作るからの要件定義から結合テスト、リリースするまで3ヶ月ちょっとの期間で開発したアプリでした。
アプリの実装は私が担当して、その他デザイナやサーバーサイドのエンジニア、アルバイトの方などと作っていきました。
その頃は、Swift 1.2で、ReactiveCocoa/MVVMが流行っていた時期(解釈)で、私もその流れにのりObjective-Cしかやったことがなかった中Swiftに取り組み、Rxも利用したことがなかったですが
ReactieveCocoaを使って楽しく開発していました。
アプリのアーキテクチャに関してはMVVM(これも実践で使うのは初めて)を採用し、「Fat ViewControllerにならないな、よしよし」と開発していたのを覚えています。
ただ、振り返って見ると、初めて採用する言語であったり、Rxのライブラリも初めてで、MVVMにも挑戦したのでコードの書き方には若干独自ルールのようなものが存在したり、コードの責務の分け方が若干揺れていたりして、後にiOSアプリエンジニアの方が新しく入ってきたときに一々説明しないダメな箇所が発生したりで若干の課題感を抱えていました。
クライアントアプリの振り返り
2016年のある日、新しいアプリ開発の話があがりました。
それまではメンバー向けアプリは存在していたものの、クライアント向けのアプリがなかったためそのアプリを開発することとなったのです。
当時は、新しいiOSアプリ開発エンジニアも入っていた関係で、メンバーアプリとは違いアプリ開発も複数人で行うこととなりました。
そこで、下記は当時の経緯をまとめたものとしてあったのですが、このような課題感もありクリーンアーキテクチャを採用する方向に動きました。
メンバーアプリについてはMVVMをMVCのFat ViewControllerを解消する、ということで導入しました。
がやはりMVVMのViewModelでも肥大化しやすい、という状況になってきました。
(これでもだいぶMVCと比べるとましにはなっていると思いますが。。)それに加えて、一人で実装しているときは良かったのですが自分以外の担当者が実装するようになると、ViewController, ViewModel, Modelの役割分担やクラスの分け方など、もともと意図していたような構成を保ちつつエンハンスしていくのが難しいことがわかりました。
それはどのような基準でクラスや責務を分割していくというのが不明確であったところも大きいかと思っています。そこで、今回一般的にある程度は知られているクリーンアーキテクチャを採用しました。
クリーンアーキテクチャによって役割分担の仕方がより明確になり、担当者によりぶれない作りになることが期待されます。
Clean Swift
Clean Swiftがクライアントアプリで採用したアーキテクチャです。
Clean Swiftは、Swiftでクリーンアーキテクチャを実装する具体的な方法についてまとめたものとなっており、コンポーネントとして
- View
- ViewController
- Presenter
- Interactor
- Model
- Router
- Worker
- Configurator
といったものが定義されています。
その具体的な仕様については、ここでは詳しく述べませんのでリンク先を参照ください。
クライアントアプリでの各種コンポーネントの全体像は以下のようになります。
Clean SwiftではWorkerがビジネスロジックを扱うコンポーネントとして登場していたのですが丸っとした印象をうけたので、WorkerをクリーンアーキテクチャのUseCase以降で置き換えました。
そしてこのアーキテクチャで、とある一覧画面を実装したクラス構成は次のようになりました。(ViewやModelなども含めるとものすごい数になるので割愛)
各コンポーネントについて
Presentation Layer
ViewControllerとPresenter、Interactorについては以下のように一方向のつながりにするよう制約します。
- ViewControllerは、Viewが検知したタップイベントなどをうけて、Interactorにそのイベントに対応するビジネスロジックを実行するよう依頼します。
- InteractorはUseCaseを呼び出しビジネスロジックを実行してPresenterにその結果を通知します。
- PresenterはInteractorからの結果通知をうけてViewControllerに表示内容を指示します。
- ViewControllerはPresenterからの指示に従いViewの更新を行ったり、Routerを介して画面遷移を行ったりします。
以下のコンポーネント間はプロトコルでI/Fを定義しそれぞれI/Fに依存した実装をします。
- ViewController -> Interactor
- Interactor -> Presenter
- Presenter -> ViewController
また、それぞれのコンポーネント間のメッセージは、以下のModel(struct)を定義してやりとりします。
Model | 方向 | 説明 |
---|---|---|
Request | ViewController -> Interactor | Viewのイベントを通知する |
Response | Interactor -> Presenter | Interactorで処理した結果を通知する |
ViewModel | Presenter -> ViewController | 表示する内容を指示する |
struct ThreadList {
struct Filter {
struct Request {
var star: Bool
}
struct Response {
var star: Bool
}
struct ViewModel {
var filterLabel: String
}
}
}
Domain Layer
UseCaseはInteractorから依頼されたビジネスロジックを実行します。
必要に応じてUseCaseからはRepositoryを介してDataStoreへアクセスしDBアクセスやAPI通信行います。
ここでもそれぞれプロトコルでI/Fを定義して、そのプロトコルに依存した実装にします。
また、UseCaseはDBアクセスなどの結果取得したEntityを、TranslatorにてPresentation Layerで利用できるModelに変換してから渡すようにします。
Data Layer
実際にDBにアクセスしたりAPI通信をしたりするDataStoreを置きます。
ここでもプロトコルでI/Fを定義して、そのプロトコルに依存した実装にします。
RepositoryからはそのI/FでDataStoreを呼びます。
実際に開発してみて
ポジティブな印象
上記に見てきたように一つの画面を実装するにも登場するコンポーネントが多いですが、確かにそれぞに意味はあって、FatなViewControllerが生まれにくいな、というイメージがあります。
ネガティヴな印象
一方、一つのviewアクションを追加するにしても ViewController->Interactor->UseCase->Repository->DataStore->Repository->UseCase->Interactor->Presenter とまわってやっと画面更新が行えることになります。しかもPresentation Layerでは各コンポーネント間はModelを定義してメッセージやりとりをするようにするのと、Domain Layerで扱うEntityについてはPresentation Layerに渡す前にTranslatorで変換してやったりで結構大変です。
これまでMVVMでやっていたように、ViewControllerでViewイベントを受け取ったらViewModelにイベント通知して(API通信などを行った上で)その結果をうけてViewControllerが画面を更新する流れに比べると、だいぶ細分化されている感があって手数が多く開発のオーバーヘッドがかかる印象です。
テンプレートも作ったよ
ちなみに上記の反省もあるので、開発時に一気に必要なファイルを自動生成するようにXcodeテンプレートを作成したりしてます。(Clean Swiftで配布されていたテンプレートをカスタマイズしたものです)
プロジェクトディレクトリ直下にあるXcodeTemplatesディレクトリ内でmakeするとインストールでき、そのあとはXcodeからNew File ...
で作成したテンプレートを選択すると、関連したファイルを一気に作れます。
% cd XcodeTemplates/
% make install_templates
mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates
rm -fR ~/Library/Developer/Xcode/Templates/File\ Templates/CW\ Client\ iOS
cp -R CW\ Client\ iOS ~/Library/Developer/Xcode/Templates/File\ Templates
それぞれ自動生成されたファイルには、実装しておくべきプロトコルなどが事前に定義されているので、間違いなく実装していけるようになると思います。
例えば、ViewControlllerは以下のように生成されます。
//
// AppFeedbackViewController.swift
// cw-client-ios
//
// Created by Takayoshi Koshida on 2017/06/04.
// Copyright (c) 2017年 CrowdWorks Inc. All rights reserved.
//
// This file was generated by the CW Client iOS Templates.
//
import UIKit
protocol AppFeedbackPresenterViewInterface: class {
}
class AppFeedbackViewController: UIViewController {
var output: AppFeedbackViewInteractorInterface!
var router: AppFeedbackViewRouterInterface!
// MARK: - Private Methods
}
// MARK: - AppFeedbackPresenterViewInterface
extension AppFeedbackViewController: AppFeedbackPresenterViewInterface {
}
おわりに
今回のチームメンバーからの反発はClean Swiftやクリーンアーキテクチャによる問題ではなく、単に深く検証せずにClean Swiftをカスタマイズして拡張したためコンポーネントの数が増えてしまったためかと思います。が、ビジネスロジックが外部を知らないようにするには必要な手段だったようにも思うので難しいところです。
クリーンアーキテクチャを採用することは、コンポーネント間の責務もより明確になりコンポーネント間の依存も減らすことができ、Fat ViewContorollerのようなものが発生しなくなり、メンテナンス性の向上といった恩恵が得られると思いメリットは大きいと思います。
開発効率が落ちるから単純にMVVMに戻るなどではなく、開発効率も考慮しながらより現場の実情にあわせたアーキテクチャを検討していければと思いました。