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
55
プロジェクト開始以来継ぎ足しながら使ってきたソースを捨てた話
LT
Kazuki Nara
March 01, 2019
Tweet
Share
More Decks by Kazuki Nara
See All by Kazuki Nara
FlutterアプリでChromecastに接続する
kazukinr
2
750
Room with Kotlin
kazukinr
0
10
AOSPにパッチを送ってみた
kazukinr
0
17
作ろう! Android TVアプリ
kazukinr
0
22
Other Decks in Programming
See All in Programming
生成 AI の中身を覗いてみよう〜基礎から医療現場での応用まで〜
soh9834
2
770
PHPerKaigi 2024〜10年以上動いているレガシーなバッチシステムを Kubernetes(Amazon EKS) に移行する取り組み〜
tshinowpub
1
220
WinUI 3デモ - "CommunityToolkit.Mvvm"NuGetパッケージ編
andrewkeepcoding
0
140
Dockerで始めるAWS Lambda開発
stutkhd0709
14
2.5k
RubyVM を PHP で実装する 〜Hello World を出力するまで〜
memory1994
PRO
1
490
上手な探索的テストとその上達方法について
matsu802
4
660
15分間でふんわり理解するDocker @ Matsuriba MAX
ukwhatn
PRO
1
340
object-oriented-conference-2024
fuwasegu
6
1.9k
チームでモデリングを育てるうえで 考えたこと・気づいたこと / Cultivating Modeling in Teams: Thoughts and Insights
mackey0225
5
2.4k
TDDと今まで
kanayannet
0
140
Creating Retro-Style Photos Using Swift
ski
1
360
Deno に Web 標準 API を実装する / Implementing Web Standard API to Deno
petamoriken
0
350
Featured
See All Featured
Testing 201, or: Great Expectations
jmmastey
27
6.3k
The Mythical Team-Month
searls
214
42k
In The Pink: A Labor of Love
frogandcode
137
21k
Rails Girls Zürich Keynote
gr2m
91
13k
Code Review Best Practice
trishagee
54
15k
VelocityConf: Rendering Performance Case Studies
addyosmani
319
23k
Designing for humans not robots
tammielis
247
25k
Statistics for Hackers
jakevdp
789
220k
Gamification - CAS2011
davidbonilla
76
4.5k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
242
20k
Art, The Web, and Tiny UX
lynnandtonic
288
19k
StorybookのUI Testing Handbookを読んだ
zakiyama
10
4.5k
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を作って単純作業を削減 興味のある方は個別に聞いてください!