Slide 1

Slide 1 text

ちいさく始める レイヤード アーキテクチャ @tondol from Kyobashi.swift 俺コン Vol.1 / Day.1 – 2017.10.2 (Mon.) 1

Slide 2

Slide 2 text

このトークでは • 先⼈から受け継いだコードベースに、 レイヤードアーキテクチャを導⼊したときに 考えたことをお話します • 俺が考えた最強のアーキテクチャ!ってよりは、 実際にやるならこんぐらいのバランスも いいんでないの、くらいのテンション感で 紹介していきます 2

Slide 3

Slide 3 text

こんな話はしません • RxSwift の使い⽅ • サンプルコード中に⾃然に出てきます • 知らない⼈は、Observable は Promise 的な ものだと思って⾒てください • DDD (Domain-Driven-Design) の紹介や説明 • Tech Boosterさんの同⼈誌を読んだ程度なので、 そもそも語れるほど理解していません・・・ • レイヤードアーキテクチャetc.は 「DDD を実装に落とし込む際に便利なテク」 くらいの捉え⽅をしています 3

Slide 4

Slide 4 text

About Me • リクルートマーケティングパートナーズ • iOSエンジニア (ときどきJava/Kotlinも) • Kyobashi.swift 4

Slide 5

Slide 5 text

Kyobashi.swift • リクルートマーケティングパートナーズの オフィスが東京・京橋にあります • オフィス内のスペースで Swift/iOS の勉強会を 不定期で開催しています • AKIBA.swift (クラスメソッドさん) や Otemachi.swift (⽇経新聞さん) とコラボも! • 最新情報は • https://kyobashi-swift.connpass.com/ 5

Slide 6

Slide 6 text

ここから本題 6

Slide 7

Slide 7 text

巷にあふれるアーキテクチャ の話題・・・ MVC MVP MVVM Flux Redux Clean Architecture DDD Layered Architecture Fat ViewController 絶対に 殺すマン 7

Slide 8

Slide 8 text

iOSDCのCfPでもこんなに! アーキテクチャを移⾏する by @d_date Layered Architecture x RxSwiftを活⽤した 適切なエラーハンドリング by @nonchalant0303 Redux+Rxを活⽤したアプリアーキテクチャ by @susieyy MVC→MVP→MVVM→Fluxの 実装の違いを⽐較してみる by @marty_suzuki 節⼦、それViewControllerやない...、 FatViewControllerや...。 by @ktanaka117 漸進的にViewControllerの肥⼤化を防ぐ by @kazuhiro494949 レッドオーシャン感 8

Slide 9

Slide 9 text

Clean Architecture Presenter View UseCase Translator Model DataStore Entity Presentation Domain Infrastructure Repository 9

Slide 10

Slide 10 text

1画⾯に必要なクラス数 FooViewController FooPresenter FooUseCase FooTranslator FooModel FooRepository FooDataStore FooEntity Presentation/Domain Domain/Infrastructure 8コンポーネント! 10 5分⽬安

Slide 11

Slide 11 text

本当にこんなに必要? • そもそも何のためにやるんだっけ? • 新規の開発ならともかく、 既存アプリのリファクタリングのために、 たくさんのクラスを実装するのは⾟い・・・ • メンテナンス性と開発速度のバランス 11

Slide 12

Slide 12 text

ちいさく始める レイヤード アーキテクチャ ちいさく始める: 必要最⼩限のコードで「欲しいもの」を 実現するアーキテクチャを考える 12

Slide 13

Slide 13 text

リファクタ前の状況 ⼤量のソースに分割されているが 密結合しまくりで 単体テストが書けないクラス群 通信も UI 更新も全部やる、 絵に描いたような FatViewController 機能追加のついでに モダンアーキテクチャを 導⼊しよう! 設 計 13

Slide 14

Slide 14 text

欲しいもの • テスト可能な実装 • UIViewController や UIView のテストは厳しい • これらのコンポーネントに依存しないクラスに アプリのロジックを書きたい • 依存するコンポーネントは外部から注⼊可能に • ⼊れ替え可能なインフラレイヤー • 永続化する先は API かもしれないし、 ローカル DB かもしれない • テストの観点からも⼊れ替え可能であるべき 14

Slide 15

Slide 15 text

割り切りポイント (1) • Model と Entity を共通化 • 表⽰⽤に必要なプロパティは Extension にすれば、 ⼀旦実装を分離できる • Translator などが不要になり、 ボイラープレートコードを減らすことができる Presenter View UseCase Translator DataStore Presentation Domain Infrastructure Repository Model 共通化 15

Slide 16

Slide 16 text

割り切りポイント (2) • Repository を省略 • DataStore を動的に切り替えたりしないのなら、 UseCase に直接 DataStore を注⼊すれば⼗分 Presenter View UseCase DataStore Presentation Domain Infrastructure Model Repository 省略 16

Slide 17

Slide 17 text

割り切りポイント (3) • RxSwift の導⼊ • 「ちいさく」はないかもしれないが、 中⻑期的に⾒れば開発速度の向上に寄与するはず Presenter View UseCase DataStore Presentation Domain Infrastructure Model この辺のやりとりに RxSwift を導⼊ 17

Slide 18

Slide 18 text

こうなりました Presenter View Controller UseCase DataStore Presentation Domain Infrastructure Model 18

Slide 19

Slide 19 text

サンプルアプリ • ニュースの⼀覧画⾯と詳細画⾯ • ⼀覧画⾯と詳細画⾯に Like ボタン 19

Slide 20

Slide 20 text

Presenter の実装 class NewsListPresenter: NSObject { let newsList = Variable([News]()) let showDetailViewControllerMessage = PublishSubject() private let useCase: NewsUseCase private let bag = DisposeBag() init(useCase: NewsUseCase) { self.useCase = useCase } extension NewsListPresenter: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.newsList.value.count } 最後の [News] を保持 更新時にイベント発⽣ 詳細画⾯の 表⽰イベント 依存 UseCase を イニシャライザから 注⼊ [News] を参照し Cell を提供する UITableViewDataSource 20 10分⽬安

Slide 21

Slide 21 text

UI と Presenter のバインド class NewsListViewController: UIViewController { private var presenter: NewsListPresenter! ... private func setupBindings() { self.presenter.newsList .asDriver() .drive(onNext: { [unowned self] _ -> Void in self.tableView.reloadData() }) .disposed(by: self.bag) self.presenter.showDetailViewControllerMessage .asDriver(onErrorDriveWith: Driver.empty()) .drive(onNext: { [unowned self] id -> Void in self.showDetailViewController(id: id) }) .disposed(by: self.bag) } 新しい [News] が来たら reloadData を発⾏ イベントが来たら 詳細画⾯を push 21

Slide 22

Slide 22 text

初回ロードの流れ setupUI() viewDidLoad() fetchNewsList() fetchNewsList() Variable<[News]> NewsListPresenter: UITableViewDataSource NewsListViewController: UIViewController, UITableViewDelegate NewsUseCaseImpl: NewsUseCase NewsStoreImpl: NewsStore tableView. reloadData() Update Observable<[News]> Observable<[News]> onNext ⾮同期処理の戻り値は Observable 新しい [News] が来たら reloadData を発⾏ 22

Slide 23

Slide 23 text

セルタップ時の流れ selectNews() didSelectRowAt(…) PublishSubject NewsListPresenter: UITableViewDataSource NewsListViewController: UIViewController, UITableViewDelegate NewsUseCaseImpl: NewsUseCase NewsStoreImpl: NewsStore showDetail ViewController(id: Int) onNext onNext イベントが来たら 詳細画⾯を push 23

Slide 24

Slide 24 text

Presenter のテスト • モックした DataStore を使い、 Presenter – UseCase を⼀気にテストする例 NewsListPresenter NewsUseCaseImpl NewsStoreMock Input (メソッドコール) Output (戻り値やイベント列) 24

Slide 25

Slide 25 text

⾮同期処理のテスト (1) class NewsListPresenterSpec: QuickSpec { override func spec() { var presenter: NewsListPresenter! var bag: DisposeBag! ... _ = try! presenter.setupUI().toBlocking().first() let newsList = try! presenter.newsList.asObservable().toBlocking().first() // setupUI の完了後は News が 3 個表⽰されており、 // 先頭の News は id=1 expect(newsList).to(haveCount(3)) expect(newsList?[0].id) == 1 setupUI の完了を待機 更新後の Variable を取得し Assertion 25

Slide 26

Slide 26 text

⾮同期処理のテスト (2) class NewsListPresenterSpec: QuickSpec { override func spec() { var presenter: NewsListPresenter! var bag: DisposeBag! ... let scheduler = TestScheduler(initialClock: 0) let observer = scheduler.createObserver(Int.self) presenter.showDetailViewControllerMessage .subscribe(observer).disposed(by: bag) _ = try! presenter.setupUI().toBlocking().first() presenter.selectNews(indexPath: IndexPath(row: 0, section: 0)) // selectNews に先頭の IndexPath を渡すと、id=1 の News を詳細表⽰ expect(observer.events).to(haveCount(1)) expect(observer.events[0].value) == Event.next(1) テスト⽤の Observer を⽤意 得られたイベント列を Assertion 26

Slide 27

Slide 27 text

欲しいもの再考 • テスト可能な実装 • Presenter だけのテストもできるし、 UseCase や Store を含む横着テストもできる • 依存コンポーネントはイニシャライザで注⼊ • ⼊れ替え可能なインフラレイヤー • UseCase と Store は疎結合なので、 必要に応じてデータレイヤーをモックしたり、 実装を差し替えたりできる • インフラレイヤーの内部実装を変更しても、 ドメインレイヤーには影響しない 27

Slide 28

Slide 28 text

悩みどころ • UseCase の責務が Presenter と DataStore 間の 仲介のみで、あんまり仕事をしていない気が • API クライアント的なアプリだと特にこうなりがち • ViewController 周りの調整を⼀⼿に引き受ける Presenter が肥⼤化しがち • ViewController 同⼠のイベント送受信どうやる? • 現状は ViewController に⽣やした PublishSubject を 遷移元の ViewController で subscribe している • Redux のようにアプリ全体で ひとつのイベントストリームを共有すれば解決? 28

Slide 29

Slide 29 text

まとめ • レイヤードアーキテクチャを⼀部導⼊した結果、 実装⼯数を抑えながら欲しいものを得られた • とはいえ、やはり銀の弾丸は存在しない • 各プロダクトで必要としているものを検討し、 アーキテクチャも取捨選択するのが⼤事 • ソースコードは下記リポジトリから読めます! • https://github.com/tondol/LayeredArchSampl e 29

Slide 30

Slide 30 text

参考資料 • RxSwift? いやClean Swiftっしょ • https://qiita.com/takahia/items/67b9e122968 2127d924e • まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて • https://qiita.com/koutalou/items/07a4f9cf51a 2d13e4cdc • iOSDesignPatternSamples • https://github.com/marty- suzuki/iOSDesignPatternSamples 30 15分⽬安