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
97
プロジェクト開始以来継ぎ足しながら使ってきたソースを捨てた話
LT
Kazuki Nara
March 01, 2019
Tweet
Share
More Decks by Kazuki Nara
See All by Kazuki Nara
FlutterアプリでChromecastに接続する
kazukinr
2
1k
Room with Kotlin
kazukinr
0
27
AOSPにパッチを送ってみた
kazukinr
0
41
作ろう! Android TVアプリ
kazukinr
0
44
Other Decks in Programming
See All in Programming
管你要 trace 什麼、bpftrace 用下去就對了 — COSCUP 2025
shunghsiyu
0
390
プロダクトという一杯を作る - プロダクトチームが味の責任を持つまでの煮込み奮闘記
hiliteeternal
0
450
「リーダーは意思決定する人」って本当?~ 学びを現場で活かす、リーダー4ヶ月目の試行錯誤 ~
marina1017
0
210
Go製CLIツールをnpmで配布するには
syumai
2
1.2k
#QiitaBash TDDで(自分の)開発がどう変わったか
ryosukedtomita
1
360
なぜあなたのオブザーバビリティ導入は頓挫するのか
ryota_hnk
5
590
Understanding Kotlin Multiplatform
l2hyunwoo
0
250
CEDEC2025 長期運営ゲームをあと10年続けるための0から始める自動テスト ~4000項目を50%自動化し、月1→毎日実行にした3年間~
akatsukigames_tech
0
120
技術的負債で信頼性が限界だったWordPress運用をShifterで完全復活させた話
rvirus0817
1
1.5k
実践!App Intents対応
yuukiw00w
1
240
構文解析器入門
ydah
7
2.1k
Scale out your Claude Code ~自社専用Agentで10xする開発プロセス~
yukukotani
9
1.9k
Featured
See All Featured
KATA
mclloyd
32
14k
Agile that works and the tools we love
rasmusluckow
329
21k
Designing for humans not robots
tammielis
253
25k
The Power of CSS Pseudo Elements
geoffreycrofte
77
5.9k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
656
60k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.5k
Rebuilding a faster, lazier Slack
samanthasiow
83
9.1k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Balancing Empowerment & Direction
lara
1
540
Imperfection Machines: The Place of Print at Facebook
scottboms
267
13k
Git: the NoSQL Database
bkeepers
PRO
431
65k
Building Adaptive Systems
keathley
43
2.7k
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を作って単純作業を削減 興味のある方は個別に聞いてください!