Open11
typescript functional DDD考察
関心事 | F#(DMMF) | typescriptでは? |
---|---|---|
エラーハンドリング | ・result型 | ・Result型を独自で作る ・https://effect.website/ effect-tsとかのライブラリ |
逐次処理(手続的記載のためのshell) | ・パイプ | ・不要とする(愚直に縦に書いてしまう。上から下に) ・パイプを独自で作る ・effect-tsとかのライブラリ |
ADT(データモデル) | ・Discriminated Union | ・union type (パターンマッチ的模倣をする場合、いわゆるタグ付きユニオン) |
パターンマッチ(データによる振る舞いの多態) | ・パターンマッチ | ・タグ付きユニオンをPJ規約にしてswitch最後nevweで網羅性チェックするやつ ・ライブラリ使う |
値オブジェクト | ・Smart Constructor | ・コンパニオンオブジェクトhttps://typescriptbook.jp/tips/companion-object ・zod併用 |
副作用の扱い | ・DMMFでは副作用はトップレベル関数に全部逃す感じしか書かれてなかった記憶 ・IOモナド |
・同様にトップレベル(オニオンの外周の層のことろ)に逃す →複雑になってくると厳しそう →そもそもnano serviceをFaaSに載せるような世界観なら1つ1つが深く巨大にならない ・effect-ts? ・更新系はリポジトリを返すでも良さそう |
依存性の扱い | ・DMMFでは引数のバケツリレーをしていた記憶。(いっぱいあったら1つのオブジェクトにまとめてややこしさ軽減くらい?) ・リーダーモナド |
・同様にバケツリレーを許容する →複雑になってくると厳しそう →そもそもnano serviceをFaaSに載せるような世界観なら1つ1つが深く巨大にならない ・effect-ts? |
個人のお気持ち
業務フロー的なもの -> 実装
の間に、クラスベース思考というメンタルモデルの異なるステップが挟まることが開発を難しくしていないか?
対象ドメインを、ゲームだったりではなくwebサービスバックエンドや業務システムのようなものと想定した時に、
ドメインであるところのビジネスプロセスや業務は、in-process-outおよびその連鎖の集合体である、という理解は事実に近いと思う。
業務フロー的なもの(in-process-outのモデル) -> 関数ベースの実装(in-process-outのモデル)の流れは非常に接続が容易。
もちろんこれはtypescriptでは、型->関数->型ということ。
クラス駆動の世界観は、効率性(共通化や、似たデータ・関数へのアクセスの容易さ)に価値がある。
しかし、それはコードスプリッティングの単位や、名前空間をドメインの知識を借りて行うということであり、プログラムの効率性駆動と考える。
プログラマ駆動のモデル(およびそこへの変換ステップ)を介在させずにドメイン(in-process-outのモデル)をそのままコードに写すことがドメイン駆動的とは言えないか。
参考情報
ゴール状態 <- Deriver <- Deriver <- Derver <- 元のStream
// Eq 型クラスに相当するインターフェースを定義
interface Eq<T> {
equals(a: T, b: T): boolean;
}
// 数値に対する Eq の実装
const numberEq: Eq<number> = {
equals: (a, b) => a === b,
};
// 文字列に対する Eq の実装
const stringEq: Eq<string> = {
equals: (a, b) => a === b,
};
// 汎用的な等価性関数
function isEqual<T>(eq: Eq<T>, a: T, b: T): boolean {
return eq.equals(a, b);
}
// 使用例
console.log(isEqual(numberEq, 1, 1)); // true ここでnumberEqとか渡さなきゃいけない。haskelみたいに自動解決は無理
console.log(isEqual(stringEq, 'apple', 'banana')); // false
typeの追加に対する開放閉鎖は厳しそう。
typeを追加したら、isEqualを使用する箇所を追加する必要がある。
Expression problem
データ切りで操作を抽象化 / 操作切りでデータを抽象化
やはりコンパイラによる自動決定がないと難しいか
データ増減方向で困る点、つまり昔々にプロセス志向の問題点であり、クラス志向が誕生した理由に遡る必要がある。
- 既存のブロックをいじるのでリグレッションテストが工数かかる
-> 自動テストがない昔の前提の話では? - 影響箇所が絞れずデグレる
-> コンパイルエラーになる。これも現在のような型システムが充実していない前提の話では?
// 擬似コード
const process = (data: Animal) => {
match(data)
case Goat:
return "%&guASFeh$%dfs"
case Cat:
return "huh?"
case // ここを追加するときに開放閉鎖的ではない。
// が、Animalにバリエーションが増えたときにここがエラーになるのって別に良くないか?
// むしろ明示的に追加すべきか箇所がわかる。
}