Kotlin1.2でAndroid用のモデルをJavascriptから利用する

Javascriptのブラウザ版を作ったのをQiitaに書いたけどこっちに書いてないのすっかり忘れてた…。

Fire Emblem Heroes Battle Simulator

そろそろなんか機能追加とかプレイ画面作成とかしないとな。

要約

Javaに依存せずDDD的にKotlinだけでモデルを記述すればブラウザ上のJSから問題なく利用できる。実際の画面はこちら

KotlinはJavascriptへの変換がサポートされていますが今一つ有効活用されていないようです。これとか。
全てをKotlinで済ませるのは非常に大変ですし、Javaに依存するコードやJSに依存するコードが混在するとさらにややこしいことになります。これは1.2から導入されたMultiplatformでも変わりません。

しかし、JavaにもJSにも依存しないコードならば手間要らずで変換できますし、真のドメイン駆動設計ならばドメインモデルはドメイン自体以外の何物にも―Java.utilにすら依存しないはずです。そこでDDDのモデルをJSに変換して利用します。

 対象のプロジェクト

しつこくこれを使います。Androidでは画面が狭いのでWeb版が欲しかったのですがJSで書き直すのは辛いのでちょうどいいのと、たまたまですがこれのドメインモデル部分はほとんどJavaに依存しておらずこれまた都合が良いからです。具体的にはLocaleとString.format()だけで、これらは容易に除去することができます。listやmapは普通に使えます。

 Multiplatformでのビルド

MultiplatformはIntelliJ IDEAの機能なのでIntelliJからプロジェクトを作成します。
KotlinMPP.png

KotlinMPPnames.png

JVMとJSのモジュール名は実際にモジュール名になるので短くわかりやすい名前が良いです。たとえばuntitled2-jsはuntitled2-js.jsになるしJS上ではuntitled2-js.jp.co...とパッケージをたどってオブジェクトにアクセスすることになります。

これが作成したばかりのプロジェクトです。
KotlinMPPstart.png

ここにドメインモデルのコードを そおい!と放り込みます。
KotlinMPPimported.png
Kotlin/JSやMultiplatformの説明では色々詳しい説明がありますが、何にも依存していないモデルだけ変換する分には関係ありません。
あと今回は事情によりこの環境は捨てるので適当にやってますが、モジュールとして取り込んで真面目にgradleでビルドしたほうが良いと思います。

放り込んだままビルドすると普通にJSが生成されます。
KotlinMPPbuild.png
このJSはそのまま普通に使えます。ランタイムがあればですが。JSにランタイムが入ってないしJVMAndroidではなくJVMだしで、このMultiplatformはWeb/Androidマルチという感じがしません。node.jsとSpringでの利用が目的でしょうか?
とはいえこのままでは使用に耐えないので方針を変更してKotlin/JSを利用します。

 Kotlin/JSでのビルド

Kotlin/JSでプロジェクトを作ります。
KotlinJSP.png

シンプルな初期プロジェクトです。ていうかGradleプロジェクトじゃないんですね。AndroidやMultiplatformはGradleなのに…
KotlinJSPstart.png

ここにもやっぱりドメインモデルのコードを そおい!と放り込みます。
KotlinJSimported.png
モジュールとして取り込むビルド環境を作るべきなのでしょうが分かりませんでした。

ビルドするとMultiplatformと同じようにJSが生成されますが、こちらにはランタイムも入っています。
KotlinJSbuild.png
生成されているJSはMultiplatformで生成されるJSと同じものです。なのでKotlin.jsだけMultiplatformに移植すれば普通に動くはずです。いずれMultiplatformでKotlin.jsとAndroid用のモジュールの取り込みをサポートしてもらえるとWeb/Androidでのマルチプラットフォームが楽になるのですが。

 github.ioで公開する準備

これはサンプルではなくWeb版のアプリとして公開するものなのでそのままgithub.ioで公開します。
まずは公開用のdocsディレクトリを作ってJSをコピーし、index.htmlを書きます。JSは今回の主題ではないのでJQueryでシンプルに書きます。
KotlinJSdoc.png
GithubにPushした後、このプロジェクトのSettingsからGitHub Pagesとして公開します。
KotlinJSGithub.png

 JS側のコード

index.htmlを右クリック>ブラウザで開くと普通に開けるのでF12のコンソールからモデルの構造を見てみます。
KotlinJSbrowser.png
モデルのクラスやオブジェクトに普通にアクセスできますので、これを使うだけです。以下は画面上の入力を省略したコード例です。

//長いパッケージ名を省略する
var fehs = FEHSIM.jp.blogspot.turanukimaru.fehs
//全てのヒーローを取り出してソートする。allItems()はKotlinのListだがtoArray()でJSのArrayになる。
var allHeroes = fehs.StandardBaseHero.allItems().toArray().sort(function (a, b) {
    if (a.name > b.name) {
        return 1
    } else if (a.name < b.name) {
        return -1
    } else {
        return 0
    }
})
//オーバーロードしているとメソッド名が変わるのでオーバーロードは避ける事
var heroA = fehs.StandardBaseHero.get_61zpoe$('エフラム')
var armedHeroA = new fehs.ArmedHero(heroA , "new heroA")
var battleUnitA = new fehs.BattleUnit(armedHeroA,armedHeroA.maxHp)

var heroB = fehs.StandardBaseHero.get_61zpoe$('エイリーク')
var armedHeroB = new fehs.ArmedHero(heroB , "new heroB")
var battleUnitB = new fehs.BattleUnit(armedHeroB,armedHeroB.maxHp)

var fightResults = battleUnitA.fightAndAfterEffect_trfvk0$(battleUnitB)
var last = fightResults[fightResults.length - 1]
//あとは戦闘結果を表示するだけ

このドメインモデルは二人のユニットを生成して能力値や状況を設定して戦わせ結果を得る、というものなので他のコードは画面の入力をJQueryで拾ってnewするときに初期値を変更しているJSだけです。
画面側の処理もKotlinで書きたい!というのはそれはそれでありでしょうが、画面は直接JSのほうがデバッグや管理がしやすいかと思います。欲しい結果やフィルタがあったらドメインモデルで作成するべきでしょう。
気を付けるべきは「ついJava.utilを使ってしまうが避ける」「List,MapはJava.utilではないので普通に使えるがJSのArrayでなかったりする」「コンパニオンオブジェクトはクラス名.Companionになる」「プロパティはあまり名前が変わらないがメソッド名は簡単に変わる」「JSやMapで使われそうなメソッド名は変わってしまうので避ける」「newはつい忘れるしコンストラクタでvoid 0ではなくnullを渡すと落ちたりするのでなるべく避ける」「Enumは安心して使える」くらいでしょうか。

 おわりに

Kotlin/JSもMultiplatformもまだ発展途上中とはいえ、モデルを利用することだけならできました。
今回作ったアプリはゲームのツールなので、ゲームのバージョンアップに合わせてモデルに新キャラが追加されていきます。その更新をKotlin内で終わらせられるだけでも私には価値がありました。
ただしこれはDDDにより依存が少なかったためでDDDの力ともたまたまとも言えます。
ビルド環境も統合されているとは言えませんし、いくらDDDだってJava.utilにすら依存しないモデルとか普通は作りません。たまたま使えるなら使う、くらいでいいんじゃないでしょうか。