Lock in $30 Savings on PRO—Offer Ends Soon! ⏳
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ちいさく始めるレイヤードアーキテクチャ
Search
tondol
October 02, 2017
Programming
7
2k
ちいさく始めるレイヤードアーキテクチャ
俺コン Vol.1 / Day.1 Track C で発表した資料になります。
#orecon_ios #kyobashiswift
tondol
October 02, 2017
Tweet
Share
More Decks by tondol
See All by tondol
RxSwift 3.3.0: Observable のフレンズが増えました!!
tondol
2
2.7k
Amazon Cloud Driveのご紹介
tondol
0
540
自家製オタクソリューションの紹介
tondol
1
520
ドはDockerのド
tondol
1
2.8k
Other Decks in Programming
See All in Programming
Claude Codeの「Compacting Conversation」を体感50%減! CLAUDE.md + 8 Skills で挑むコンテキスト管理術
kmurahama
1
630
クラウドに依存しないS3を使った開発術
simesaba80
0
160
Findy AI+の開発、運用におけるMCP活用事例
starfish719
0
1.7k
脳の「省エネモード」をデバッグする ~System 1(直感)と System 2(論理)の切り替え~
panda728
PRO
0
120
Rubyで鍛える仕組み化プロヂュース力
muryoimpl
0
160
AIコーディングエージェント(Gemini)
kondai24
0
270
ELYZA_Findy AI Engineering Summit登壇資料_AIコーディング時代に「ちゃんと」やること_toB LLMプロダクト開発舞台裏_20251216
elyza
2
590
AI Agent Tool のためのバックエンドアーキテクチャを考える #encraft
izumin5210
3
1.1k
チームをチームにするEM
hitode909
0
370
Canon EOS R50 V と R5 Mark II 購入でみえてきた最近のデジイチ VR180 事情、そして VR180 静止画に活路を見出すまで
karad
0
140
著者と進める!『AIと個人開発したくなったらまずCursorで要件定義だ!』
yasunacoffee
0
160
令和最新版Android Studioで化石デバイス向けアプリを作る
arkw
0
440
Featured
See All Featured
Taking LLMs out of the black box: A practical guide to human-in-the-loop distillation
inesmontani
PRO
3
1.9k
Lightning Talk: Beautiful Slides for Beginners
inesmontani
PRO
1
410
Designing for Timeless Needs
cassininazir
0
93
We Have a Design System, Now What?
morganepeng
54
7.9k
Fashionably flexible responsive web design (full day workshop)
malarkey
407
66k
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
9
1k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
286
14k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.1k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
GraphQLの誤解/rethinking-graphql
sonatard
73
11k
How To Stay Up To Date on Web Technology
chriscoyier
791
250k
Skip the Path - Find Your Career Trail
mkilby
0
27
Transcript
ちいさく始める レイヤード アーキテクチャ @tondol from Kyobashi.swift 俺コン Vol.1 / Day.1
– 2017.10.2 (Mon.) 1
このトークでは • 先⼈から受け継いだコードベースに、 レイヤードアーキテクチャを導⼊したときに 考えたことをお話します • 俺が考えた最強のアーキテクチャ!ってよりは、 実際にやるならこんぐらいのバランスも いいんでないの、くらいのテンション感で 紹介していきます
2
こんな話はしません • RxSwift の使い⽅ • サンプルコード中に⾃然に出てきます • 知らない⼈は、Observable<T> は Promise<T>
的な ものだと思って⾒てください • DDD (Domain-Driven-Design) の紹介や説明 • Tech Boosterさんの同⼈誌を読んだ程度なので、 そもそも語れるほど理解していません・・・ • レイヤードアーキテクチャetc.は 「DDD を実装に落とし込む際に便利なテク」 くらいの捉え⽅をしています 3
About Me • リクルートマーケティングパートナーズ • iOSエンジニア (ときどきJava/Kotlinも) • Kyobashi.swift 4
Kyobashi.swift • リクルートマーケティングパートナーズの オフィスが東京・京橋にあります • オフィス内のスペースで Swift/iOS の勉強会を 不定期で開催しています •
AKIBA.swift (クラスメソッドさん) や Otemachi.swift (⽇経新聞さん) とコラボも! • 最新情報は • https://kyobashi-swift.connpass.com/ 5
ここから本題 6
巷にあふれるアーキテクチャ の話題・・・ MVC MVP MVVM Flux Redux Clean Architecture DDD
Layered Architecture Fat ViewController 絶対に 殺すマン 7
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
Clean Architecture Presenter View UseCase Translator Model DataStore Entity Presentation
Domain Infrastructure Repository 9
1画⾯に必要なクラス数 FooViewController FooPresenter FooUseCase FooTranslator FooModel FooRepository FooDataStore FooEntity Presentation/Domain
Domain/Infrastructure 8コンポーネント! 10 5分⽬安
本当にこんなに必要? • そもそも何のためにやるんだっけ? • 新規の開発ならともかく、 既存アプリのリファクタリングのために、 たくさんのクラスを実装するのは⾟い・・・ • メンテナンス性と開発速度のバランス 11
ちいさく始める レイヤード アーキテクチャ ちいさく始める: 必要最⼩限のコードで「欲しいもの」を 実現するアーキテクチャを考える 12
リファクタ前の状況 ⼤量のソースに分割されているが 密結合しまくりで 単体テストが書けないクラス群 通信も UI 更新も全部やる、 絵に描いたような FatViewController 機能追加のついでに
モダンアーキテクチャを 導⼊しよう! 設 計 13
欲しいもの • テスト可能な実装 • UIViewController や UIView のテストは厳しい • これらのコンポーネントに依存しないクラスに
アプリのロジックを書きたい • 依存するコンポーネントは外部から注⼊可能に • ⼊れ替え可能なインフラレイヤー • 永続化する先は API かもしれないし、 ローカル DB かもしれない • テストの観点からも⼊れ替え可能であるべき 14
割り切りポイント (1) • Model と Entity を共通化 • 表⽰⽤に必要なプロパティは Extension
にすれば、 ⼀旦実装を分離できる • Translator などが不要になり、 ボイラープレートコードを減らすことができる Presenter View UseCase Translator DataStore Presentation Domain Infrastructure Repository Model 共通化 15
割り切りポイント (2) • Repository を省略 • DataStore を動的に切り替えたりしないのなら、 UseCase に直接
DataStore を注⼊すれば⼗分 Presenter View UseCase DataStore Presentation Domain Infrastructure Model Repository 省略 16
割り切りポイント (3) • RxSwift の導⼊ • 「ちいさく」はないかもしれないが、 中⻑期的に⾒れば開発速度の向上に寄与するはず Presenter View
UseCase DataStore Presentation Domain Infrastructure Model この辺のやりとりに RxSwift を導⼊ 17
こうなりました Presenter View Controller UseCase DataStore Presentation Domain Infrastructure Model
18
サンプルアプリ • ニュースの⼀覧画⾯と詳細画⾯ • ⼀覧画⾯と詳細画⾯に Like ボタン 19
Presenter の実装 class NewsListPresenter: NSObject { let newsList = Variable([News]())
let showDetailViewControllerMessage = PublishSubject<Int>() 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分⽬安
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
初回ロードの流れ setupUI() viewDidLoad() fetchNewsList() fetchNewsList() Variable<[News]> NewsListPresenter: UITableViewDataSource NewsListViewController: UIViewController,
UITableViewDelegate NewsUseCaseImpl: NewsUseCase NewsStoreImpl: NewsStore tableView. reloadData() Update Observable<[News]> Observable<[News]> onNext ⾮同期処理の戻り値は Observable<T> 新しい [News] が来たら reloadData を発⾏ 22
セルタップ時の流れ selectNews() didSelectRowAt(…) PublishSubject <Int> NewsListPresenter: UITableViewDataSource NewsListViewController: UIViewController, UITableViewDelegate
NewsUseCaseImpl: NewsUseCase NewsStoreImpl: NewsStore showDetail ViewController(id: Int) onNext onNext イベントが来たら 詳細画⾯を push 23
Presenter のテスト • モックした DataStore を使い、 Presenter – UseCase を⼀気にテストする例
NewsListPresenter NewsUseCaseImpl NewsStoreMock Input (メソッドコール) Output (戻り値やイベント列) 24
⾮同期処理のテスト (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<T> を取得し Assertion 25
⾮同期処理のテスト (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
欲しいもの再考 • テスト可能な実装 • Presenter だけのテストもできるし、 UseCase や Store を含む横着テストもできる
• 依存コンポーネントはイニシャライザで注⼊ • ⼊れ替え可能なインフラレイヤー • UseCase と Store は疎結合なので、 必要に応じてデータレイヤーをモックしたり、 実装を差し替えたりできる • インフラレイヤーの内部実装を変更しても、 ドメインレイヤーには影響しない 27
悩みどころ • UseCase の責務が Presenter と DataStore 間の 仲介のみで、あんまり仕事をしていない気が •
API クライアント的なアプリだと特にこうなりがち • ViewController 周りの調整を⼀⼿に引き受ける Presenter が肥⼤化しがち • ViewController 同⼠のイベント送受信どうやる? • 現状は ViewController に⽣やした PublishSubject<T> を 遷移元の ViewController で subscribe している • Redux のようにアプリ全体で ひとつのイベントストリームを共有すれば解決? 28
まとめ • レイヤードアーキテクチャを⼀部導⼊した結果、 実装⼯数を抑えながら欲しいものを得られた • とはいえ、やはり銀の弾丸は存在しない • 各プロダクトで必要としているものを検討し、 アーキテクチャも取捨選択するのが⼤事 •
ソースコードは下記リポジトリから読めます! • https://github.com/tondol/LayeredArchSampl e 29
参考資料 • 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分⽬安