Upgrade to Pro — share decks privately, control downloads, hide ads and more …

拡大期を迎えたプロダクトに起きたこと - Android編

拡大期を迎えたプロダクトに起きたこと - Android編

私の携わるプロダクト「abceed」が拡大期を迎えたとき、人の増加に伴う設計変更の必要性に遭遇したときの話です。
なぜ再設計が必要だったのか、そしてなぜFluxを選択し、同実装したのかを解説します。

Masaya Yashiro

April 20, 2022
Tweet

More Decks by Masaya Yashiro

Other Decks in Technology

Transcript

  1. 拡大期を迎えたプロダクトに
    起きたこと - Android編
    2022/04/20
    @大人気教育アプリ 3社 のモバイルアプリ開発秘話
    @yashims85

    View Slide

  2. 皆様はどのようなチームで開発を
    行っていますか?

    View Slide

  3. 私がGlobeeに入社したのは
    abceedの売上が安定し、規模が
    拡大していく最中のことです。

    View Slide

  4. 拡大期を迎えたことで、様々な事情
    から、再設計を行う必要が出てきた
    タイミングでした。

    View Slide

  5. Who is me?
    ● 本名: 屋代昌也
    ● 年齢: 36歳くらい
    ● @ : @yashims85 (やしむす)
    ● ❤ : スノボ、アニメ、バイク、お酒
    ● 2020年末頃にGlobeeに入社し、
    Android開発をメインで担当

    View Slide

  6. View Slide

  7. 何を作ってるの?
    どうやって発展してきたの?

    View Slide

  8. 作っているもの

    View Slide

  9. abceedとは
    クイズ、シャドーイング、スラッシュリーディングなど
    約20種類の豊富なアプリ学習に対応

    View Slide

  10. 累計ユーザー数
    220万人突破
    おかげさまで

    View Slide

  11. なぜ設計を変える必要が出てきた

    View Slide

  12. なぜ設計を変える必要があったか
    ● つくった人がずっとコードを追っているので、未知のコー
    ドが存在しなかった
    ● 可読性、メンテのしやすさよりも、最短でリリースし収益
    化することが優先された
    ● ミニマムから始め機能を追加していったために、スー
    パーなんでもクラスが生まれていた

    View Slide

  13. あとから来た人が沼にハマりやすい
    環境になっていた。

    View Slide

  14. 再設計をするときの注意点

    View Slide

  15. 設計を変えるという事が
    今までの否定になってはいけない
    ● 今あるものは作ったときのリソースの制約や、環境のも
    とで全力で作った結果
    ● その当時の設計で拡張できる限界。設計寿命に達した
    ● だから我々は再設計を行う

    View Slide

  16. 既存コードが悪いわけではなく、プロ
    ダクトに必要とされる価値や優先順
    位が変化してきた

    View Slide

  17. 実際に変えた設計の一例をご紹介

    View Slide

  18. Fluxアーキテクチャの採用

    View Slide

  19. Flux
    ● ソフトウェアアーキテクチャの一つ
    ● MVCやMVVM、Reduxなどと並行概念
    ● Redux、Flutter等はFluxの実装系
    View Dispatcher
    Store
    Action
    send
    commit
    observe
    create

    View Slide

  20. Flux
    ● コードの制約度が高い
    ● 大規模なイベント駆動型ソフトウェア向け
    ● ボイラープレートが多くなりがち
    View Dispatcher
    Store
    Action
    send
    commit
    observe
    create
    単方向Pub/Sub
    参照の向き
    呼び出しの順番

    View Slide

  21. なぜFlux?
    ● iOS, Androidのエンジニアは相互に補助したり、
    p-rを出したり
    しながら開発を行っている
    ● そのため何かしらの共通化を行いたい要求がある
    ● 制約性の高いアーキテクチャで設計を共通化させる。
    ○ 制約度が高いと自然と似たようなコードになる
    ○ それぞれのOS設計上で実装されるので、無理無くできる
    ● 多人数大規模開発に向いている
    ● 完全分業ならAndroid標準のMVVMにしたほうが無難な場合

    View Slide

  22. AndroidでFlux
    Fluxは設計概念なので、実装は無数に存在する。
    今回は独自で実装したので、それのご紹介です。

    View Slide

  23. AndroidでFlux
    ● FluxのViewはFragmentに相当
    ● Dispatcherは単体だと都合が悪いので、Dispatcherと
    ビジネスロジックのUseCaseを分離
    Fragment
    (Presenter)
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create

    View Slide

  24. Presenter
    AndroidでFlux
    ● FragmentがもりもりになるのでPresenterに分離
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create
    Fragment
    call
    push
    ViewModel
    observe

    View Slide

  25. Presenter
    AndroidでFlux
    ● プレゼンテーション層とビジネスロジック層の分離ができた
    ● ビジネスロジックはプレゼンテーションへの参照を持たない
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create
    Fragment
    call
    push
    ViewModel
    observe
    プレゼンテーション ビジネスロジック

    View Slide

  26. 実装
    ● Flux Store: reactivex.Observable
    ● Presenter: FragmentとBLのつなぎ処理
    ● Flux Action: メッセージング用data class
    ● Flux Dispatcher: kotlinx.coroutine
    ● UseCase: Dispatcherから叩かれ、Storeを更新する処

    View Slide

  27. Presenter
    AndroidでFluxのStore
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create
    Fragment
    call
    push
    ViewModel
    observe

    View Slide

  28. Flux Store
    class UserDetailStore {
    val error = RxProperty()
    val userInfo: RxProperty = RxProperty()
    fun commitError(exception: Exception) {
    this.error.value = exception
    }
    fun commitUserInfo
    (info: UserInfo) {
    this.userInfo.value = tabList
    }
    }
    AndroidのLiveDataと同じようなものと思っ
    て問題ありません。
    実装の実態はRx.BehaviourSubjectです。

    View Slide

  29. Presenter
    AndroidでFluのView
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create
    Fragment
    call
    push
    ViewModel
    observe

    View Slide

  30. Flux View
    (Presenter)
    class UserDetailPresenter(
    private val store: UserDetailStore
    ,
    private val dispatcher: FluxDispatcher
    ,
    ) {
    private val disposables = CompositeDisposable()
    private var vm: UserDetailViewModel? = null
    // onViewCreated
    時などに呼ばれる
    fun initialize(vm: UserDetailVideoViewModel) {
    this.vm = vm
    store.error.asObservable()
    .subscribe { e: Exception -> vm.postError(e) }
    .addTo(disposables)
    store.userInfo.asObservable()
    .subscribe { info: UserInfo -> vm.postUserName(info.
    name) }
    .addTo(disposables)
    }
    // onStart
    時などに呼ばれる
    fun showUserDetail
    (userId: String) {
    PrepareUserDetailAction(
    userId = userId).sendTo(
    dispatcher)
    }
    }

    View Slide

  31. Presenter
    AndroidでFluxのAction
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create
    Fragment
    call
    push
    ViewModel
    observe

    View Slide

  32. Flux Action
    interface FluxAction {
    fun uid(): String = hashCode().toString()
    fun sendTo(dispatcher: FluxDispatcher) = dispatcher.dispatch(
    this)
    }
    data class PrepareUserDetailAction(
    val userId: String
    ) : FluxAction
    FluxAction インターフェースを継承することで、後述の
    FluxDispatcherにいい感じに送れるようにしています。

    View Slide

  33. Presenter
    AndroidでFluxのDispatcher
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create
    Fragment
    call
    push
    ViewModel
    observe

    View Slide

  34. Flux
    Dispatcher
    class FluxDispatcher(
      private val coroutineCxt
    : CoroutineContext = CoroutineName(
    "FluxDispatcher"
    ) + Dispatchers.
    Default
    ) {
      private val observerList
    : MutableList> =
    mutableListOf()
     @Suppress
    ("UNCHECKED_CAST"
    )
      fun register
    (
      klass: KClass<
    T>,
      cb: suspend (action: T) -> Unit
      ): ActionObserver<
    T> = ActionObserver(
      klass = klass,
      cb = cb
      ).also {
      runBlocking(coroutineCxt
    ) {
      observerList
    .add(it as ActionObserver)
      }
     }
      @Suppress
    ("UNCHECKED_CAST"
    )
      fun registerOnce
    (
      klass: KClass<
    T>,
      cb: suspend (action: T) -> Unit
      ): ActionObserver<
    T> = ActionObserver(
      klass = klass,
      cb = cb,
      dispatchCount = 1
      ).also {
      runBlocking(coroutineCxt
    ) {
      observerList
    .add(it as ActionObserver)
      }
      }
      fun unRegister
    (observer: ActionObserver<
    out FluxAction>) {
      runBlocking(coroutineCxt
    ) {
      observerList
    .remove(observer)
      }
     }
      fun dispatch
    (action: T) {
      runBlocking(coroutineCxt
    ) {
      val execObserverList = observerList
    .filter { it.isMyAction(action) }
      Log.d(
      "Flux"
    ,
      "Dispatch action: ${action::
    class.simpleName
    }#${action.uid()
    } for ${execObserverList.
    size} observers."
     )
      execObserverList.
    forEach { observer: ActionObserver
    ->
      CoroutineScope(Dispatchers.
    Default
    ).launch {
      observer.observe(action)
      withContext(
    coroutineContext
    ) {
      if (observer.
    dispatchCount > 0) {
      observer.
    dispatchCount
    --
      if (observer.
    dispatchCount == 0) {
      unRegister(observer)
      }
      }
      }
      }
      }
      }
      }
      companion object {
      private const val DISPATCH_UNLIMITED
    : Int = -
    1
     }
      data class ActionObserver<
    T : FluxAction>(
      val klass: KClass<
    T>,
      val cb: suspend (action: T) -> Unit
    ,
      var dispatchCount
    : Int = DISPATCH_UNLIMITED
      ) {
      fun isMyAction
    (action: T): Boolean = klass.isSuperclassOf(action::
    class)
      suspend fun observe
    (action: T) = cb.invoke(action)
      }
    }
    資料は公開する予定です
    ので、後ほどご確認くださ
    い!

    View Slide

  35. UseCase
    // data class PrepareUserDetailAction
    (
    // val userId: String
    // ) : FluxAction
    class UserDetailUseCase(
    private val dispatcher: FluxDispatcher
    ,
    private val userRep: UserRepository
    ,
    private val store: UserDetailStore
    ,
    ) {
    private val disposables = CompositeDisposable()
    init {
    dispatcher.register(PrepareUserDetailAction::
    class) {
    prepareUserDetail(
    it.userId)
    }
    }
    private fun prepareUserDetail
    (userId: String) {
    userRep.readUserInfo(config)
    .subscribe({ userInfo: UserInfo ->
    store.commitUserInfo(userInfo)
    }, {e: Throwable ->
    store.commitError(e as Exception)
    })
    .addTo(disposables)
    }
    }
    Dispatcherに登録。
    callbackは、バックエン
    ドスレッド内で実行され
    てる
    NWエラー等が発生し
    てもエラーの正常系と
    して扱える!
    バックエンドからデータ
    を取得しStoreに
    commit

    View Slide

  36. Fluxイベントサイクル
    プレゼンテーションイベントサイクル
    Presenter
    AndroidでFlux
    プレゼンテーションイベントサイクルと
    Fluxのイベントサイクルが分離さ
    れ、Presenterで接続されている
    Dispatcher
    UseCase
    Store
    Action
    send
    commit
    observe
    call
    create
    Fragment
    call
    push
    ViewModel
    observe
    参照の向き
    呼び出しの順番
    赤字 非同期

    View Slide

  37. まとめ
    ● プロジェクトや組織のフェーズによって、コードに求めら
    れる優先価値は変わってくる
    ● プロダクトは組織や顧客の写し鏡
    ○ ある設計やその設計を元にしたコードには寿命が
    存在する
    ○ 課題が変化したタイミングが再設計の良いタイミン

    ● Fluxの単方向Pub/Subは多人数開発ではとても具合が
    良いので、同じような境遇の方はネタの一つに加えてみ
    てはいかがでしょうか?

    View Slide

  38. Eigo
    wakaran!!
    Globeeでは一緒に働いていただける
    エンジニア募集中です!
    (英語ができなくても大丈夫です!)

    View Slide

  39. おわり

    View Slide