Open24

DMMFを読む

boritaborita

DDDも勉強途中であるが
Domain Modeling Made Functional

を読んで気になった点、調べたことなど書いていく。

全体の構成

  • Part1. Understanding the domain (ch1-3)
  • Part2. Modeling the domain (ch4-7)
  • Part3. Implementing the model (ch8-13)
boritaborita

Chapter1
shared mental model 作る際のDDDコミュニティによるガイドラインが紹介されている。

  • データ構造よりも、ビジネスイベントとワークフローに注目する
  • ドメインをサブドメインに分割する
  • 各サブドメインに対してモデルを作る
  • ユビキタス言語をを作る

なぜデータ構造よりビジネスイベントに注目するのか?ビジネスとは単なるデータではなくそれを変換するプロセスであり、そこに価値がある。だからその変換プロセスがどのように機能するかを理解することが重要なのだと。何がプロセスのトリガーになるのかを設計の中で捉えることが重要であり、これをドメインイベントと呼ぶ。

イベントストーミングで、ドメインイベントをタイムラインに沿って整理していく。

なおイベント名はOrderPlacedのように過去形でつける。それが既に起こって変わることのない事実だからだ。

boritaborita

次にCommandの話になる。Command名は命令形でつける。
コマンドは失敗することもあるが、成功するとワークフローを始動させる。
ワークフローの結果ドメインイベントが発生する。

このようにパイプラインで考えると関数型プログラミングと合うのだと。
コマンドのトリガーになるのは別のイベントの場合もあるし、スケジューラーやモニタリングシステムのこともあると。
個人的にコマンドの理解が十分ではないので以下のあたり整理してみてもいいかも
https://blog.ttulka.com/events-vs-commands-in-ddd/
https://enterprisecraftsmanship.com/posts/cqrs-commands-part-domain-model/
https://medium.com/ingeniouslysimple/command-vs-event-in-domain-driven-design-be6c45be52a9

boritaborita
probrem space(real world)
* (sub?)domains

solution space(domain model)
* bounded contexts

bounded contextは実装で何らかのソフトウェアコンポーネントになる(DLL, サービス、名前空間、etc)
このあたりは実践ドメイン駆動設計の内容と概ね同じか。

bounded contextの境界を正しく定めることが非常に重要であり(this is an artだと)、そのガイドラインが挙げられている。いくつか気になったものもある。

境界を定めるヒントとして、既存のチームや部門の境界を参考にできる、とある。
コンウェイの法則を思い出した。

Design for friction-free business workflows. ワークフローが複数のbounded contextsと作用しているとワークフローがブロックされたり遅延が起こり得るので、見直せ(autonomyにしておけ)(例え醜い設計になったとしても)、とある。

boritaborita

Chapter2

ワークフローのoutputはeventであるべきだという。
あと、ここでの顧客へのメール送信はワークフローの副作用であってoutputではないと。

Place Orderワークフローの図

上記改行できなくてつらい。live editorだとできたんだけど

boritaborita

ドメインの要求を集めたら、
永続化やオブジェクト指向のクラス設計に進むのではなく (persistence ignorance )
それらをテキストでまとめる。

Bounded context: Order-Taking

Workflow: Place Order
    triggered by:
        "OrderFormReceived" event (when Quote checkbox is not checked)
    primary input:
        an order form
    other input:
        product catalog
    output events:
        "OrderPlaced" event
    side effects:
        an acknowledgment is sent to the customer,
        along with the placed order

ワークフローに関連するデータ構造について記載してもよい。(疑似コード)

data Order =
    CustomerInfo
    AND ShippingAddress
    AND BillingAddress
    AND list of OrderLines
    AND AmountToBill

data OrderLine = 
    Product
    AND Quantity
    AND Price

data CustommerInfo = ??? // don't know yet

data WidgetCode = string starting with "W" then 4 digits
data GizmoCode = string starting with "G" then 3 digits
data ProductCode = WidgetCode  OR GizmoCode

data OrderQuantity = UnitQuantity OR KilogramQuantity
data UnitQuantity = integer between 1 and 1000
data KilogramQuantity = decimal between 0.05 and 100.00

これぐらい最低限の構造化とテキストベースの記載なら開発者以外(domain experts)に共有できるだろうと。

boritaborita

ライフサイクルのあるOrderについては各フェーズについて別々の名前で定義していく。

before validation

     data UnvalidatedOrder =
         UnvalidatedCustomerInfo
         AND UnvalidatedShippingAddress
         AND UnvalidatedBillingAddress
         AND list of UnvalidatedOrderLine

     data UnvalidatedOrderLine =
         UnvalidatedProductCode
         AND UnvalidatedOrderQuantity

after validation

     data ValidatedOrder =
         ValidatedCustomerInfo
         AND ValidatedShippingAddress
         AND ValidatedBillingAddress
         AND list of ValidatedOrderLine

     data ValidatedOrderLine =
         ValidatedProductCode
         AND ValidatedOrderQuantity

after placed order

     data PricedOrder =
         ValidatedCustomerInfo
         AND ValidatedShippingAddress
         AND ValidatedBillingAddress
         AND list of PricedOrderLine  // different from ValidatedOrderLine
         AND AmountToBill             // new

     data PricedOrderLine =
         ValidatedOrderLine
         AND LinePrice                // new

to create acknowledgment

data PlacedOrderAcknowledgment =
         PricedOrder
         AND AcknowledgmentLetter
boritaborita

最終的にワークフローをサブステップに分割して
テキストベースで記載する。

     workflow "Place Order" =
         input: OrderForm
         output:
            OrderPlaced event (put on a pile to send to other teams)
            OR InvalidOrder (put on appropriate pile)

         // step 1
         do ValidateOrder
         If order is invalid then:
             add InvalidOrder to pile
             stop

         // step 2
         do PriceOrder

         // step 3
         do SendAcknowledgementToCustomer

         // step 4
         return OrderPlaced event (if no errors)
     substep "ValidateOrder" =
         input: UnvalidatedOrder
         output: ValidatedOrder OR ValidationError
         dependencies: CheckProductCodeExists, CheckAddressExists

         validate the customer name
         check that the shipping and billing address exist
         for each line:
             check product code syntax
             check that product code exists in ProductCatalog

         if everything is OK, then:
             return ValidatedOrder
         else:
             return ValidationError
boritaborita

chapter3
ドメインモデルがどのようにして関数型プログラミングで実装されるか、について考える。

Simon Brown's C4 approach: ソフトウェアアーキテクチャを構成する4つのレベル。上位レベルは下位レベルから構成されている。

  • system context: システム全体を表す最上位レベル
  • containers: デプロイ可能な単位 (website, web service, database, etc)
  • components:
  • classes, modules: that contains methods or functions
boritaborita

分散モノリスについて。

It’s tricky to create a truly decoupled microservice architecture – if you switch one of the microservices off, and anything else breaks, you don’t really have a microservice architecture, you just have a distributed monolith!

各サービスは自律的に機能するものであるべきで、他に依存しているようならそれはただの分散させたモノリスか。

boritaborita

Bounded contexts間の通信はイベントによって行い、イベントは必要なデータを含んでいる。
このデータは単なるドメインオブジェクトとは異なり、シリアライズされコンテクスト間で共有されるために設計されている。(DTO, Data Transfer Object)

boritaborita

Chapter4

In fact, a type is just the name given to the set of possible values that can be used as inputs or outputs of a function

型とは単に、可能な値の集合のことなのか

boritaborita

Composition of Types

AND types, Product(積) types, records

type FruitSalad = {
  Apple: AppleVariety
  Banana: BananaVariety
  Cherries: CherryVariety
}

OR types, Sum(和) types, Tagged unions

type FruitSnack =
  | Apple of AppleVariety
  | Banana of BananaVariety
  | Cherries of CherryVariety

type AppleVariety =
  | GoldenDelicious
  | GrannySmith
  | Fuji
boritaborita

関数表記(引数、戻り値)でunitが見えたら、その関数には副作用がある確率が高い。

boritaborita

Chapter 5. Domain modeling with types
immutabilityが重要な関数型プログラミングにおいて、エンティティの更新はどのように扱うのか。

type Person = { PersonalId: int; Name: string }
let initialPerson = { PersonalId = 1; Name = "Joseph" }
let updatedPerson = { initialPerson with Name = "Joe" }

reduxのstateの扱いと同じ気がする。
元のrecordのフィールドを書き換えるのではなく、元のフィールド値をコピーして新しいrecordを作る。

boritaborita

Chapter 6. Integrity and Consistency

IntegrityというのはValidityのことらしい。

boritaborita

Integrity of simple values

smart constructer approach
通常のコンストラクタをprivateにするなどして利用不可にして、値の生成はvalidationありの別関数(smart constructor)で行う。

boritaborita

Make illegal case unrepresentable

型システムを活かして、不正なケースを表現不可能にする。
不正はコンパイラチェックによって防がれるので、実行時チェックやユニットテストで防ぐ必要はない。

boritaborita

ドメインロジックはステートレスな純粋関数として実装すると、テストしやすくなる。
外部サービス呼び出しや副作用を伴う処理をdependencyパラメータとして明示的に渡せるように設計する。

boritaborita

起こり得るエラーは、コンパイラでチェックできるように関数戻り値として明示的に記述したい。(total functionにしたい) => Result型

boritaborita

エラーを三つに分類

  • Domain error: ドメインルールの一部として想定されているエラー
  • Panic:
  • Infrastructure error: アーキテクチャから想定されるエラー。ネットワークタイムアウトなど。
boritaborita

Result<Ok, Error>の戻り値を持つ関数をパイプラインとして繋げるにはどうするか。
Resultをそのまま次の関数のinputにはできない。

アダプターを使って、
'a -> Result<'b, 'c>Result<'a, 'c> -> Result<'b, 'c>に変換する。
このアダプターは関数型プログラミングでbind, flatMapというらしい。

let bind f aResult =
    match aResult with
    | Ok success -> f success
    | Error failure -> Error failure
// カリー化した実装
let bind f =
    fun aResult ->
        match aResult with
        | Ok success -> f success
        | Error failure -> Error failure
boritaborita

もう一つの便利なのが 'a -> 'bResult<'a, 'c> -> Result<'b, 'c> に変換するアダプターであり、
これは関数プログラミングではmapと呼ばれるらしい。

let map f aResult =
    match aResult with
    | Ok success -> Ok(f success)
    | Error failure -> Error failure

ちなみに以下はエラーの方をmapする関数

let mapError f aResult =
    match aResult with
    | Ok success -> Ok success
    | Error failure -> Error(f failure)