Successfully reported this slideshow.
Your SlideShare is downloading. ×

Flutterアプリ開発におけるモジュール分割戦略

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad

Check these out next

1 of 81 Ad

More Related Content

Recently uploaded (20)

Advertisement

Flutterアプリ開発におけるモジュール分割戦略

  1. 1. Flutterアプリ開発におけるモジュール分割戦略 〜dart 2500ファイルを100分割する〜 VIVIWARE株式会社 山下武志 @eaglesakura
  2. 2. 自己紹介
  3. 3. 山下 武志 ● VIVIWARE株式会社 所属 ● アカウント ○ Twitter @eaglesakura ○ Github @eaglesakrua ● 2008年からずっとモバイルアプリ開発中心 ● 2019年頃からFlutterアプリ開発に関わる
  4. 4. 導入したプロジェクトの紹介
  5. 5. VIVIWARE Cell Plus ● https://cell.viviware.com/ ● ノードベースのプログラミングツール ● Bluetoothで自社ハードウェアと連携
  6. 6. VIVIWARE Cell Plus ● Androidアプリ(Kotlin)をFlutterでリニューアル ○ 現在iOS, Android対応 ○ Windows, Webなどへも展開予定 ● 2022年9月時点で*.dartファイル約2500個、約15万行ほどの規模 ● 約100個の細かいモジュールから構成される ○ Dart的には `package` のほうが正確であるが、このセッションでは「モジュール」に統一
  7. 7. VIVIWARE Cell Plus ● 開発当初から、巨大なプロジェクトになることが予想された ○ 「ユーザーにプログラミング環境を提供する」という複雑なアプリ ○ マルチプラットフォーム対応 ● 導入が予想される多数のプラグイン ○ カメラ ○ Bluetooth ○ データベース ○ データ暗号化 ○ 音源再生機能 ○ その他...
  8. 8. プロジェクトの健康寿命を考える
  9. 9. プロジェクトの健康状態は常に悪くなる ● 開発が続く限り、何もしなければプロジェクトは不健康になる ○ 技術的負債が溜まる、非推奨な APIを使い続けるなど ○ 誰が何のために導入したかも定かではないライブラリがエラーを出しているなど ● 可能な限りプロジェクトの健康状態を良好に保てる設計が必要である ● 健康を願うだけでは叶わない、健康であり続ける環境が必要になる
  10. 10. VIVIWARE Cell Plusの寿命 ● リリース後3〜5年を最低限の健康寿命と設定した ● 5年後も健康であり続けるために、モノリシック設計とマイクロモジュール設計のど ちらが良い選択かを検討した ● 結果としてある程度の追加投資をしても、モジュールを積極的分割するリターンの 方が大きいと判断した ● 新たな設計開発手法・アーキテクチャが登場する可能性は高い ○ 5年、もしくはそれよりも早く「存在自体が古臭い」と言われる可能性はある ○ 未来は誰にもわからないが、少なくとも 2021年時点ではこれが最良であると判断した ○ 2022年時点でもその判断は変わっていない
  11. 11. マイクロモジュール設計の定義 ● このセッションでは「(ライブラリではない)アプリ専用の複数のモジュールを組み合 わせて1つのアプリを構築する」設計をマイクロモジュール設計と呼ぶ ● サーバーサイドの「マイクロサービス」のイメージ
  12. 12. マイクロモジュールの利点
  13. 13. 機能単位でのリプレイスを容易にできる ● 「ある機能でだけ依存しているライブラリ」の影響範囲特定が容易である ● これはライブラリやAPIが突然の死を迎えた際の治療を容易にする ○ 導入しているライブラリの更新停止も珍しく無い ○ 問題が発生した際の代替手段は常に考える必要がある
  14. 14. 影響範囲を限定的にできる ● Dart言語は*dartファイル単位で公開・非公開が行える ○ 厳密にはいろいろ例外あり ● 公開していないクラス群の影響範囲は、モジュールのなかに限られる ○ モノリシックの場合、「無関係なはずのコードから参照される」ことを制限する手段がない ● NEED NOT TO KNOW(知る必要のないこと)をソースコードレベルで実現できる
  15. 15. モジュール単位での積極的リファクタリング実施 ● モジュールの外への影響範囲が限定されている ● ソースコードのコンフリクトが起こりにくく、積極的リファクタリングが行える ● 積極的に新しいアーキテクチャを導入しやすい ○ モジュール単位でアーキテクチャやライブラリを選定 /更新できる ○ 基本的にはRedux Architectureを採用しているが、将来的な変更をしやすくしている
  16. 16. レビューの重点を把握しやすくなる ● モジュール内の非公開コードである限り、ある程度品質に妥協できる ○ 「完璧」なコードを常に求めるのは難しい ○ アプリ開発にかけられる期間は有限であるため、メリハリをつけてレビューする ● コードレビューや影響範囲が限定されるため、観点整理しやすい ● レビューの重点をAPIインターフェースとPublicなコードとする
  17. 17. レビューの重点を把握しやすくなる ● 多数のdartコードが内部に存在している ● 例の場合exportされているのは1classだけ ○ それ以外のコードはある程度妥協してマージする ○ 品質に致命的問題が発生した場合、モジュール自体を再実装する
  18. 18. モジュール分割の書き方
  19. 19. モジュールの作成はflutter createから行える ● `flutter create –template=package util_core` の実行例
  20. 20. テンプレートを作成し、プロジェクトの事情を反映 ● VIVIWARE Cell Plusではコピペで使えるテンプレートを用意した
  21. 21. pubspec.yamlの記述 / 同一リポジトリの場合 ● pub.devにアップロードされているライブラリを参照するのとほぼ同様 ● 分割したモジュールを参照する際、pubspec.yamlに次のように記述する ○ util_coreモジュールに便利系処理を分割し、別なモジュールから参照する例 ● 1リポジトリに複数のモジュールを追加して開発できる
  22. 22. pubspec.yamlの記述 / gitの別リポジトリの場合 ● 複数プロジェクト間で共有する際は別リポジトリにしても良い ● 分割したモジュールのリポジトリとバージョンを指定できる ○ “ref:” にはtag名、branch名、ハッシュが記述できる
  23. 23. refにハッシュ値を指定する場合注意点 ● ハッシュ値の文字列 “aabbccdd…” で比較されるため、意図せず古いバージョンが 使われる恐れがある ○ 複数モジュールから異なるバージョンが参照される場合問題となる ● gitのモジュールを参照する場合はtagを指定した方が問題が少ない
  24. 24. モジュール分割の方針と具体例
  25. 25. ドメインの分割
  26. 26. ドメイン層は積極的に分割する ● 製品ドメインに関わる部分は独立したモジュールにする ● VIVIWARE Cell Plusでの例(10以上に分割)
  27. 27. 画面単位の分割
  28. 28. 全ての画面は別モジュールに分割されている ● 画面遷移(Navigator操作)を伴う箇所は全て別モジュールに分割 ログイン画面 view_login モジュール ユーザーデータ一覧画面 view_home モジュール プログラミング画面 view_project_editor モジュール
  29. 29. 画面内での分割
  30. 30. 複雑な画面は複数のモジュールに分割されている ● プログラミング画面の例 ● 複雑な画面をUsecase(処理)とView(描画)に分割
  31. 31. 複雑なダイアログも別モジュールに分離 ● 複雑なダイアログは基本的に別モジュールとして実装されている ○ ユーザーデータ保存ダイアログなど、複雑なダイアログが多い ○ この画面でしか使用しないコードが多数あるため、モジュールを分割
  32. 32. プログラム編集画面の構成 ● プログラム編集画面 ○ view_project_editor ■ view_project_editor_circuit_jump ■ view_project_editor_comment ■ view_project_editor_variable_register ■ view_project_editor_save ○ usecase_project_editor
  33. 33. インフラ層の分割
  34. 34. プラグインは別モジュールに独立させる ● プラグイン(ネイティブコード)を必要とする処理はモジュールを分離 ○ ネイティブコードを必要とするモジュールもプロジェクトでは「インフラ」と分類した ● プラグインはマルチプラットフォームでは常にリスクと隣り合わせ ○ 対応していると思ったら機能が限定されていた、とかもある ○ 明日、あなたのもとに LinuxやWindows対応リクエストが届くかもしれない ● モジュールを分離し、適切なインターフェースを介してアクセスする ○ WebではFile Systemが使えない(制限がある)など、プラットフォーム固有の知識も必要 ○ VIVIWARE Cell Plusプロジェクトではファイルアクセスもプラグインと同等の扱い
  35. 35. ビルド時のexport切り替え方法 ● Dart言語ではビルド時に定数を使用してexport切り替えることができる ● 使用するdartファイルが切り替わるため、全く異なるプラグインを使用することがで きる
  36. 36. Bluetooth処理の例 ● flutter_blueがサポートしているプラットフォームは3つ ○ Android, iOS, Mac ● BLE関連をflutter_blueに全て依存すると、Windows版開発時に問題となる
  37. 37. ● infra_celldevice_ble3モジュールのみがflutter_blueに依存している設計 ● flutter_blueは他のモジュールでは依存していない ○ 依存関係を限定することにより、意図しない参照を避ける ○ 「やらんやろ」と思っても意外とやらかすのである ● 言語レベルで制限をかけるのが重要 インフラ層にプラグインを閉じ込める
  38. 38. プラグインが モジュールの境界を越えるパターン
  39. 39. プラグインがモジュールの境界を越えているパターン ● Databaseを扱うライブラリはSQLとは切り離しにくい ● 各機能のためのクエリが大量に存在する ● 出来ないことはないが、「これ全部Wrapするの?」というコストがある ○ SQLに関わる全ての例外を Wrapするのは現実的ではなかった ● プラグインが生成したClassへの直接アクセスを許可している ○ Repository層まではアクセスを許可している ○ Repository層よりも上はプラグイン関連へのアクセスを許さない ■ 各RepositoryはDriftを知っている ■ Repositoryの利用者はDriftを知らない ○ Clean Architectureにおける”結婚”の覚悟を持つ
  40. 40. 相当の覚悟を持ったexport ● Driftプラグインやsqlite3プラグインの一部をexportしている ● Repository層ではクエリ発行が頻発するため、効率を優先した ○ 全ての細かいQueryに対してラッパーを用意することが高コストと判断した ○ RepositoryのDBアクセス部分がDriftと「結婚」する覚悟を持つ ● Driftが非推奨(開発終了など)となった場合は、「離婚」するコストを払う ○ という覚悟である ○ ダラダラとDriftと結婚生活を続けるのはやめよう
  41. 41. プラグインとの縁談 ● どのような観点で結婚を決めるか ● 確認した要素 ○ 更新頻度 ○ 対応プラットフォーム ○ PubのLike数 ○ 自動判定されるScore ○ ドキュメントの充実度
  42. 42. 分割方針のまとめ
  43. 43. 完璧にするコストと、 完璧でないリスクを勘案する
  44. 44. 分割の基本方針まとめ ● 1画面につき最低1モジュール以上に分離 ○ 1画面が複数のモジュールで構成されている場合もある ● 共通で使うコードはutilとしてモジュールを分離 ● インターフェースに対して実装が複数ある場合は実装ごとに分離 ● プラグインは必ず1モジュール以上に分離する ○ プラグインとは、ネイティブコードを含む( dartファイルで完結しない)モジュールである ○ レビュー時、「導入するライブラリにネイティブを含むか」は重要な観点である ● プロジェクト内でPrefix/Suffixを決めておくと良い ○ VIVIWARE Cell Plusの例 ■ util, domain, infra, usecase, view など
  45. 45. 分割したモジュールを効果的に扱う
  46. 46. Dart言語でInternalアクセスを使う
  47. 47. 公開したclassの、一部機能を制限したい場合 ● Dartは言語レベル(コンパイラレベル)の制限が弱い ○ 言語レベルではPrivateとPublicだけが用意されている ○ Privateは自分以外誰も使えず、 Publicにすると無制限に誰でも使える ○ 特にInternalは有用である ■ コンストラクタをinternalにして勝手にインスタンス化するのを防ぐなど ● コメントに「触るな危険」と書いても意味がない ● KotlinやJavaでは可視性が豊富に用意されている ○ Protected(継承先だけ使える) ○ Package Private(同じPackageのグループだけ使える) ○ Internal(同じモジュール内だけで使える)
  48. 48. Annotationで警告を与えることはできる ● 言語レベルで完全なアクセス制御を行うことができない ● `@internal` Annotationを付与することで、警告を与えることができる ○ classのAnnotationとしても有効で、意図しない exportを防げる ○ `@protected` `@visibleForTesting`など、アクセス制御用の Annotationが用意されている ○ https://api.flutter.dev/flutter/meta/internal-constant.html ● Analyzerと組み合わせることで、意図しないアクセスを抑制できる
  49. 49. 現代的なアプリ開発において、 Analyzerの存在は不可欠である
  50. 50. Analyzerの利用
  51. 51. ● `flutter analyze` コマンドでAnalyze(静的解析)を行える ● 正確・迅速に問題点を把握できる ● コンパイラとは違うレイヤーの問題を発見する ○ コンパイラは言語レベルでの問題検出 ○ Analyzerはコーディングルールの問題検出 ● Analyzerが発見した問題は適切な解決方法がガイドされる Flutter標準のコマンドを利用する
  52. 52. Analyzerは必ず設定する ● Analyzerはコード品質の向上に非常に有用である ● Analyzerは開発者の「気を付ける」よりもずっと信頼できる ● 可能な限り全てのモジュールにAnalyzerを導入する ● Flutter 2.5以降で標準で導入されるようになった ○ 無視しているなら今すぐ警告を直そう!
  53. 53. Analyzerの共通化
  54. 54. Analyzerのメンテナンスを簡略化する ● analysis_options.yamlファイルはinclude文が使える ● 共通のanalysis_options.yamlファイルを用意し、他のモジュールはこれをinclude することで共通化する
  55. 55. Analyzerは最新に保つ
  56. 56. Analyzerは導入して終わりではない ● Analyzer(Lint)は適宜アップデートされている ○ flutter_lintsのバージョンを確認してみよう ○ https://pub.dev/packages/flutter_lints ● Flutter / Analyzerバージョンを最新に保つ + Analyzerを有効化するだけでコード品 質の改善に役立つ
  57. 57. 意図しない直接importを防ぐ ● これだけは絶対に有効にしておく ● depend_on_referenced_packages ○ https://dart-lang.github.io/linter/lints/depend_on_referenced_packages.html ● pubspec.yamlのdependenciesに記述されていないライブラリを使用している ○ コンパイラレベルでは、 「依存しているライブラリが依存しているライブラリ ....」の使用を防げない ○ モジュールを分割して隠蔽したつもりなのに依存している、という問題を可視化する ○ 「内部でしか使ってないはずだから」と思ってライブラリを切り替えたら大混乱 ...を防ぐ
  58. 58. Continuous Integrationの問題
  59. 59. 並列化しすぎるとコストが増大する ● Github ActionsのJobを並列化することでテスト速度を最適化しようとした ○ 100モジュールを、100の並列実行で短時間に終了させることができた ● 1つのJobが高速化する代わりにオーバーヘッドが大きくなった ○ cloneやpub get、キャッシュ操作などの 1実行に対するオーバーヘッドが 100倍になった ○ 実時間3分程度で完了するが、 100並列あるので1実行300分の課金となる ○ コミッター×コミット数で加速度的に課金が増える ● 「Jobの実行時間が15分を超えたら分割する」というポリシーで対応した ○ 現在はモジュールを 7つのグループに区切って実行している ○ 実時間11分、課金時間60分程度となっている ■ Github Actionsで1回のCI実行につき$0.5程度となる
  60. 60. 開発環境が高スペックを要求する問題
  61. 61. 開発環境が非常に高スペックを要求する ● モジュール数に比例して開発環境がメモリを要求する ● VIVIWARE Cell Plusの場合、最大で30GB程度のメモリを消費していた ● エミュレータなどの実行環境も加わるとさらに消費
  62. 62. 銀の弾丸はない
  63. 63. 金の弾丸を使う
  64. 64. RAMを増設する ● メモリはPCのパーツとして安価な部類である ● 金はエンジニアのスキルに関係なく、平等に解決してくれる ● Macbookなど、増設できないマシンを購入するさいはケチらずいこう ● 2022年現在はメモリ64GBを搭載していると安心できる
  65. 65. 最近の良い話
  66. 66. Flutterバージョンアップでメモリ使用量改善 ● 最近のFlutterアップデートでメモリ使用量が大幅改善 ● 開発環境の消費量が5GB弱に低減 ○ VIVIWARE Cell Plusの場合 ○ プロジェクト規模に依存する ● 開発環境は可能な限り最新を保つことが大事 ● CPUはビルド速度に直結するため、やはり金の弾丸は無駄にならない
  67. 67. 未解決問題
  68. 68. 多言語対応のモジュール分割
  69. 69. Intlライブラリ + モノリシックアプリでの多言語対応 ● 言語ファイル(*.arb)を作成してコード生成を行う ● モノリシックであれば全ての言語が1モジュールに含まれていれば問題ない ● 公式ドキュメントではこの利用方法が解説されていた ● モジュールを分割する場合の方法が見つからなかった
  70. 70. スマートな解決方法はなかった
  71. 71. 妥協案を採用した
  72. 72. CSVファイルから変換する内製ツールを開発 ● CSVファイルにモジュールごとのリソースを記述 ● 文字列リソースアクセス用クラスが生成される 変換・生成
  73. 73. モジュール内で文字列リソースにアクセス ● StringsMixInのインスタンスにアクセス
  74. 74. 全てのCSVが統合されたarbファイルを生成する ● 全モジュールのCSVを統合したarbファイルを生成 ● 生成後はIntlライブラリのドキュメントに従って扱う 統合
  75. 75. 依存しているモジュールの持つ文字列リソースを使用 ● MixInと組み合わせることで複数のモジュールの文字列を使用する ● Androidの文字列リソース管理と異なり、手動で組み合わせている
  76. 76. モジュール間の文字列リソース共通化を解決したい ● Androidアプリ開発における文字列リソースの解決はそれなりにスマート ○ 依存しているモジュールが持つリソースは自動的に使用可能になる ● 是非ともFlutter公式も対応して欲しい ○ できればよりよくしてほしい( XMLではなく)
  77. 77. 必要であれば内製ツールも検討 参考: CSVからの変換ツールは2〜3日程度で検討と実装
  78. 78. 公式にマルチモジュール+ 多言語対応のサポートを求む (既にあるなら教えてください)
  79. 79. まとめ
  80. 80. プロジェクトが 短期間(数か月以内)に終了する場合、 オーバーヘッドが大きい
  81. 81. 長期的な開発・メンテナンスを考えれば、 プロジェクトへのマルチモジュール適用は非常 に大きなリターンがある

×