Slide 1

Slide 1 text

Copyrights(c) Henry, Inc. All rights reserved. Kotlinを用いたDSL的な 設計手法と使用上の注意 サーバーサイドKotlin LT大会 vol.11 @kohii 2024-03-08

Slide 2

Slide 2 text

Copyrights(c) Henry, Inc. All rights reserved. About Me Kohei Ishikawa / @kohii ● 2021.08〜 Henry Inc. ○ Developer ○ CTO室 Lead Architect ● Indie Hacker ○ Moyuk - ブラウザ上でTypeScriptでツールを作るプラットフォーム ○ MediXplorer - 医科診療行為マスタービューアー ○ SmoothCSV - CSVエディター ○ など ● X: @kohii00

Slide 3

Slide 3 text

Copyrights(c) Henry, Inc. All rights reserved. ヘンリーが作っているもの ● レセコン一体型電子カルテ「Henry」 ○ 電子カルテ: 医療情報を記録・管理するソフトウェア ○ レセコン: 診療報酬制度に基づいた会計情報を管理するソ フトウェア

Slide 4

Slide 4 text

Copyrights(c) Henry, Inc. All rights reserved. 今日話すこと 1. コードは表現 2. DSLについて 3. KotlinでDSL的なことをやる 4. ヘンリーでやったこと 5. 用法・容量

Slide 5

Slide 5 text

Copyrights(c) Henry, Inc. All rights reserved. 今日話すこと 1. コードは表現 2. DSLについて 3. KotlinでDSL的なことをやる 4. ヘンリーでやったこと 5. 用法・容量

Slide 6

Slide 6 text

Copyrights(c) Henry, Inc. All rights reserved. すべてのコードは表現である ● コードはシステムを動かすための命令であると同時に、読む人のための表現 ● 単に動作するコードを書くのでは足りない

Slide 7

Slide 7 text

Copyrights(c) Henry, Inc. All rights reserved. なぜなら... ● 作って終わりではない ○ 作る時間より、その後保守運用される時間の方が圧倒的に長い ○ コードは様々な理由で修正される ● チームで開発する ○ 自分が作っていないコードの面倒を見る ○ 昨日の自分は他人 「そもそも、他人の書いたコードを理解すること自体が既に十分に難しい作業なのです。」 (Vladimir Khorikov「単体テストの考え方/使い方」p.79)

Slide 8

Slide 8 text

Copyrights(c) Henry, Inc. All rights reserved. コードが意図の表現に見えるか、文字の連なりに見えるか ● 前提知識の問題 ○ コードを理解するために学習しないといけないことがどれくらいあるか ○ ドメイン知識、設計指針、過去の経緯、 etc… ● 設計・実装の問題 ○ コードがどれくらい意図をダイレクトに伝えるものになっているか ○ いろいろな要素がある ■ 適切な命名 ■ 適切な構成 ■ 適切な責務 ■ 手続き的 ←→ 宣言的(グラデーション) ■ etc.

Slide 9

Slide 9 text

Copyrights(c) Henry, Inc. All rights reserved. 今日話すこと 1. コードは表現 2. DSLについて 3. KotlinでDSL的なことをやる 4. ヘンリーでやったこと 5. 用法・容量

Slide 10

Slide 10 text

Copyrights(c) Henry, Inc. All rights reserved. DSL(ドメイン固有言語) ● DSL = 特定のドメインやタスクに特化した構文やAPI ○ そのドメインにおける操作や設定を直感的に表現できる ○ 手続きではなく欲しい結果や意図を宣言的に記述する

Slide 11

Slide 11 text

Copyrights(c) Henry, Inc. All rights reserved. ● 内部DSL ○ 特定の言語内で機能する DSL ○ 例: ■ Kotestのアサーション ■ KoinのDI設定 ● 外部DSL ○ 完全に独立した構文 ○ 例: ■ SQL ■ HTML ここでは内部DSLについて話します 内部DSL と 外部DSL

Slide 12

Slide 12 text

Copyrights(c) Henry, Inc. All rights reserved. 今日話すこと 1. コードは表現 2. DSLについて 3. KotlinでDSL的なことをやる 4. ヘンリーでやったこと 5. 用法・容量

Slide 13

Slide 13 text

Copyrights(c) Henry, Inc. All rights reserved. KotlinでDSL的なことをやる 〜 基本: Poor man's DSL 〜

Slide 14

Slide 14 text

Copyrights(c) Henry, Inc. All rights reserved. 例題: ワークフロー Kotlinでワークフロー的なものを実装 (CIやジョブネットみたいなタスクの連鎖 )

Slide 15

Slide 15 text

Copyrights(c) Henry, Inc. All rights reserved. 愚直に書く ● `ExecutorService` で並列処理を管理 ○ これはこれで程よく抽象化されていて良い APIだと思う ● Workflow中の各タスクは一旦 `println` で表現している

Slide 16

Slide 16 text

Copyrights(c) Henry, Inc. All rights reserved. 欠点 ● ミスや見落としが発生しやすい ○ 例えば並列処理の待ち合わせを忘れたりとか、 shutdownする対象のExecutorServiceを間違えた りとか... ● 構造が見えにくい ○ 処理を順に追わないとワークフローの構造が見えてこない ● 本質的なことと副次的なことがごちゃまぜ ○ ワークフロー制御のためのコード と ワークフロー内で実行されるタスク本体のコード が混ざってい て読みにくい

Slide 17

Slide 17 text

Copyrights(c) Henry, Inc. All rights reserved. 宣言的に書けるようにしてみる ● 方針: 仕組みとそれを使う側に分ける ○ 仕組み側: ワークフローの構造を表現するための APIと実行機構を提供 ○ 使う側: 仕組みを使ってワークフローを記述する

Slide 18

Slide 18 text

Copyrights(c) Henry, Inc. All rights reserved. ①ワークフローをモデリング ワークフローはファイルシステム(ファイル/ フォルダ)のような木構造で表現できる ● Task ○ 実行対象の処理を持つモデル ○ 木構造の葉となるノード ● Sequential ○ 複数の子ノードを持つ ○ 子ノードは順次実行される ● Parallel ○ 複数の子ノードを持つ ○ 子ノードは並列実行される ○ (オプショナルなパラメータとして並列度も 持つ)

Slide 19

Slide 19 text

Copyrights(c) Henry, Inc. All rights reserved. ①ワークフローをモデリング ワークフローはファイルシステム(ファイル/ フォルダ)のような木構造で表現できる ● Task ○ 実行対象の処理を持つモデル ○ 木構造の葉となるノード ● Sequential ○ 複数の子ノードを持つ ○ 子ノードは順次実行される ● Parallel ○ 複数の子ノードを持つ ○ 子ノードは並列実行される ○ (オプショナルなパラメータとして並列度も 持つ)

Slide 20

Slide 20 text

Copyrights(c) Henry, Inc. All rights reserved. モデルを使ってワークフローを記述 ● コンストラクタでオブジェクトを組み立てる ○ 単にインスタンスを作っているだけだが、ワークフ ローの構造を宣言的に表現できている ○ この手法を Poor man’s DSL と呼ぶことにする (https://github.com/zsmb13/VillageDSL から借用) ● 細かいところ: ○ Task: コンストラクタの引数がラムダ式だけなの で `()` を省略して呼び出せる ○ Parallel / Sequential: コンストラクタ引数を `vararg`(可変長引数)として定義しているので、 `listOf` とかを使わなくても複数の子要素を直接 渡せる ○ 組み立てたワークフローは `run` でそのまま実行 可能 ②ワークフローを記述

Slide 21

Slide 21 text

Copyrights(c) Henry, Inc. All rights reserved. Poor man’s DSL ● シンプル。他のプログラミング言語でもできる ● Kotlinだとさらに ○ newを書かなくていいのでスッキリ記述できる ○ 名前付き引数でパラメータの意味が明示的になる ● これがDSLの基本 ○ 他のアプローチでも結局このオブジェクト構造を組み立てるのはやる ○ これを如何にスタイリッシュに書けるようにするかで、より “DSL” らしくなっていく

Slide 22

Slide 22 text

Copyrights(c) Henry, Inc. All rights reserved. ● 意図や構造が見えやすい ○ ワークフローの構造がダイレクトに表現される ○ 実行制御等の詳細は仕組み側に隠蔽されるため、本質的なことだけに集中したコードになる ● ミスが発生しにくい ○ DSL利用側は間違える余地が少ない ○ コードを見渡しやすい ● 関心の分離 ○ 業務上の関心事の表現 vs 表現を処理するための内部の仕組み ○ 関心が分離されているとそれぞれ進化させやすい ■ 例1: ExecutorServiceをCoroutineに置き換える ■ 例2: 各タスクの前後にログ出力を仕込む ■ 例3: 失敗したタスクを自動リトライする ■ これらの変更はDSL利用側には手を入れずにでき、しかもすべての利用箇所が恩恵を受け る DSL的なやり方の利点

Slide 23

Slide 23 text

Copyrights(c) Henry, Inc. All rights reserved. KotlinでDSL的なことをやる 〜 応用: もっとDSLらしくする 〜

Slide 24

Slide 24 text

Copyrights(c) Henry, Inc. All rights reserved. テクニック1. レシーバー付き関数リテラル ● KotlinのDSLでよく使われるテクニック ● 例1: Gradle ● 例2: HTMLビルダー (Kotlin公式サイト https://kotlinlang.org/docs/type-safe-builders.html )

Slide 25

Slide 25 text

Copyrights(c) Henry, Inc. All rights reserved. コードのしくみ Gradleの `dependencies` ブロックも どきを実現する簡易的なコードを書い てみる `dependencies` ブロックは依存 (Dependency)のリストを組み立てるも のとしてデザインする →

Slide 26

Slide 26 text

Copyrights(c) Henry, Inc. All rights reserved. 1⃣ 2⃣ 1⃣ Dependency ● 依存先を表すモデル 2⃣ DependenciesBuilder ● `List` を作るため のビルダー ● `api` / `implementation` メソッド を呼び出すと`dependencies` フィールドに `Dependency` のイ ンスタンスが追加される

Slide 27

Slide 27 text

Copyrights(c) Henry, Inc. All rights reserved. 3⃣ dependencies 関数 ● ユーザーに公開する DSL関数本体 ● 引数として関数を受け取る ○ この関数リテラルにはレシーバー ( `DependenciesBuilder.`) が付いて いるので、関数内からビルダーの インスタンス直接にアクセスできる ● やってること: ○ ビルダーのインスタンスを作って ○ `block` に渡して実行して ○ 実行後のビルダーから `dependencies` を取り出す 3⃣

Slide 28

Slide 28 text

Copyrights(c) Henry, Inc. All rights reserved. さっきのワークフローの例 ● 構造をダイレクトに表現するという 意味では Poor man’s DSL と同じ ○ このDSLは先ほどのモデルを組み立て るビルダーとして機能する ● Pros: ○ ちょっとカッコよくなった ○ オプショナルなパラメータである `maxConcurrency` の指定が読みやす くなった ● Cons: ○ 使う側が気持ちよく書ける分、仕組み 側の負担が増える ■ モデルの実装に加えてビルダー やDSL関数の実装が増える DSLのコードはこちら: https://github.com/kohii/kotlin-dsl-example

Slide 29

Slide 29 text

Copyrights(c) Henry, Inc. All rights reserved. テクニック2. Infix notation ● スペース区切りでメソッド呼び出せるやつ ● 例: Kotestのアサーション ● 例2: Gradleでも見かける

Slide 30

Slide 30 text

Copyrights(c) Henry, Inc. All rights reserved. しくみ ● `infix` をつけた関数は `.` と `()` を省略 して呼び出せる ● Pros: ○ より英文らしく書ける /読める ○ 普通のメソッド呼び出しより左右が対等ぽく 書ける ● Cons: ○ 乱用すると明瞭性が損なわれる

Slide 31

Slide 31 text

Copyrights(c) Henry, Inc. All rights reserved. さっきのワークフローの例 ● ワークフローの構造を宣言する代わりに、 タスクとタスク間の依存を宣言する DSL ○ `task2 dependsOn task1` =「task2は task1に依存する」=「task2はtask1が終 わってから実行する」 ○ 実行順序は依存関係から導き出せる ● 利用者の関心がワークフローの構造では なく、タスク間の依存関係の場合はベター なDSLと言える ○ (`infix` がこの違いをもたらすわけでは ないが...) DSLのコードはこちら: https://github.com/kohii/kotlin-dsl-example

Slide 32

Slide 32 text

Copyrights(c) Henry, Inc. All rights reserved. テクニック3. Operator overloading ● オペレーター(`+` とか `-` とか)をオーバーロードできる ● 例1: Algoliaの検索クエリDSL(取得するプロパティを指定) ● 例2: Coroutine(coroutine context の設定)

Slide 33

Slide 33 text

Copyrights(c) Henry, Inc. All rights reserved. ● `operator fun` で定義できる ○ `a.unaryPlus()` → `+a` ○ `a.unaryMinus()` → `-a` ○ `a.not()` → `!a` ○ `a.plus(b)` → `a+b` ○ `a.minus(b)` → `a-b` ○ `a.times(b)` → `a*b` ○ その他いっぱいある ● Pros: ○ 通常のメソッドより短く書ける ○ ふるまいが演算子の意味と合致する場 合に直感的に読める ● Cons: ○ 演算子を一般的な意味と異なる意味で 用いた場合に可読性が低下する しくみ

Slide 34

Slide 34 text

Copyrights(c) Henry, Inc. All rights reserved. 今日話すこと 1. コードは表現 2. DSLについて 3. KotlinでDSL的なことをやる 4. ヘンリーでやったこと 5. 用法・容量

Slide 35

Slide 35 text

Copyrights(c) Henry, Inc. All rights reserved. ヘンリーでやったことの紹介 前提: ヘンリーには診療報酬制度に基づく医療費の会計機能がある ● 診療報酬制度 = 医療費の計算・請求のルール ○ 提供した医療行為ごとに定められた点数に基づき医療費を計算し、患者や保険組合に請求 ● こんな感じの会計データを作成する ○ ↓は先日耳鼻科にかかったときの明細 ○ 1点10円で、通常そのうちの 3割を会計時に支払う

Slide 36

Slide 36 text

Copyrights(c) Henry, Inc. All rights reserved. 診療報酬制度に基づく会計内容のチェック機能 ● 「ある条件を満たしたらこっちも追加で算定できる」みたいなのがある ○ 例: 初診料(288点)算定時に、6歳未満の場合は乳幼児加算(75点)を算定できる。ただし 時間外 加算、休日加算、深夜加算を加算する場合は算定できない。 ● ヘンリーに求められる機能: ○ ① 条件を満たしていたら自動で算定する( or 算定するよう提案する) ○ ② 逆に条件を満たしていないのに算定していたら警告する ○ 元々はそれぞれ手続き的に実装していた

Slide 37

Slide 37 text

Copyrights(c) Henry, Inc. All rights reserved. 算定ルールを定義するDSL ● ※コードは雰囲気 ● ここから次のような挙動を導き出せる ○ ① `baseCost` (初診料) を算定時に、 `requirements` をすべて満たしていた 場合、`target` (乳幼児加算) を自動で算 定する ○ ② `target` (乳幼児加算) を算定してい るが `baseCost` (初診料) が存在しな い、もしくは `requirements` を満たさ ない場合は警告する

Slide 38

Slide 38 text

Copyrights(c) Henry, Inc. All rights reserved. やってみての所管 ● 成果: ○ 処理や手続きではなく仕様を直接表すコードになった ○ コード量が約7〜9割減(DSL利用側) ● トレードオフ: ○ 利用側が楽になる分、仕組み側に難しさを押し付ける ■ 制度が複雑かつバリエーションが無限にあり設計が大変 ○ それなりにキャッチアップコストがある

Slide 39

Slide 39 text

Copyrights(c) Henry, Inc. All rights reserved. 今日話すこと 1. コードは表現 2. DSLについて 3. KotlinでDSL的なことをやる 4. ヘンリーでやったこと 5. 用法・容量

Slide 40

Slide 40 text

Copyrights(c) Henry, Inc. All rights reserved. 社内で使う用ならあまり凝ったことやらない方がいいかも ● カッコいいのやりたくなるけど... ○ コード量増える ○ それをメンテナンスするのはチーム ● だいたいの場合は Poor man’s DSL で十分

Slide 41

Slide 41 text

Copyrights(c) Henry, Inc. All rights reserved. 利用する側が多いほど仕組みを凝るコストが見合う DSLを利用する側の数 アプローチ メモ 少 DSL化しない 利用箇所が少ないなら、 DSLの仕組みをメンテナ ンスするコストが見合わない場合が多い 中 Poor man's DSL コスパ重視 - 作成者と利用者が同じ人達の場合 - ターゲットが社内のみの場合 多 リッチなDSL 利用数が多ければクールで気持ちいい書き心地を 提供するための労力が報われる - OSSの場合 - 大企業における社内向けライブラリの場合

Slide 42

Slide 42 text

Copyrights(c) Henry, Inc. All rights reserved. 様々な視点を行き来する ● 使う側の視点、満たしたい挙動の視点、内部構造の視点を行き来して設計 ○ 全部を同時に満たさないとだめ ● 使う側のコードがどうあってほしいかから始めるのがおすすめ

Slide 43

Slide 43 text

Copyrights(c) Henry, Inc. All rights reserved. 責任を持つ ● 仕組みを作るというのは「制約と誓約」 ○ 縛りを設けることでより力を発揮 ● 制約の中で扱えない概念が現れたときに、土台となる設計をちゃんと見直していけ るか ○ 既存の枠組みの中で無理やり対応しようとして余計に複雑になるあるある ● 一定程度安定するまでは関わり続ける(自戒) ● 手離れすることにも責任を持つ(マジ自戒)

Slide 44

Slide 44 text

Copyrights(c) Henry, Inc. All rights reserved. まとめ ● KotlinはDSLを作りやすい言語 ● 用法・容量を守っていい感じにやろう 🚀

Slide 45

Slide 45 text

Copyrights(c) Henry, Inc. All rights reserved. Thank you We are hiring! 𝕏: @kohii00 https://jobs.henry-app.jp/

Slide 46

Slide 46 text

Copyrights(c) Henry, Inc. All rights reserved. (おまけ)ネストした関数リテラルでのスコープ制御 ● DSL関数をネストする場合、祖先のレシーバーにアクセスできてしまう ● `@DslMarker` を使うことで、直近のレシーバーに対してのみアクセスできるように 制御するできる ○ ↑のようなコードをコンパイルエラーにできる ○ 詳しくは https://kotlinlang.org/docs/type-safe-builders.html#scope-control-dslmarker

Slide 47

Slide 47 text

Copyrights(c) Henry, Inc. All rights reserved. (おまけ2)DSLによるプログラミングの基本的な構造