🌐

①ドメインモデルの分類と②関数型DDDによる実装パターンを考える 〜「事業活動」の構成要素とは...?〜

2024/12/09に公開

サマリー

  • ドメインモデルには以下の種類がありそう

    • 🧠 業務知識: 知識レベル
    • 🏃 業務フロー: オペレーションレベル
  • 🧠業務知識(知識レベル)について、以下のような分類ができそう

    1. 業務手順の知識: プロセスモデル
    2. 業務資源情報の知識: リソースモデル
    3. 属性・値の知識: プロパティモデル
    4. 意思決定・制約条件の知識: ポリシーモデル
  • 🏃業務フロー(オペレーションレベル)について、以下の構成要素がありそう

    1. 業務連鎖: ワークフローモデル
    2. 業務イベント: イベントモデル

    ※ 「ビジネスルール」は上記全てに付随するイメージ

  • 上記はどんな文脈でも抜け漏れなく分析できるフレームとは思わないが、以下に参考にならないか

    • 人間が業務領域を理解したいときの、ver.0.1の定規として
    • AIがコード設計するときの、プロンプトに書いておくモデル分類の指示として
  • コード例もあります!

🖕 ビジネスプロセスの整理との対応(イベントストーミングを例に) 👇

所謂、戦略的DDD<->戦術的DDD の中間くらいのトピックかな、と思います。

モチベ

モデルによってとらえられる知識は、「名詞を見つける」ことに留まらない。ビジネスの活動ルールも、ドメインに含まれるエンティティと同じように、ドメインにとって中心的なのだ。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (p.17). 翔泳社. Kindle 版.

経営資源をどこに張って自分たちはどのフィールドを攻めるのか、が所謂 「ドメイン」(事業領域)ですが、そこでの 活動(事業活動/ビジネスプロセス)の一部(業務/オペレーション) がシステム化の対象になることが多いと思います。
(ドメインモデルとは?のドメインとは?の部分)

その際、一度事象をモデル化してから[1]そのモデルをコードに写していく[2]わけですが、そもそもモデル化とはどのような切り口で行えば良いのでしょうか?(例えば、モノ・コト分析など言われたりしますが)

また昨今、環境変化へのビジネスの予測・適応(あるいは環境を創っていく)にあたり、コード変更がビジネス変化のボトルネック[3]になることも多いのではないかと考えます。(SaaSビジネスなどは典型)
やはりビジネス上の概念をモデル化し、変更容易性のためにそれをコード構造と同期、カプセル化することは、一層投資価値[4]を増していそうです。

そこで、どのようなモデルの切り方/そしてそれぞれに対する典型的な実装パターンが考えられるでしょうか?そこを考えて/実装してみたいと思います。

記事のゴール

ドメインおよびそこでの活動を構成するモデルの分析にあたり、以下2点を考えていきます

  1. ある程度 汎用性があると思われる枠組み(ドメインモデルの分類)の再考
  2. その表現方法のパターン(自然言語+コード)も例示してみる

根本的にはモデルの分類も 「具体ドメイン毎に適切なフレームを見出して/進化させていく」でしかないのはそうなのですが、ある程度参考に、もしなれるなら幸いです。

ゴール①: ドメインモデルの分類を求めて

手がかりとして、モデル/概念の分類を3つの角度から考えてみます。

  • 🤔コードアーキテクチャを分解すると?の角度
    • ソフトウェア工学界隈
  • 🤔そもそも「事業活動」とは?の角度
    • 経営学?界隈
  • 🤔もっとそもそも「世界にはどのような種類のものが存在するのか?」の角度(なんか壮大になってきたぞ...)
    • 存在論界隈

先に、まとめた結果から貼っておきます

結論

分類 モデル名称 モデルの説明 現実の事業活動の例 考えられる実装パターン
知識レベル ①業務手順(プロセスモデル) 進行ステータスを含むプロセスのモデル。
業務の「手順」自体や順番のルール。
受注から出荷完了までの一連のフローにおける手順・進行状態群 Typed State Machine
ResultやEither
知識レベル ②業務資源情報(リソースモデル) 業務遂行において利用・消費するリソースの情報。
補助的な情報(マスタデータのような)も含まれる。
在庫リソース、注文履歴トラン、組織マスタなど 代数的データ型 (ADTs) ※ これは全般的に用いますが
知識レベル ③属性・値(プロパティモデル) エンティティの特徴や属性・値となる概念 住所、金額単位、文字列としての表示 newtype pattern
スマートコンストラクタ
知識レベル ④意思決定・制約条件(ポリシーモデル) プロセスの中で価値を最大化するための判断基準、戦略モデル
ビジネスの制約条件も含む
割引適用条件、クレジットチェック、承認必須条件 Active Patterns
オペレーションレベル ⑤業務フロー(ワークフローモデル) ドメインモデルを組み合わせ、実際のユースケースに即して実行する一連の手順。
タイムラインが存在する1本の業務連鎖
注文受付ワークフロー Railway Oriented/モナディック合成
オペレーションレベル ⑥業務イベント(イベントモデル) オペレーション開始・変更・終了の契機となる出来事。 「新規注文受領」イベント発生で在庫引当開始、「支払完了」で出荷処理起動など イベントストリーム+Reducer(fold)パターン

1. コードアーキテクチャの構成要素から考える

概念モデルの分類法を実現するサブクラスは縦の構造、サブクラスを利用するサービスは横の構造ととらえます。一般には、長期間変化しない安定した要求を縦の構造とします。
荻原 正義 アーキテクトの審美眼 P82

まずコードアーキテクチャ[5]の全体像を見てみます。
一般的に、タテヨコの構造になってくるかと思います。タテがシナリオ横断的なモデルで、ヨコがユースケース特化的なシナリオになりますね。

横の構造: オペレーションレベル(a.k.a. ワークフロー/ユースケース/オーケストレーション...)

  • 動的で、時間軸を持ち、特定のシナリオに特化した、実行時フロー
  • 業務フローの表現。
  • 上述の各モデルをオーケストレーションする層・なんでもいいのですがここではワークフローと呼ぶことにします

縦の構造: 知識レベル(a.k.a. Term/概念/ドメインモデル/サービス... )

  • 静的で、時間軸を持たず、汎用的な、特定の関心毎の、コードの管理のまとまり
  • 業務知識の表現(in投げたらout返してくれるサービスとしても見做せる)

この2つについて、タテが知識/ナレッジレベルのモデル、ヨコが実行/オペレーションレベルのモデルと言い換えてもいかなと思っています。

まず、一番上の切り口としてこれを採用することにします。

2. 「事業活動」の構成要素から考える

次に、ドメインにおける「事業活動」、あるいはその一部である「業務/オペレーション」はどのような要素で構成されているのか考えてみます。

プロセス(業務手順)

あらかじめ結論を言えば、経済体としての企業は「技術的変換」という変換を行い、それによって付加価値という成果を生み出している
伊丹 敬之、加護野 忠男, 2022, ゼミナール経営学入門(新装版)

「技術的変換」...関数みたいなことですかね。

まず、 付加価値を生み出す「変換」「活動」「ハタラキ」「プロセス」である動詞 が存在しています。
上記は経営のレベルの話ですが、オペレーションのレベルでもそのようなハタラキが事業「活動」の本質かと見受けられます。

業務のレベルでは、業務「手順」の知識という知識レベルのモデルが相当するのではないでしょうか。

例えば...

  • 注文受付業務の手順
    • その手順の中で取りうる状態の知識
    • 計算や手順そのものの知識(付随するビジネスルール)
    • 手順の分岐条件の知識(付随するビジネスルール)

リソース(業務資源情報)

そして、上記のinputとして必要なのが 「リソース」(経営資源) になりますね。

いわゆるヒト・モノ・カネ・情報...ですが、システムとして扱うのはそのリソースの情報(物理的実体としての商品そのものではなく、在庫数や価格情報を扱う)がメインかと思います。

(ゲーム開発やシミュレーション系のシステムを除き、物理的実体としてのモノそれ自体はプログラム上そこまで扱わないかと思います。)

業務のレベルでは、業務資源の知識という知識レベルのモデルが相当すると考えます。
業務手順のinputであり、またoutputになり得ます。

例えば...

  • 注文明細の知識
    • その情報の属性や構造の知識
    • 情報間の関係性の知識
    • その情報の不変条件の知識(付随するビジネスルール)
    • 導出的プロパティの計算式の知識(付随するビジネスルール)

ここで、「注文」と言っても業務手順としての「注文受付」と情報構造としての「注文明細」は別のモデル、と捉えることもできますね。

ポリシー(意思決定・制約条件)

そして、前述のプロセスの中で必要になるのが、意思決定かと思います
利用できるリソースを参照(ケイパビリティの判断)し、付加価値を最大化するための選択基準を提供します。
また、ビジネス上の制約条件のモデルともなり得ます。

例えば...

  • 商品発送料戦略の知識
    • ビジネスルールそのもの
    • また、条件そのもの定義
      • ex) 遠隔地は送料を1,000にするというルールにおける、遠隔地という業務用語の定義

ここは、

  • MLモデル
  • ハードコーディングされたルールベースの知識

が実体になってくるのかと思います。今回は、後者のイメージで進めます。

さて、ここで出た3つをまとめると、以下のような関係になるかと思います。

👆を、縦の構造: 知識レベル の要素とします。
👇で、横の構造: オペレーションレベル の要素も考えていきます。

ワークフロー(業務フロー)

ビジネスプロセスの中心を貫く〝背骨〟に当たるものが業務です。業務は、前工程から何らかのインプットを受け取り、何らかの処理を加えたうえで、後工程にアウトプットを送り出します。このインプット、処理、アウトプットのひと塊のセットが業務です。すべてのプロセスはこの業務が連鎖することで成り立っており、

山本 政樹. ビジネスプロセスの教科書 第2版―共感とデジタルが導く新時代のビジネスアーキテクチャ (p.29). 東洋経済新報社. Kindle 版.

知識だけが存在するのではなくもちろん実際には、

  • 業務手順を、リソースを用いながら/ポリシーに沿って実行するオペレーション

業務連鎖が存在します。
これは、手順/リソース/ポリシーのモデルをオーケストレーションして、タイムラインに沿って実行するスクリプトに相当します。

例えば...

  • 一連の注文受付業務そのもの

あまり詳しくないですが、エージェントがリソースを参照やポリシーを参照しつつプロセスを実行していくようなMLワークローにも互換可能でしょうか。

イベント(業務イベント)

また、ワークフローの実行契機やその結果としての事象/ファクトがモデリングできます。
ユーザの要求や需要がワークフローの実行契機となったり、ワークフローの実行結果が別のワークフローの実行契機となることが通常でしょう。

例えば...

  • ユーザの注文要求を元に、注文受付業務が開始される
  • 注文が確定した、という事実が、商品発送業務の契機となる
  1. 「事業活動」の構成要素から考える のまとめとして、
    今回登場した5要素を、先ほどの軸にはめていきましょう。

3. 世界の構成要素から考える

壮大で草

ここまで2軸5要素を考えてはみましたが、例えば、ユーザという情報に関連する名前という概念はどう表現すれば良いのでしょう?リソースとも少し違う気がします。
まだまだ補完すべき要素がありそうです。

そこで、もっと広く・網羅的に考えるべく、「存在するものの種類」とはなんぞや、について訪ねてみます。

「世界にはいったい何が存在するのか」これが存在論の最初の問いであり、究極の問いでもある。
〜(中略)〜存在論者は、「何が存在するのか」という問いに対して、あれやこれやのものを枚挙するという答え方を求めているわけではない。〜(中略)〜あれやこれやのものが属するリストを作ろうとしているのだ
倉田 剛 現代存在論講義 I 2017

ヒントとして、上記書籍で出てきた存在論的セクステッド(🤔?)というフレームを参考にしてみます。

独立的継続物 依存的継続物 プロセス
普遍者 (1) 普遍的実体
ex) 人間・ネコ
(3) 普遍的質
ex) 頭痛・白さ
(5)普遍的プロセス
ex) 歩行・在庫情報の取得プロトコル
個別者 (2) 個別的実体
ex) 太郎くん・シロ
(4) 個別的質
ex) 太郎のあの時の頭痛・
シロの毛並みのこの白さ
(6)個別的プロセス
ex)シロの今している歩行・
このトランのポスグレからの在庫情報の取得

(Smith, B. (2005). Against Fantology.)倉田 剛 現代存在論講義 I 2017より間接的に参照・やや編集

単語が難しいですが、我々に馴染みのある表現に置き換えることができそうな雰囲気があります。

エンティティ プロパティ プロセス
抽象 (1) 構造体の型 (3) interface/trait/型クラス (5)関数の型(I/Oのプロトコル)
具体 (2) 構造体のインスタンス (4) 属性・値・メソッド (6)関数

今までのまとめには 「依存的継続物」の観点が足りなそうです

  • 例えば依存的継続物として考えられるもの...
    • 普遍者
      • ex) Showableという特徴
      • ex) Colorという属性のクラス
      • ex) 金額単位というクラス
    • 個別者
      • ex) display()の実装
      • ex) "white"という具体の属性
      • ex) yenという具体type

上記の各象限の関係性はこんな感じです。(整列できてないですが、おまけ程度に🙏)

is-apart-ofなどの関係の話になってくるかと思います。
具体/抽象の区分は今回は深掘りしません。

依存的継続物を、プロパティモデル(属性・値)と捉え、分類に補完します。
この6要素で、ゴール① ドメインモデルの分類はfixします!

ゴール②: それぞれの表現方法のパターンを考える!

  • 上述した6分類のそれぞれについて、以下3パターンの表現を書いていきます
    • ビジュアル
    • 自然言語(日本語)
    • コード(Kotlin)
      • 理由はログラスの現在のプライマリ言語なので
  • お題は ECビジネスにおける、「商品注文受付」業務を使います。(なんか芸がなくてすいません)

まず、全体の静的構造のビジュアル表現です(対象と矢印のカテゴリ図?特に名称は無いですが)

  • イベントストーミングが事象・流れに着目するとすると、こちらは関係性・構造に着目した表現になります

① プロセスモデルの表現

  • 例: 注文受付手順
  • 諸説考え方があると思いますが、システム化対象であるオペレーションの構成要素(ハタラキ)そのものであるため、これが本来主軸であるのではないか?と思っています。

ビジュアル表現

自然言語(日本語)表現

イメージこんな感じになるかと思います。

コード(Kotlin)表現

  • typed state machine風に実装します。
  • これらの状態分類を更に、"成功or失敗 のどちらかである、という1階上の文脈"でラップする(あるいは乗せる)と、railway-orientedな書き方になります。
  • 別のメンバが、よりここにフォーカスした記事を書いているので、こちらもご参考ください!

ちなみに... 上記をプロンプトに入れてのo1実装一発目はこんな感じ
いい感じの雰囲気(外部ライブラリの特定の構文を指定とか厳しそうだけど、頑張ってくれている😄)

package order_taking.order_taking_process

import com.github.michaelbull.result.*
import com.github.michaelbull.result.coroutines.coroutineBinding

import order_taking.user.Address
import order_taking.user.User

// input
data class OrderRequest(
    val userId: String,
    val productId: String,
)

// state
sealed interface OrderTakingState
    // happy path
    data class Checked(
        val detail: OrderDetail,
        val user: User.Active
    ) : OrderTakingState

    data class AwaitingShipment(
        val detail: OrderDetail,
        val user: User.Active,
        val shipping: ShippingInfo
    ) : OrderTakingState

    // error path
    data class InvalidUser(
        val detail: OrderDetail,
    ) : OrderTakingState

    data class UnsupportedDestination(
        val detail: OrderDetail,
    ) : OrderTakingState


// I/O Actions Protocol
typealias FetchUserInfo = suspend (String) -> Result<User.Active, String>
typealias DetermineShippingFee = suspend (address: Address.Valid) -> Int


// Operations
internal suspend fun verifyClient(
    fetchUserInfo: suspend (String) -> Result<User.Active, String>, //参照リソース
    request: OrderRequest // input
): Result<Checked, InvalidUser> =
    coroutineBinding {
        val detail = OrderDetail(request.userId, request.productId)
        val userInfo = fetchUserInfo(request.userId).bind()

        Checked(detail, userInfo)
    }.mapError { _ ->
        val detail = OrderDetail(request.userId, request.productId)
        InvalidUser(detail)
    }

internal suspend fun determineShipping(
    determineShippingFee: DetermineShippingFee,
    orderInfo: Checked
): AwaitingShipment = AwaitingShipment(
    orderInfo.detail,
    orderInfo.user,
    ShippingInfo(
        orderInfo.user.address,
        determineShippingFee(orderInfo.user.address)
    )
)

// 簡単のため、インラインで定義するリソース情報たち
// 注文明細
data class OrderDetail(
    val userId: String,
    val productId: String,
)

// 配送情報
data class ShippingInfo(
    val address: Address.Valid,
    val cost: Int
)

✅ 状態がADTsで表現されています
✅ それぞれの手順もexportしています

② リソースモデルの表現

  • 例: ユーザリソース
  • テーブル駆動設計はもちろん、クラス駆動アプローチでもここがまず注目されてくるかと思います
    • モノ然 としているため

ビジュアル表現

自然言語(日本語)表現

notion

コード(Kotlin)表現

package order_taking.user

import com.github.michaelbull.result.*


// entity
sealed class User {
    abstract val id: String

    data class Unauthenticated(
        override val id: String
    ) : User()

    data class Active(
        override val id: String,
        val address: Address.Valid
    ) : User()

    data class Revoked(
        override val id: String,
        val address: Address,
        val reason: String,
    ) : User()

    // derived properties
    fun isActive(): Boolean = this is Active
}

// state transition actions
fun User.Unauthenticated.activate(address: Address.Valid): User.Active =
    User.Active(id = this.id, address = address)

fun User.Active.revoke(reason: String): User.Revoked =
    User.Revoked(id = this.id, address = this.address, reason = reason)

// read resource
typealias FindUserById = (String) -> Result<User, String>
fun getActiveUser(
    findUserById: FindUserById
): (String) -> Result<User.Active, String> = { userId ->
    findUserById(userId).andThen { user ->
        when (user) {
            is User.Active -> Ok(user)
            else -> Err("DomainError: UserNotActive")
        }
    }
}

✅ 状態がADTsで表現されています
✅ リソースのqueryも配置されています

  • 本来は更新commandも配置されます

③ 属性・値の表現

  • 例: 住所

ビジュアル表現

自然言語(日本語)表現

notion

コード(Kotlin)表現

package order_taking.user

import com.github.michaelbull.result.*


// value
sealed class Address: Showable {
    sealed class Valid : Address() {
        data class Domestic(
            val prefecture: Prefecture,
            val detail: DetailedAddress
        ) : Valid()

        data class Overseas( // 海外は県がnull、なんてことはしない 
            val detail: DetailedAddress
        ) : Valid()
    }

    data class Invalid(
        val prefecture: Prefecture?,
        val detail: DetailedAddress,
        val reason: String,
    ) : Address()

    // derived properties
    fun isDomestic(): Boolean = this is Valid.Domestic
    fun isValid(): Boolean = this is Valid
    override fun display() = when (this) {
        is Valid.Domestic -> "${prefecture.displayName} ${detail.value}"
        is Valid.Overseas -> "海外 ${detail.value}"
        is Invalid -> "不正な住所: ${detail.value} (${reason})"
    }
    fun verify(inputPrefecture: Prefecture?): Valid {
        return when (this) {
            is Valid -> this
            is Invalid -> {
                if (inputPrefecture != null) {
                    Valid.Domestic(
                        prefecture = inputPrefecture,
                        detail = this.detail
                    )
                } else {
                    Valid.Overseas(this.detail)
                }
            }
        }
    }

    // transformation
    companion object {
        fun from(prefectureInput: String?, detailInput: String): Result<Address, String> {
            val detailStr = detailInput.trim()
            if (detailStr.isEmpty()) {
                return Err("住所詳細が空です") //そもそもNonEmptyStringを受け取るでもいい
            }

            val prefectureStr = prefectureInput.orEmpty().trim()
            return if (prefectureStr.isEmpty()) {
                Ok(Valid.Overseas(DetailedAddress(detailStr)))
            } else {
                Prefecture.from(prefectureStr)
                    .map { pf -> Valid.Domestic(pf, DetailedAddress(detailStr)) }
            }
        }
    }
}

@JvmInline
value class DetailedAddress(val value: String) // new type。ラップしただけ

enum class Prefecture(val displayName: String) {
    北海道("北海道"),
    // 中略
    鹿児島県("鹿児島県"),
    沖縄県("沖縄県");

    companion object {
        private val displayNameMap: Map<String, Prefecture> = entries.associateBy { it.displayName }

        fun from(name: String): Result<Prefecture, String> {
            val trimmed = name.trim()
            return displayNameMap[trimmed]?.let { Ok(it) } ?: Err("不正な県名: $name")
        }
    }

    override fun toString(): String {
        return displayName
    }
}

// 本来はどこかcommon的なところに
interface Showable {
    fun display(): String
}

✅ ADTs での定義により、海外住所には県の概念がないという制約が表現されています
✅ newtype を定義しています

  • value class DetailedAddress(val value: String)
    ✅ 取ってつけたみたいですが、Address: Showable->住所表示されるという特徴を持ちます
    • Showableはsealedではない、開いた・グローバルな抽象です。色々なところで実装される必要があるため。
    • 逆に、その必要がないAddressなどは閉じた・ローカルな抽象です。openである必要がない場合は、以下からこのようにした方がよさそうです。
      • 網羅性チェック可能
      • あちこちに実装先が散らばっておらず、一目で把握可能

④ ポリシーモデルの表現

  • 例: 送料算出ポリシー
  • (これは典型的には、「オーバーブッキングポリシーのやつ」です)
  • ここは以前フォーカスした記事を書いたので、もしご興味あれば眺めてみてください。

ビジュアル表現

自然言語(日本語)表現

notion

コード(Kotlin)表現

package order_taking.shipping_estimate

import order_taking.user.Address
import order_taking.user.Prefecture


// 実際に export する算定ロジック
typealias EstimateShippingFee = (Address.Valid) -> Int
    val shippingFeeBase: EstimateShippingFee = { location ->
        decideStrategy(basePolicy)(location)
    }
    val shippingFeeInCampaign: EstimateShippingFee = { location ->
        decideStrategy(freeCampaignPolicy)(location)
    }

// classification definition
private sealed interface DeliveryArea {
    data object Neighborhood : DeliveryArea   // 近隣
    data object Remote : DeliveryArea         // 遠隔地
    data object Overseas : DeliveryArea       // 海外

    companion object {
        private val neighborhoodPrefectures = setOf(
            Prefecture.東京都, Prefecture.神奈川県, Prefecture.埼玉県,
            Prefecture.千葉県, Prefecture.山梨県, Prefecture.群馬県,
        )

        fun classify(location: Address.Valid): DeliveryArea = when (location) {
            is Address.Valid.Domestic ->
                if (location.prefecture in neighborhoodPrefectures) Neighborhood else Remote
            is Address.Valid.Overseas -> Overseas
        }
    }
}

// Strategies
private typealias ShippingCostStrategy = (DeliveryArea) -> Int
    private val basePolicy: ShippingCostStrategy = { area ->
            when (area) {
                DeliveryArea.Neighborhood -> 500
                DeliveryArea.Remote -> 1000
                DeliveryArea.Overseas -> 3000
            }
        }
    private val freeCampaignPolicy: ShippingCostStrategy = { area ->
            when (area) {
                DeliveryArea.Neighborhood, DeliveryArea.Remote -> 0
                DeliveryArea.Overseas -> 3000
            }
        }

// helper
private fun decideStrategy(
    policy: (DeliveryArea) -> Int
): (Address.Valid) -> Int {
    return { location ->
        policy(
            DeliveryArea.classify(location)
        )
    }
}

✅ 分類の定義と、意思決定ポリシーが配置されています
✅ 具体のストラテジーをexportしています

洗練

面白くなってくるのが、ここまで書いてみるとどうやら、 概念と DeliveryAreaカタマリ化した方が良さそうか?、などとも見えてくることですね。

あるいは、その県と合わせて、Adrressも併せLocationパッケージに移動した方がマトマリがよさそうだ、などなど...ドメインエキスパートあるいはエンジニアと議論しましょう!

⑤ ワークフロー -> ⑥ イベントの表現

  • 最後に、前述のモデルたちをオーケストレーションする層を書きます。
package order_taking

import com.github.michaelbull.result.*
import com.github.michaelbull.result.coroutines.coroutineBinding

import common.DomainEvent

import order_taking.order_taking_process.OrderRequest
import order_taking.order_taking_process.determineShipping
import order_taking.order_taking_process.verifyClient
import order_taking.shipping_estimate.shippingFeeBase
import order_taking.shipping_estimate.shippingFeeInCampaign
import order_taking.user.FindUserById
import order_taking.user.getActiveUser


// resulting events
data class OrderConfirmed(
    val userId: String, // 今回省略
    val productId: String,
    val shippingFee: Int,
    val address: String,
    override val occurredAt: String,
) : DomainEvent

data class OrderFailed(
    val userId: String, // 今回省略
    val productId: String,
    override val occurredAt: String,
) : DomainEvent

// workflow
// API層からコールされる想定
suspend fun orderTakingWorkflow(
    findUser: FindUserById, // 具体IOアクションのDI
    request: OrderRequest
    // ここでは、ワークフローはイベントを返すだけとする
    // リソースの更新は、API層で、イベント -> リソースの投影処理をコールする想定
): Result<OrderConfirmed, OrderFailed> {
    
    // config
    val campaigning = false
    val shippingFeeStrategy = if (campaigning) shippingFeeInCampaign else shippingFeeBase

    // bake IO Action
    val activeUser = getActiveUser(findUser)

    // workflow main
    return coroutineBinding {
        val checkedState = verifyClient(activeUser, request).bind()
        val readyOrder = determineShipping(shippingFeeStrategy, checkedState)
        readyOrder
    }.map {
        OrderConfirmed(
            request.userId,
            request.productId,
            it.shipping.cost,
            it.user.address.toString(),
            "now",
        )
    }.mapError { order ->
        OrderFailed(
            order.detail.userId,
            order.detail.productId,
            "now"
        )
    }
}

✅ 具体のIO処理などの依存性をDIします
✅ API層に、イベントをリターンします

  • 今回の例ではリソースの更新は、イベントから投影する関数を別途API層からコールするイメージです

おわりに!

実装寄りのトピックについては、言語機能や開発を取り巻く状況などの前提が昔とはすごい勢いで変わっていっているので、バックエンド領域においても必要に応じ考え方・手法もアップデートしていけば良いかと思います。

しかしやはり、「そもそも業務領域ってなに?事象はどのように目的に応じてキリトリするといいの?」という方法論については、不変性が高いように思います。

ログラスの開発は、

  • 経営管理ドメイン x DDD(関数型概念輸入も積極🦾) x フルサイクル

なので、そもそも経営、事業活動ってなに...?のような広めの構造を学習できる機会に恵まれていると感じます。

今後とも、ドメイン(事業活動)/ 事象をいい感じにキリトリするには?について学習(オントロジー/応用存在論的な話も掘りたい)・検証していきたいです🏃‍♀️‍➡️

脚注
  1. もちろん、このステップはas-is業務をそのまま写すのではなく、テック視点からモデルの改善を図っていく・そもそも既存に存在しない業務を創っていく活動も含むニュアンスです ↩︎

  2. もちろん、全てそのまま写すのではなく、典型的にはパフォーマンスなどの絡みで「非正規化」したりしますよね ↩︎

  3. 同時に価値の源泉であることは言うまでもなく ↩︎

  4. コアサブドメインを中心に。そのへんの話はこの記事のフォーカスではないです ↩︎

  5. マイクロサービス間の構造とか、インフラレベルも含めた「システムアーキテクチャ」ではないニュアンスで言っています ↩︎

株式会社ログラス テックブログ

Discussion