KotlinでひたすらDSLを書いてたらだいぶノウハウが溜まってきたので共有
KotlinのDSL作成でググるとこんな記事が出てくると思いますが、大半の人は下の記事を読むだけで作りたいものが作れると思うのでぜひ読んでください。
Type-safe builders | Kotlin Documentation
データの実体とDSLが分離されたタイプのDSL#
上で紹介した記事では全部データの実体となるクラスに直接DSLを生やしています。これはこれで綺麗にまとまっていいのかもしれませんが、僕は頭Javaな人なのでBuilderに相当する部分は分けられていてほしいです。特に内部でStringBuilderなどを使うようになってくるとややこしくなってきます。
分離すると何が嬉しいかというと
- DSL上の構造と実際のデータ構造が大幅に乖離しているときにDSLを書きやすい
- StringBuilderなどが使いやすい
- データ保持側に手を入れなくていい(責務の分離)
- クソややこしいDSLを書いたときにデータ保持側を読みやすい
- パフォーマンスが向上する場合がある(とりあえずListに溜め込んでbuild時に生成するStringBuilder的なアプローチがとれる)
逆にデメリットは
- 単純に考えて必要なクラス数が増える(どこまで凝るかによるが2倍以上)
- それに伴う保守性の低下
- プロパティの露出
メリット#
DSL上の構造と実際のデータ構造が大幅に乖離しているときにDSLを書きやすい#
実際のデータにDSLを生やした場合全然関係ないインスタンスを操作することは可能ですが不自然です。しかし、DSL部分を分離するとそのへんの制約は完全になくなって自由になります。
StringBuilderなどが使いやすい#
上と同じで、データ側がStringBuilderをもともと持っているなら自然に使えますが、持ってない場合StringBuilderの意味がなくなります。
データ保持側に手を入れなくていい#
そう、責務の分離ってやつです。わざわざ説明するまでもない
クソややこしいDSLを書いたときにデータ保持側を読みやすい#
例えば一つのオブジェクトのDSLに300行以上あったらすごいややこしくないですか? そういうときに読みやすいです。
パフォーマンスが向上する場合がある#
StringBuilderがいい例なんですが、一旦Listに溜め込んで、build時にインスタンスの生成するっていうアプローチを取れます。
これはKotlin標準のDSL
val buildString = buildString {
append("hello")
append(" ")
append("world")
for (i in 1..10) {
append(i)
}
}
デメリット#
必要なクラス数が増える & 保守性の低下#
データのクラスとそれに対応するBuilderというかDSLの分増えるので雑に計算すると2倍です。共通化できる部分もあるのでもうちょっと少ない場合もあるかも。それに伴って保守性も低下します。
プロパティの露出#
デフォルト実装作ろうとするとinterfaceはprotectedにできないしpackage-privateも無いのでpublicにせざるを得ず、触ってほしくないプロパティまで露出してしまいます。一応internalはありますが、プロジェクト内で使うDSLとかだと微妙だし~~そもそもinternalは無視できる~~
というわけで色々実践してみたリポジトリ