はじめに
みんなのマーケットで iOS / Android アプリエンジニアとして働いているYangです。
非常に分(意)か(味)り(不)や(明)す(!)く(!)タイトルに書いた通り、今回はデカップリングを話題として、Androidのモジュール化を紹介したいと思います。
モジュール化とは
イメージとしては:
(モザイク除去はこちらへ画像の出典)
違う!
複雑で巨大なシステムやプロセスを設計・構成・管理するとき、全体を機能的なまとまりのある“モジュール”に要素分割すること。設計・製造時の擦り合わせ作業をできるだけ少なくするために構成要素(部品)の規格化・標準化を進め、その相互依存性を小さくすることをいう。
(出典)
「wwwの生みの親」「Webの父」などの異名を持つ、ティム・バーナーズ=リーさんの本「Principles of Design」に以下の名言があります。
Principles such as simplicity and modularity are the stuff of software engineering; decentralization and tolerance are the life and breath of Internet.
会社の事業拡大と共に、アプリの機能もどんどん増えて、複雑になっています。
巨大とまではいかなくてもビルドに時間はかかるし、ファイルツリーがだんだん長くなって、開発もしづらく、依存が多すぎて単体テストが書きづらいです。また、部分的な不具合や変更が発生したときに、その影響が全体へと波及してしまいます。
解決しないといけないこと
1. コードのデカップリング(分離原則)
どうやって一つの巨大のプロジェクトを複数のモジュールに分けられますか。もしモジュール間で、再度お互いに直接アクセスするのであれば、デカップリングとは全く言えません。どうすれば直接の参照を避けられるでしょうか。
2. 各モジュールの独立実行
一回のデバッグにもし一つか二つぐらいのモジュールだけ参加させたら、ビルドの時間は大幅に短縮できますから、開発の段階でどうやって最低限必要なモジュールだけをコンパイルさせますか。各モジュールが疎結合であれば,どうやって独立に実行やデバッグをさせますか。
3. 各モジュール間のデータ通信
各モジュールは外部へのサービスを提供することができますが、メインモジュール(Host)や他のモジュールへどうやってデータを送受信すればいいでしょうか。
実際画面遷移の本質も特殊なデータ送信ですが、Android上の画面遷移は Intent
を使わないといけないので、解決の方法は少し違います。
コードのデカップリングの解決
巨大なコードを分割しやすくするために、Android Studio IDEに Multiple Module
という機能をサポートしていて、これを使って最初に簡単な分離はできます(つまり参照が複雑なところは自分で整理しないといけないです)。
ここで明確したい概念があって、モジュールは二種類に分けることができます。
一つは Base Library で、これらは他のモジュールから直接参照できます。例えば、ネットワークモジュールや画像処理モジュールは二つの Base Library として考えられると思います。
もう一つは Component と似ているような感じで、完全な機能を持っています。くらしのマーケットのアプリを例にすると、予約モジュールやチャットモジュールは二つの Component と呼ばれたらいいです。
Base Library は単純に汎用的なサービスを提供するため、 モジュール化
に言った モジュール
は基本的に二つ目です。
これで、コードの分離は図の通りにできます。
矢印を書いているところだけ依存関係が存在して、つまり Reservation Module
と Calendar Module
はお互いに見えないです。こうすると、コードのデカップリングを実現できます。
各モジュールの独立実行の解決
ビルドツールに対して、あるモジュールがプロジェクトの入り口かどうかの判断は該当モジュールの build.gradle
を参照しています。
apply plugin: 'com.android.library'
の場合は部品で、 apply plugin: 'com.android.application'
の場合はアプリと識別されます。
つまり一個 isRunAlone
の変数を用意すれば、切り替えは可能です。
これ以外、アプリと識別された場合、 AndroidManifest.xml
ファイルに最初ページとしての Activity
も指定する必要があります。私の解決方法では二つのManifestファイルを用意して、リリースの際にdebug用のファイルを除外します。
最後、アプリとして実行するために必要な applicationId
も指定して、独立実行の準備はできました。
if (isRunAlone.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } // ...... defaultConfig { if (isRunAlone.toBoolean()) { applicationId "jp.curama.shop.module1" } // ...... } // ...... sourceSets { main { if (isRunAlone.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/release/AndroidManifest.xml' java { exclude 'debug/**' } } } // ...... } // ......
各モジュール間のデータ通信の解決
前書いた通り、ここは二つのシーンがあって、①画面遷移と②メソッド呼び出しです。色々調べて、一番良い解決案はAlibabaが提供している Open Source Library ARouter
です。一発で二つのシーンを解決できます。
Android の標準画面遷移は下の通りです。ここでの問題は、 ActivityA
に ActivityB
をインポートしないと遷移できません。
// filePath: module1/ActivityA.kotlin // module2への依存が発生してしまいました import module2.ActivityB class ActivityA: BaseActivity() { fun navigateToB(id: String) { val intent = Intent(this, ActivityB::class.java).apply { putExtra("id", id) } startActivity(intent) } } // filePath: module2/ActivityB.kotlin class ActivityB: BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); val id = intent?.getStringExtra("id") // ...... } }
ARouterを使う場合、 ActivityA
は遷移先の URL がわかったら遷移できます。私達のシステムでは管理上の考えによって、 RouterPath
という URL の集約ファイルを作って、 Base Business Layer
に置いて、 URL のハードコードを避けました。またどの画面がどのURLを使っていて、必要なパラメーターが何であるかは誰でもすぐに一目瞭然で、把握できます。もう一つ便利なところは ARouter
経由で渡したパラメーターがインジェクターによって自動的に遷移先へアサインされるところです。
// filePath: module1/ActivityA.kotlin // import ActivityB は不要です class ActivityA: BaseActivity() { fun navigateToB(id: String) = ARouter.getInstance().build("module2/b").withString("id", id).navigation() } // filePath: module2/ActivityB.kotlin @Route(path = "module2/b") class ActivityB: BaseActivity() { @Autowired lateinit var id: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); ARouter.getInstance().inject(this); // ここから this.id はもう使えます // ...... } }
シーン②も似たやり方で、 URL でアノテーションをして、使用者はインタフェースに依存することで、ARouterから実現を取得して、使ったらうまく動けます。
これでモジュール化の大きい問題を全部解決できました。
最後に
我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/ )
次回は、SREチームの千代田さんの予定です。