December 21, 2018

Kotlin Multiplatform構想 ~設計編~

前回の記事 : Kotlin Multiplatform構想 ~今やる理由編~

なぜ今Kotlin Multiplatformを使うのかについて語った記事は前回の記事で解説したので、先に目を通すとスムーズに理解が出来るかと思います。
この記事では現在考えているKotlin Multiplatformにおける設計をサーバサイド、クライアントサイドの観点から話したいと思います。 ソースコードに関しては晒すか迷ったのですが、一応業務時間で作成したものですので現段階では控えたいと思います。頃合いを見計らってサンプルは公開したいなとは思っています。 もう一つのAdvent calendarの記事で、Kotlin ExtensionsのAndroid Parcelize Annotationを使う方法の記事を書くつもりなので、Gradleの構成に関してはそれがヒントになるかなと思います。

なお、設計に関しては十人十色です。サービスの内容、開発メンバー次第で全く別のものになります。 本記事においての設計は、ある程度大規模な動画サービスを開発する上でKotlin Multiplatformの現状を考慮した、 "オレの考える最強の設計"である点をご理解の上読み進んで頂ければ幸いです。

設計

全体設計

Backendサーバを含む全体の構成はこのようになっています。

architecture all

Backend側はマイクロサービスアーキテクチャとなっていて、各サービス毎に役割が異なります。 マイクロサービスなので、動画の処理を行う部分はGoで書かれていたり、APIの処理はKotlinで書かれていたりとその辺は柔軟に開発出来るようになっています。 前の記事でも書いたのですが、次のサービスではBFFサーバを用意してマイクロサービス間の情報を束ねて、1リクエストでクライアント側に返却したいと思っています。(BFFサーバの是非については賛否両論あると思いますが...)

BFFサーバのフレームワークはKtorを考えています。(読み方はケイター) サーバサイドKotlinといえばSpring Bootが有名ですが、SpringはJavaで書かれています。 一方KtorはフルKotlinで開発されているので、Kotlin Multiplatformとの相性がとても良いです。 つまり、BFFサーバをKtorで作成すれば、Kotlin Multiplatformとしてサーバ側とクライアント側とのDomain object(Domain Objectは前記事を参照)を共有することが出来ます。 これは、大きなメリットだと思っています。これまで共有できなかったDomain objectを統一出来る事はサービス開発の効率向上に繋がるでしょう。

クライアント

次に、クライアント側の設計について話したいと思います。
既にKotlin Multiplatformとして2018年10月に行われたKotlin Conf 2018の公式アプリであるKotlin conf appを見るとMVPで開発されています。 その他のサンプルコードを見てもKotlin Multiplatformで開発されているサービスはMVPで開発されているものがほとんどです。
しかしながら、実際にMVPでコードを共通化して開発してみると、AndroidやiOSで異なるライフサイクル、更にそこにWebが入ってくると複雑になることでプレゼンター部分の肥大化が避けられなくなってしまい、共通化のデメリット部分が大きくなってしまいました。もちろん今回のカンファレンスアプリのように単純なアプリの場合は共通化部分を増やした方がメリットがあるのでMVPのようなプレゼンターまで共通化できる設計の方がメリットが大きいのでこの様になっているのだと思います。
また、各クライアントではRxJavaやReactive Swift等のReactive Libraryを使いたいという要望がありました。 私自身もすっかりReactive脳になってしまったので、複雑な仕様のアプリ開発では使わない選択肢はありません。
以上の理由から、多くのサンプル通りのMVPで作成するのではなく、最近ではメジャーになりつつある MVVM + Flux の構成で開発することにしました。 これは、現在の開発メンバーが前サービスでも同様の設計で動画サービスを開発しており、複雑な仕様になりがちな動画アプリにおいて「MVVM + Flux が一番適しているよね」というチーム内同意の元この設計になっているので、一概に正しい設計とは言えない点はご注意下さい。ただ、大規模なプロジェクトレベルでKotlin Multiplatform開発を行う場合は 現状この設計が一番適しているのではという感覚はあります。

現段階でのクライアント側の具体的な設計は以下のようになっています。

architecture mpp

MVVM風とFlux風とService層以下がClean Architecture風の作りになっています。 (風としたのは設計は宗教みたいなもので 人、環境によって認識が変わるので概念としての設計として受け入れて下さい)
この設計にしたメリットは、データの流れを一方向に出来る点とKotlin Multiplatform部分の実装を簡単に切り離せるメリットがあります。

  • データの流れを一方向に出来る

データの流れに関しては、ViewからFlux部分の流れは各プラットフォームでのReactiveライブラリであるRxJava、ReactiveCocoaが使われています。(図中赤い矢印)
Service層以降の部分に関してはCoroutineを用いています。CoroutineはReactiveの概念とは異なるので、厳密には図のような矢印表記だと誤解を招くかもしれませんが、データの流れとして解釈して下さい。Androidでは、この部分は従来ReactiveXでいうSingleを用いて通信を行っていたので、Coroutineに置き換わっただけで特に違和感はありませんでした。iOS側は通信のStreamからView側のStreamに繋げることが多いみたいなので、そこは歯がゆさを感じるかもしれませんが、データの流れは一方向を保ったままですので、複雑な仕様だとしてもシンプルに実装することが出来ます。

  • Kotlin Multiplatformの実装部分を簡単に切り離す事が出来る

Api Clientとして通信に使っているKtor Clientは、残念ながら現時点でgRPCには対応していません。そのため、Kotlin Multiplatform環境でgRPCを使おうと思った場合に独自で実装する必要があります。上記のような設計にしておけば、各プラットフォーム毎に実装をした場合でもAction Creator側で呼び出しコードを分けるだけで、既存コードに影響を与えることはありません。
また、前記事でも述べたとおりKotlin Nativeを含むKotlin Multiplatformはまだβバージョンです。 今後リリースするにあたって大きな問題に直面する可能性は十分にあります。 もし仮にそういった問題が起きた場合に既存の設計に影響しないようにしておくことが重要です。 最終的にサービスをリリース出来なければエンジニアとしての価値はありません。そのため、現時点でKotlin Multiplatformを選定するにあたって、技術的な挑戦とのバランスを取っていく事は一番重要視しています。

上記の設計にしておけば、iOS側で何か問題が発生した場合でもAction Creatorで呼び出しているKotlin Native側のService以下の層を従来通りSwift等でプラットフォーム独自の実装に置き換えれば良いですし、既存コードに影響を与えること無く切り戻すことが可能です。
現在開発をしている実際のソースコードでも、Action Creator以外からはKotlin Nativeのコードを呼び出していません。 つまり、共通のモジュールへの依存が最小限に留められているので、容易に切り離すことが出来るのです。それだと開発工数に影響を与えるのでは?と思うかもしれませんが、Kotlin Nativeとして実装した既存のコードはAndroid側で問題なく利用することが可能かつ、本来実装すべきであった箇所をiOS側のコードで実装しなおしているだけなので、理論上の工数は増えていないはずです。なぜなら、Kotlin Nativeによって削減出来ていた工数が元に戻っただけだからです。

サーバ

サーバサイドはフレームワーク依存になるので、Kotlin Multiplatformの観点から述べる設計はありません。
前述の通りフレームワークはKtor、KtorはKoinをDIツールとして標準サポートしているのでKoinを使うつもりです。
Ktorは別で検証を進めていますが、先日1.0になったため今の所 実用上の問題は無い認識です。

DIの話

サーバ部分でDIの話が出たので、MultiplatformというよりはKotlin寄りの話にはなりますが設計にも大きく関わる部分ですのでDIにも少し触れたいと思います。
Android開発をしている方なら、Dagger2を利用するのが一般的だと思います。DaggerはAnnotation Processorを用いているので、学習コストが若干高いという弱点がありますが、コンパイル時にエラーになる点と実行速度の点から広く使われているDIライブラリです。大規模なプロダクトだと現在ではDagger2一択なのではないでしょうか。
他には、Kotlinで作られたDIライブラリであるKodeinやKoinもあります。 最近、これらのDIライブラリの速度を比較したリポジトリが話題となりました。 Android端末上で計測を行っているので、Kotlin Multiplatform環境だと結果が多少異なるかもしれませんが、概ね同じ結果になると思います。

Daggerはコンパイル時に依存関係を解決しているので、当然実行速度は一番早いのですがDaggerはJavaで開発されているため、Kotlin Multiplatform環境で使うことは出来ません。そのため、必然的にKodeinかKoinを使うことになると思います。 KodeinとKoinを比較すると、Koin1.0の時はKodeinの方が早かったのですがKoin2.0になると、リフレクションの関係?でKodeinより早くなるみたいです。 Kodeinの方がDagger likeに作られているので、馴染みやすいのですが、Kotlin Multiplatform環境ではKoinを使ってDIするのが良いのかもしれません。(※ 現在Koin2.0はalphaバージョンです)
今のプロジェクトではKodeinを使っていたのですが、2.0がリリースされたらKoinへの移行も考えます。

まとめ

最後は横道にそれましたが、これが現状考えているKotlin Multiplatformにおける設計です。基本的に疎結合の設計を心がけているので、なにか問題があっても切り離して既存コードに影響を与えること無く開発出来る事がわかって頂けたかと思います。 ソースコードのサンプルを載せないで説明するのは心苦しいですが、少しでもKotlin Multiplatformの可能性を感じて興味を持って頂けたなら幸いです。

急に実践的な内容になりますが、次回はKotlin MutiplatformのSerializerKotlin Android ExtensionのParcelable Supportを共存させるTipsについて書きたいと思っています。

© AAkira 2018