blog/content/posts/2025-01-27/index.md

101 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
author: usbharu
draft: false
categories:
- 技術
date: 2025-01-27T14:08:58+09:00
tags:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
keywords:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
title: Kotlinで作るDSL
relpermalink: posts/2025-01-27/
url: posts/2025-01-27/
decription: KotlinでデータとDSLが分離されたタイプのDSLの作成
---
[usbharu/kotlin-dsl-demo](https://github.com/usbharu/kotlin-dsl-demo)
KotlinでひたすらDSLを書いてたらだいぶウハウが溜まってきたので共有
KotlinのDSL作成でググるとこんな記事が出てくると思いますが、大半の人は下の記事を読むだけで作りたいものが作れると思うのでぜひ読んでください。
[Kotlin で書く DSL](https://improve-future.com/kotlin-dsl.html)
[Type-safe builders | Kotlin Documentation](https://kotlinlang.org/docs/type-safe-builders.html)
## データの実体とDSLが分離されたタイプのDSL
上で紹介した記事では全部データの実体となるクラスに直接DSLを生やしています。これはこれで綺麗にまとまっていいのかもしれませんが、僕は頭Javaな人なのでBuilderに相当する部分は分けられていてほしいです。特に内部でStringBuilderなどを使うようになってくるとややこしくなってきます。
分離すると何が嬉しいかというと
- DSL上の構造と実際のデータ構造が大幅に乖離しているときにDSLを書きやすい
- StringBuilderなどが使いやすい
- データ保持側に手を入れなくていい(責務の分離)
- クソややこしいDSLを書いたときにデータ保持側を読みやすい
- パフォーマンスが向上する場合がある(とりあえずListに溜め込んでbuild時に生成するStringBuilder的なアプローチがとれる)
逆にデメリットは
- 単純に考えて必要なクラス数が増える(どこまで凝るかによるが2倍以上)
- それに伴う保守性の低下
- プロパティの露出
### メリット
#### DSL上の構造と実際のデータ構造が大幅に乖離しているときにDSLを書きやすい
実際のデータにDSLを生やした場合全然関係ないインスタンスを操作することは可能ですが不自然です。しかし、DSL部分を分離するとそのへんの制約は完全になくなって自由になります。
#### StringBuilderなどが使いやすい
上と同じで、データ側がStringBuilderをもともと持っているなら自然に使えますが、持ってない場合StringBuilderの意味がなくなります。
#### データ保持側に手を入れなくていい
そう、責務の分離ってやつです。わざわざ説明するまでもない
#### クソややこしいDSLを書いたときにデータ保持側を読みやすい
例えば[一つのオブジェクトのDSLに300行以上あったら](https://github.com/usbharu/activity-streams-serialization/blob/6e34c1ce3f2d070396c908f139b0863312262c45/src/main/kotlin/dev/usbharu/activitystreamsserialization/dsl/ObjectBuilder.kt)すごいややこしくないですか? そういうときに読みやすいです。
#### パフォーマンスが向上する場合がある
StringBuilderがいい例なんですが、一旦Listに溜め込んで、build時にインスタンスの生成するっていうアプローチを取れます。
これはKotlin標準のDSL
```kotlin
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は無視できる](https://stackoverflow.com/questions/62500464/is-it-possible-to-access-internal-class-with-reflection-in-kotlin)~~
というわけで色々実践してみたリポジトリ
[usbharu/kotlin-dsl-demo](https://github.com/usbharu/kotlin-dsl-demo)