Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
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
非同期jobをtransaction内で 呼ぶなよ!絶対に呼ぶなよ!
alstrocrack
0
620
NetworkXとGNNで学ぶグラフデータ分析入門〜複雑な関係性を解き明かすPythonの力〜
mhrtech
3
1.2k
私達はmodernize packageに夢を見るか feat. go/analysis, go/ast / Go Conference 2025
kaorumuta
2
520
iOSアプリの信頼性を向上させる取り組み/ios-app-improve-reliability
shino8rayu9
0
170
CSC509 Lecture 05
javiergs
PRO
0
300
CI_CD「健康診断」のススメ。現場でのボトルネック特定から、健康診断を通じた組織的な改善手法
teamlab
PRO
0
200
開発生産性を上げるための生成AI活用術
starfish719
3
360
エンジニアとして高みを目指す、 利益を生み出す設計の考え方 / design-for-profit
minodriven
24
12k
Advance Your Career with Open Source
ivargrimstad
0
450
育てるアーキテクチャ:戦い抜くPythonマイクロサービスの設計と進化戦略
fujidomoe
1
170
uniqueパッケージの内部実装を支えるweak pointerの話
magavel
0
960
Introducing ReActionView: A new ActionView-Compatible ERB Engine @ Kaigi on Rails 2025, Tokyo, Japan
marcoroth
3
970
Featured
See All Featured
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.1k
Practical Orchestrator
shlominoach
190
11k
The Cult of Friendly URLs
andyhume
79
6.6k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
5.6k
Context Engineering - Making Every Token Count
addyosmani
5
210
Large-scale JavaScript Application Architecture
addyosmani
514
110k
Scaling GitHub
holman
463
140k
How To Stay Up To Date on Web Technology
chriscoyier
791
250k
Building Adaptive Systems
keathley
43
2.8k
Principles of Awesome APIs and How to Build Them.
keavy
127
17k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
9
590
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
140
34k
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を作って単純作業を削減 興味のある方は個別に聞いてください!