みなさんこんにちは!id:ar_tamaです。最近のマイブームは低温長時間発酵の手捏ね食パンを錬成することです。パンは変数が多すぎてたいへん沼ですね!
さて、今回は昨年のアドベントカレンダーで触れた、ServerSide Kotlinなアプリケーションのマルチモジュール化に挑戦したお話をお届けします。
マルチモジュール化の概要と利点
モジュールというのは意味を持ったコードのまとまりのことで、主にプロダクトの境界設計時に用いられる単位です。「単一責任の原則」の達成のために援用されることも多いでしょう。
マルチモジュール化はこの「モジュール」の分割をきちんと行うということですが、たとえばRailsではこれと真逆(どこからでも何でも読める)の思想が採用されているので、突然「アプリケーションをマルチモジュール化しよう」と言われてもピンとこない方も多いかもしれません(現に以前のわたしがそうでした)。
Kotlinプロジェクトにおけるマルチモジュール化は、依存関係がクリーンに保たれコードの見通しがよくなるだけでなく、ビルドが並列で行われるようになるためビルド時間が短縮されるというメリットもあります。
コードベースが小さいうちは「わざわざ分割しなくても十分見通しがいいし、ビルド速度も気にならないし……」と後回しにされがちではあります(あるとのことです)が、肥大化してから取り掛かるという選択肢は悪手であろうことは容易に想像がつきますね。よろしい、ならば分割だ!
下準備 - 概念の補強
……と意気込んだはいいものの、ServerSide Kotlinのモジュール分割例はインターネットにはあまり転がっていません。Androidでの例はそこそこあり、弊社のAndroiderにも「DroidKaigiのCode Laboに実践例があるよ」と教えてもらいましたが、いかんせんKotlin赤ちゃん👶にはその工程から「Android固有の処理とそうでないもの」を見分ける力がないため、早々に挫折を味わいます。
そもそも「依存関係がクリーンに保たれる」がどのような便益をもたらすのかもピンと来ていない状態だっため、もう少しマルチモジュール化そのものについてを掘り下げることにしました。
- マルチモジュールのすゝめ
- Clean Architecture 達人に学ぶソフトウェアの構造と設計
前者はDroidKaigi 2018で発表された内容とのこと。可読性と再利用性を高め、影響範囲を狭めるために「意味のまとまり(垂直分割)」「階層のまとまり(水平分割)」でグルーピングするアプローチが実践例を交えて紹介されています。
後者は書籍ですね。全編を通してモジュール化の有用性について語られていますが、直接参考になりそうなのは7章「単一責任の原則」と34章「コンポーネントによるパッケージング」でしょうか。かいつまむと「層を飛び越えてはいけないというルールを課すことはできるが、仕組み化しないと強制力がはたらかないので、モジュールを活用していきましょう」といったような内容が記されています。
……なんとなく雰囲気がわかってきました。どの方向にどの単位で分割するかというのはアプリケーションの性質にも大きく依存しそうです。ひとまずアプリケーションのドメインに関わる部分の分割は観点がいくつかあるので後回しにして、それ以外で切り出しやすい層がないか探してみます。
今回のターゲットはgRPCを喋りMySQLからデータを引いてくるWebアプリなので、以下の3点を切り出してみることにしました。
- proto
- KtorのgRPC Application Engine
- MySQL
マルチモジュール化のステップ
依存ライブラリの定義を共通化する
都合のいいことに前述のAndroider氏がサーバサイドアプリケーションの習作を社内リポジトリに公開していたので、まずはそれを見ながら真似をしていきます。ありがとうございます。
彼のコードベースには Deps
Versions
という2つのファイルがあり、それが各モジュールの build.gradle.kts
で参照されていました。定義が共通化されると分割後も参照しやすそうですし、どうやらIDEで補完も効くとのことで、大変便利ですね。さっそくやってみましょう。
// Deps.kt object Deps { object Kotlin { const val core = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}" } }
// Versions.kt object Versions { const val kotlin = "1.3.72" }
// root build.gradle.kts
dependencies {
implementation(Deps.Kotlin.core)
}
※ Deps, Versionsはsrc以下にあればよいというわけではなく、 /buildSrc
以下に build.gradle.kts
と一緒に配置して初めて機能するようになるため、注意が必要です*1。
新規モジュールを作成する
下準備が整ったところで、分割する対象のモジュールを実際に作ってみます。
IDEの File -> New -> Module...
から作っても、ルートディレクトリでmkdirしても大丈夫なようです。お好きな方法で開墾しましょう。
掘ったディレクトリのルートに build.gradle.kts
を置き、空の状態でビルドを試してみてから次のステップに進むのがよさそうです。
ビルドが通ったら、作成したgradleファイルにメインモジュールの依存ライブラリを振り分けていきましょう。
dependenciesのapi, implementationの差を適切に設定する
依存を振り分ける際に気にしなければならないのが、dependenciesブロックで依存先を指定する際の api
implementation
の使い分けです。 build.gradle.kts
では依存の宣言の際に(更にその先の)モジュールを参照可能な状態で受け渡すかどうかを設定する必要があります。
例えば、今回切り出すMySQLモジュールには mysql-connector-java
だけではなく kotliquery
*2もまとめることにしましたが、現状ではこのライブラリはメインモジュールのコードからも使用されているため、apiで宣言し参照可能な状態にしています。一方でこの状態は責務が分割しきれていないということでもあるので、次のステップではその部分もモジュール化し、implementationで済むようにしたいですね。
慣れないうちはひとまずすべてimplementationで宣言してしまって、ビルドが通らなければ変えていく、というアプローチでもよいように思います。
// mysql build.gradle.kts dependencies { implementation(Deps.Misc.mysql) // メインモジュールからアクセス不可能 api(Deps.Misc.kotliquery) // メインモジュールからもアクセス可能 }
コードを移動する
メインモジュールから切り出したモジュールに関連するコードを移動させます。各モジュールのsrc以下に適切なパッケージを切り配置し、それぞれビルドが通ることを確認しましょう。
このときパッケージ名を変えると呼び出し元で参照できないエラーが発生しますので、併せてリネームしていきます。
allProjectsを設定する
分割されたモジュール全体にリポジトリ等の設定を伝播するのに使用します。
Android Studio Projectでは自動でこの allProject
ブロックが設定されているようですが、サーバサイドアプリケーションの場合はないこともあるので確認しておきましょう。
// root build.gradle.kts
allprojects {
repositories {
jcenter()
}
}
ここまででメインモジュールのビルドが通り、分割前と動作が同一になれば完成です! お疲れさまでした。
完成、そして…
多少の躓きポイントはありましたが、以上でマルチモジュール化したアプリケーションが動作するようになりました。
次はもう一歩踏み込んで、アプリケーションコードの分割も行っていきたいですね。
クリーンアーキテクチャ的なアプローチをするならば、UseCases層とAdapters層を切り、それぞれをproto, MySQLへ依存させるときれいに水平分割が叶いそうです。
みなさんもぜひマルチモジュール化を取り入れて、DXの高い開発ライフを手に入れましょう!