Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
プロジェクト開始以来継ぎ足しながら使ってきたソースを捨てた話
Search
Kazuki Nara
March 01, 2019
Programming
0
98
プロジェクト開始以来継ぎ足しながら使ってきたソースを捨てた話
LT
Kazuki Nara
March 01, 2019
Tweet
Share
More Decks by Kazuki Nara
See All by Kazuki Nara
FlutterアプリでChromecastに接続する
kazukinr
2
1.1k
Room with Kotlin
kazukinr
0
28
AOSPにパッチを送ってみた
kazukinr
0
43
作ろう! Android TVアプリ
kazukinr
0
47
Other Decks in Programming
See All in Programming
Why Kotlin? 電子カルテを Kotlin で開発する理由 / Why Kotlin? at Henry
agatan
2
6.9k
ローターアクトEクラブ アメリカンナイト:川端 柚菜 氏(Japan O.K. ローターアクトEクラブ 会長):2720 Japan O.K. ロータリーEクラブ2025年12月1日卓話
2720japanoke
0
720
令和最新版Android Studioで化石デバイス向けアプリを作る
arkw
0
380
堅牢なフロントエンドテスト基盤を構築するために行った取り組み
shogo4131
8
2.2k
TypeScriptで設計する 堅牢さとUXを両立した非同期ワークフローの実現
moeka__c
6
3k
SwiftUIで本格音ゲー実装してみた
hypebeans
0
100
Context is King? 〜Verifiability時代とコンテキスト設計 / Beyond "Context is King"
rkaga
6
950
Tinkerbellから学ぶ、Podで DHCPをリッスンする手法
tomokon
0
120
ソフトウェア設計の課題・原則・実践技法
masuda220
PRO
26
22k
CSC305 Lecture 17
javiergs
PRO
0
340
関数実行の裏側では何が起きているのか?
minop1205
1
670
30分でDoctrineの仕組みと使い方を完全にマスターする / phpconkagawa 2025 Doctrine
ttskch
3
790
Featured
See All Featured
The Cost Of JavaScript in 2023
addyosmani
55
9.3k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
132
19k
VelocityConf: Rendering Performance Case Studies
addyosmani
333
24k
Six Lessons from altMBA
skipperchong
29
4.1k
Designing for humans not robots
tammielis
254
26k
Balancing Empowerment & Direction
lara
5
790
Documentation Writing (for coders)
carmenintech
76
5.2k
Learning to Love Humans: Emotional Interface Design
aarron
274
41k
Typedesign – Prime Four
hannesfritz
42
2.9k
Principles of Awesome APIs and How to Build Them.
keavy
127
17k
Unsuck your backbone
ammeep
671
58k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
37
2.6k
Transcript
プロジェクト開始以来 継ぎ足しながら使ってきた 秘伝のソースを捨てた話 Kazuki Nara @ AWA Co., Ltd.
About AWA 定額制音楽ストリーミングサービス 2014年12月1日 AWA株式会社設立 2015年5月27日 AWAリリース 2019年1月24日 フルリニューアルのV2リリース 詳しくはこちら https://awa.fm
長期運用に起因する問題(プロダクト) - 機能追加の連続により高エントロピーな画面構成 - ユーザーが何をすればいいのかわかりづらくなっている - 追加されたがほぼ使われていない機能・画面の存在 - リリース時点では洗練されていたが、3年を経て陳腐化したUI
長期運用に起因する問題(技術的負債) - 機能追加のスピードを重視したため追加される場当たり的な実装(FIXME) - 実装時期のトレンドによってまちまちな設計方針 - 機能廃止に対応できず、バージョンを上げられないライブラリ - 職人芸ハックによる可読性の低下。新規メンバーが理解しがたいコード e.g.
RealmのオレオレRx実装、RxJava1 <> RxJava2変換の連続 - 肥大化するView層 - 増大する機能追加・更新時の実装コスト - 疲弊するエンジニア
フルリニューアルの決断 - プロダクトの全面刷新が必要という判断 - アーキテクチャの見直しを行いたいエンジニアの要望
フルリニューアルの決断 - プロダクトの全面刷新が必要という判断 - アーキテクチャの見直しを行いたいエンジニアの要望 → 全面刷新するなら中身から作り替えよう、という判断
アーキテクチャ設計に影響する前提条件 差分API - クライアントはAPIの結果をキャッシュする - APIはクライアントのキャッシュと最新状態との差分を返す - キャッシュはオフライン時やAPI実行完了前の表示に利用される Realmの制約 -
クライアントアプリのデータキャッシュとしてRealmを採用している - Realmにアクセスする場合は同一Threadでopen-closeする必要がある - Realmから取得したObjectは同一Threadからしかアクセスできない
AWA V1(リリース時) View ApiClient DbClient Realm API Model
AWA V1(終盤) View Use Case ApiClient DbClient Realm SQLite API
Model Prefs Cache ViewModel
構造から見てとれる問題点 - レイヤー構造になっていない - Viewから呼び出すレイヤーがViewMode / Use Case / Modelとバラバラ
- あらゆる層がPrefsを参照している = Contextを参照している
プレイリスト情報を表示する(V1) - Realmからキャッシュを取得して表示する(main thread) - APIをコールして最新データを取得する - Realmのキャッシュを更新する(I/O thread) -
Realmの更新がmain threadに適用されるまで待つ - Realmから更新後のデータを取得して表示する(main thread)
プレイリスト情報を表示する(V1) PlaylistModel.kt fun fetchPlaylistById(playlistId: String): Single<String> = playlistApiClient.getPlaylistDetail(playlistId) .subscribeOn(Schedulers.io()) .flatMapCompletable
{ playlistDbClient.save(it) } .andThen(Single.just(playlistId)) fun getPlaylistById(playlistId: String): Maybe<Playlist> = Maybe.fromCallable { playlistDbClient.getById(playlistId).firstOrNull() }
プレイリスト情報を表示する(V1) PlaylistDetailActivity.kt fun onCreate(savedInstanceState: Bundle?) { playlistModel.getPlaylistById(playlistId) .subscribe { //
Set data to View. } playlistModel.fetchPlaylistById(playlistId) .observeOn(AndroidSchedulers.mainThread()) .waitForNextLooperEvent() .flatMapMaybe { playlistModel.getPlaylistById(it) } .subscribe { // Set data to View. } }
実装面での問題点 - 画面を表示するだけなのに手続きが多い(RxJava未習熟時の実装) - 更新処理が必要な場合はさらに手続きが増える - Viewの処理が多い - fetchとgetが紛らわしい。fetchなのに実際はDB更新してる初見殺し -
observeOnでthreadを切り替えるとクラッシュするStream
DataStore AWA V2 View ApiClient Realm API ViewModel Repository Data
Command Data Query SQLite Prefs Cache Use Case (Command) Use Case (Query)
何が変わったか - レイヤー構造を徹底し、1階層下のレイヤー以外の参照を禁止した - Contextを参照できるのはView / Repository / API のみ
- 同一レイヤー内でもCommand / Queryを完全に分離した(CQRS) - AWA特有の要件(差分APIによるキャッシュ更新)に対応するため、一般的な Repositoryと異なるDataCommand / DataQuery層を定義した
プレイリスト情報を表示する(V2) - Realmを変更監視して流れてきたデータを画面に表示する(main thread) - APIをコールして最新データを取得する - Realmのキャッシュを更新する(I/O thread)
プレイリスト情報を表示する(V2) PlaylistDataCommand.kt fun syncById(playlistId: String): Completable = playlistApiClient.getPlaylistDetail(playlistId) .subscribeOn(Schedulers.io()) .flatMapCompletable
{ playlistDbClient.save(it) } PlaylistDataQuery.kt fun getById(playlistId: String): RealmResults<Playlist> = playlistDbClient.getById(playlistId)
プレイリスト情報を表示する(V2) SyncPlaylistById.kt operator fun invoke(playlistId: String): Completable = playlistDataCommand.syncById(playlistId) ObservePlaylistById.kt
operator fun invoke(playlistId: String): Flowable<RealmResults<Playlist>> = playlistDataQuery.getById(playlistId) .asFlowable()
プレイリスト情報を表示する(V2) PlaylistDetailViewModel.kt val playlist = ObservableField<Playlist>() fun onStart() { observePlaylistById(playlistId)
.subscribe { it.firstOrNull()?.also { playlist.set(it) } } syncPlaylistById(playlistId) .subscribe() }
問題は改善されたか - 画面表示はsyncとobserveという2処理を独立して実行するだけになった - 更新処理はDBを更新するだけ(observeしているので画面は自動更新) - Viewからロジックを引き離すためViewModelを定義した - ViewModelが肥大化しないようにdomain層でユースケースを定義した -
APIコール→キャッシュ更新処理にsyncという名前を与えた - Realmを検索する処理はRealmResultsの型のままView層に戻すようにした - Contextを参照する層を限定的にしたため、ユースケース、ロジック層ではAndroid の制約を意識する必要がなくなった - 同様に、ユースケース、ロジック層ではモックを使用した高速テストが可能になった
それ以外にもこんなことやったよ - フルKotlin - Kotlin Extensionsの積極活用 - AndroidX対応 - レイアウトは原則ConstraintLayout
- AAC(Lifecycle、LiveData、Room、Paging)の適用 - 大量生産されるViewコンポーネントはtemplateを作って単純作業を削減 興味のある方は個別に聞いてください!