Cover Image for 読書メモ:Domain Modeling Made Functional (Part 2, Chapter 4~7)

読書メモ:Domain Modeling Made Functional (Part 2, Chapter 4~7)

概要

↑を読んだメモ

サンプルコードをRustに置き換えて遊んでたリポジトリは以下

Domain Modeling Made Functional の読書メモシリーズ は こちら

Part2 Modeling the Domain

ここからはサンプルコードが出てくるが、F#なのでRustに書き直しながら理解を深める

Chapter4 Understanding Types

Type Signature

fn add1(x: isize, y: isize) -> isize {  
    x + y  
}  
  
fn add(x: isize, y: isize) -> impl Fn(isize, isize) -> isize {  
    move |x, y| x + y  
}  
  
fn square_plus_one(x: isize) -> isize {  
    let square = move |x| x * x;  
    square(x) + 1  
}

Functions with Generic Types

fn are_equal<T: PartialEq>(x: T, y: T) -> bool {  
    x == y  
}

"Values" vs. "Objects" vs. "Variables"

  • 関数型言語ではよく "values" を使うが、OOPではよく"objects" を使う
  • Values は immutable (なのでvariablesとは呼ばない) なただのデータ
  • Objects はデータと振る舞いをカプセル化したものなのでstateを持ったデータ
  • 関数型言語では "Values" を基本的に使う( "Objects" や "Variables" よりも)

Composition of Types

struct AppleFruit {}  
struct BananaFruit {}  
struct CherriesFruit {}  
  
// AND type  
struct FruitSnack {  
    apple: AppleFruit,  
    banana: BananaFruit,  
    cherries: CherriesFruit,  
}  
  
// OR type  
enum FruitSnackEnum {  
    Apple(AppleFruit),  
    Banana(BananaFruit),  
    Cherries(CherriesFruit),  
}

"Product Types" vs. "Sum Types"

  • AND type は "Product Types" とも呼ばれる
  • OR type は "Sum Types" , "Tagged Unions", "Discriminated union", "Chois types" とも呼ばれる

Simple Types

type Kilometers = i32;

rust だと型エイリアスのこと? -> chaper5読んでわかった違う。 引数1つしか取らない tuple type の struct のこと

struct Kilometers(i32);  

Building a Domain Model by Composing Types

type CheckNumber = i32;  
type CardNumber = String;  
  
enum CardType {  
    Visa,  
    MasterCard,  
}  
  
struct CreditCardInfo {  
    card_type: CardType,  
    card_number: CardNumber,  
}  
  
enum PaymentMethod {  
    Cash,  
    Check(CheckNumber),  
    Card(CreditCardInfo),  
}  
  
type PaymentAmount = i64;  
enum Currency {  
    EUR,  
    USD,  
}  
  
struct Payment {  
    amount: PaymentAmount,  
    currency: Currency,  
    method: PaymentMethod,  
}  
  
// PayInvoice = UnpaidInvoice -> Payment -> PaidInvoice  
// ConvertPaymentCurrency = Payment -> Currency -> Payment

enum に値渡すのどう使うんだと思ってたけど、こういう使い方すれば良さそう

Modeling Optional Values

普通にOption使えばいいぐらいの話だった。

Modeling Errors

これもResult使えば良さそう。

Modeling No Value at All

これも () unit型 を使えば良さそう

Modeling Lists and Collections

これも(ry

Organizing Types in File and Projects

domainごとに以下の2ファイル作る構成にするらしい(あんまりメリットとかはピンときてない)

Foo.Types.rs
Foo.Functions.rs

Chapter5 Domain Modeling with Types

Seeing Patterns in Domain Model

  • Simple values
    • OrderId とか ProductCode とか int や String だけど名前のついているもの
  • Combinations of values with AND
    • address , order など
  • Choices with OR
    • quote, unitQuantity など
  • Workflow
    • input -> output に変換するビジネスプロセス

Modeling Simple Values

struct CustomerId(i64);  
struct OrderId(i64);  
  
fn process_customer_id(id: &CustomerId) -> String {  
    let inner_val = id.0; // 内部的な値を取り出して使うこともできる  
    format!("id:{}", inner_val)  
}  
  
fn sample() {  
    let id = OrderId(42);  
    process_customer_id(&id); // ここでエラーになる  
}

実態が同じ i64 でも取り違えた時に型エラーにできるように simple values を作っておいた方がいい 制約をかけてsimple valuesを生成する方法については次のchapterで

リアルタイム性が必要で性能面で気になるなら型エイリアスを使う方法もある(型の安全性は若干失われるが)

Modeling Complex Data

徐々に作っていくので、最初は Unknown Type を使って徐々にモデリングしていく

use std::error::Error;  
  
type Undefined = dyn Error;  
  
type CustomerInfo = Undefined;  
type SippingAddress = Undefined;  
type BillingAddress = Undefined;  
type OrderLine = Undefined;  
type BillingAmount = Undefined;  
  
struct Order {  
    customer_info: CustomerInfo,  
    sipping_address: SippingAddress,  
    billing_address: BillingAddress,  
    order_lines: Vec<OrderLine>,  
    billing_amount: BillingAmount,  
}

Choice Type はこんな感じ

enum ProductCode {  
    WidgetCode(i32),  
    GizmoCode(i32)  
}  
  
enum OrderQuantity {  
    UnitQuantity(i32),  
    Kilogram(i32)  
}

Modeling Workflows with Functions

Function で 1step づつ方変換しながら workflow を実装していく的なことが書いてある。ここはまあそうだよねぐらい。

A Question of Identity: Value Objects

DDDのValue Objects の説明。Rust的には単に PartialEq をderiveすると全要素で比較されるので実現できる。

#[derive(Debug, PartialEq)]  
struct WidgetCode(String);  
  
#[derive(Debug, PartialEq)]  
struct PersonalName {  
    first_name: String,  
    last_name: String,  
}  
  
#[cfg(test)]  
mod test {  
    use super::*;  
  
    #[test]  
    fn test_widget_code_1() {  
        let widget_code1 = WidgetCode("W1234".to_string());  
        let widget_code2 = WidgetCode("W1234".to_string());  
        assert_eq!(widget_code1, widget_code2)  
    }  
  
    #[test]  
    fn test_widget_code_2() {  
        let widget_code1 = WidgetCode("W1234".to_string());  
        let widget_code2 = WidgetCode("W12345".to_string());  
        assert_ne!(widget_code1, widget_code2)  
    }  
  
    #[test]  
    fn test_name_1() {  
        let name1 = PersonalName {  
            first_name: "aaa".to_string(),  
            last_name: "bbb".to_string(),  
        };  
        let name2 = PersonalName {  
            first_name: "aaa".to_string(),  
            last_name: "bbb".to_string(),  
        };  
        assert_eq!(name1, name2)  
    }  
  
    #[test]  
    fn test_name_2() {  
        let name1 = PersonalName {  
            first_name: "aaa".to_string(),  
            last_name: "bbb".to_string(),  
        };  
        let name2 = PersonalName {  
            first_name: "ccc".to_string(),  
            last_name: "ddd".to_string(),  
        };  
        assert_ne!(name1, name2)  
    }  
}

A Question of Identity: Entities

Entities(一意識別できるIDを持ったもの)をどう実装するか。Rustでは PartialEq を自前で実装することで実現できそう。

#[derive(Debug, Clone)]  
struct ContactId(i64);  
  
#[derive(Debug, Clone)]  
struct Contact {  
    contact_id: ContactId,  
    phone_number: String,  
    email_address: String,  
}  
  
impl PartialEq for Contact {  
    fn eq(&self, other: &Self) -> bool {  
        self.contact_id.0 == other.contact_id.0  
    }  
}  
  
#[cfg(test)]  
mod test {  
    use super::*;  
  
    #[test]  
    fn test_equal() {  
        let contact_a = Contact {  
            contact_id: ContactId(1),  
            phone_number: "123-4567".to_string(),  
            email_address: "aaaaa@example.com".to_string(),  
        };  
        let contact_b = Contact {  
            phone_number: "890-1234".to_string(),  
            email_address: "bbbb@example.com".to_string(),  
            ..contact_a.clone()  
        };  
        assert_eq!(contact_a, contact_b)  
    }  
  
    #[test]  
    fn test_not_equal() {  
        let contact_a = Contact {  
            contact_id: ContactId(1),  
            phone_number: "123-4567".to_string(),  
            email_address: "aaaaa@example.com".to_string(),  
        };  
        let contact_b = Contact {  
            contact_id: ContactId(2),  
            ..contact_a.clone()  
        };  
        assert_ne!(contact_a, contact_b)  
    }  
}

基本的にimmutableなデータを使ってworkflowを構築していくが、Entityは状態が変わる可能性がある。その場合は clone して一部書き換えたものを使うというユースケースが頻繁に発生する。RustだとEntityにはCloneをderiveしておいた方が良さそう。

Aggregate

Order と OrderLine のようにどちらも Entity だが依存関係がある場合、依存関係のトップレベルのEntityをAggregateと呼ぶ。

#[derive(Debug, PartialEq, Clone)]  
struct Quantity(i64);  
  
#[derive(Debug, PartialEq, Clone)]  
struct Price(i64);  
  
#[derive(Debug, PartialEq, Clone)]  
struct OrderLineId(i64);  
  
#[derive(Debug, PartialEq, Clone)]  
struct CustomerId(i64);  
  
#[derive(Debug, PartialEq, Clone)]  
struct ProductCode(String);  
  
#[derive(Debug, PartialEq, Clone)]  
struct Product {  
    product_code: ProductCode,  
}  
  
#[derive(Debug, PartialEq, Clone)]  
struct OrderLine {  
    order_line_id: OrderLineId,  
    product: Product,  
    quantity: Quantity,  
    price: Price,  
}  
  
#[derive(Debug, PartialEq, Clone)]  
struct Order {  
    order_lines: Vec<OrderLine>,  
    customer_id: CustomerId,  
    total_price: Price,  
}  
  
fn change_order_line_price(order: &Order, order_line_id: &OrderLineId, new_price: &Price) -> Order {  
    let order_line_pos = order  
        .order_lines  
        .iter()  
        .position(|line| line.order_line_id == *order_line_id)  
        .unwrap(); // ほんとはエラー処理必要  
  
    let order_line = order.order_lines.get(order_line_pos).unwrap().clone();  
  
    let new_order_line = OrderLine {  
        price: new_price.clone(),  
        ..order_line  
    };  
  
    let mut new_order_lines = order.order_lines.clone();  
  
    let _old_order_line = std::mem::replace(&mut new_order_lines[order_line_pos], new_order_line);  
  
    let new_total_price = new_order_lines  
        .iter()  
        .map(|line| line.price.0 * line.quantity.0)  
        .sum::<i64>();  
  
    Order {  
        order_lines: new_order_lines,  
        total_price: Price(new_total_price),  
        ..order.clone()  
    }  
}  
  
#[cfg(test)]  
mod test {  
    use super::*;  
  
    #[test]  
    fn test_change_order_line_price() {  
        let order_line = OrderLine {  
            order_line_id: OrderLineId(1),  
            product: Product {  
                product_code: ProductCode("W-123".to_owned()),  
            },  
            quantity: Quantity(1),  
            price: Price(1_000),  
        };  
        let order = Order {  
            order_lines: vec![  
                OrderLine {  
                    order_line_id: OrderLineId(1),  
                    ..order_line.clone()  
                },  
                OrderLine {  
                    order_line_id: OrderLineId(2),  
                    ..order_line.clone()  
                },  
            ],  
            customer_id: CustomerId(1),  
            total_price: Price(2_000),  
        };  
  
        let expected = Order {  
            order_lines: vec![  
                OrderLine {  
                    order_line_id: OrderLineId(1),  
                    price: Price(2_000),  
                    ..order_line.clone()  
                },  
                OrderLine {  
                    order_line_id: OrderLineId(2),  
                    ..order_line.clone()  
                },  
            ],  
            customer_id: CustomerId(1),  
            total_price: Price(3_000),  
        };  
  
        let actual = change_order_line_price(&order, &OrderLineId(1), &Price(2_000));  
  
        assert_eq!(actual, expected)  
    }  
}

Chapter6 Integrity and Consistency in the Domain

  • integrity (or validity):データが正しくビジネスルールに沿っているかどうか
  • consistency:ドメインモデルの異なるパーツの関係性が事実と一致しているか

IntegrityとConsistencyを区別せずにvalidityって言ってしまってた気がする

The Integrity of Simple Values

use anyhow::{anyhow, Result};  
  
#[derive(Debug, PartialEq, Clone)]  
struct UnitQuantity(i64);  
  
impl UnitQuantity {  
    fn try_new(quantity: i64) -> Result<Self> {  
        if quantity < 1 {  
            return Err(anyhow!("UnitQuantity can not be negative"));  
        } else if quantity > 1_000 {  
            return Err(anyhow!("UnitQuantity can not be more then 1_000"));  
        }  
        Ok(UnitQuantity(quantity))  
    }  
}  
  
#[cfg(test)]  
mod tests {  
    use super::*;  
  
    #[test]  
    fn test_unit_quantity() {  
        let actual = UnitQuantity::try_new(100).unwrap();  
        assert_eq!(actual, UnitQuantity(100));  
    }  
}

rust でやる場合は try_new を生やせば良さそう。デフォルトの構造体生成の口は潰せないので、new or try_new を使うように気をつける必要はありそう。

Unit of Measure

単位を表す型を使う方法。これは便利そうだけどRustの標準の機能だと表現できなさそう。uom とか使えばできそうではある。

Endorcing Invariants with the Type System

incariants: 状態が不変なもの。空ではない配列の例などが出ているがこれもRust標準だと難しそう nonempty などを使う必要がある(F#でも標準では無理そう)

Capturing Business rule in the Type System

validかどうかや、片方必須などをロジックではなく型で表現する方法

#[derive(Debug, PartialEq, Clone)]  
struct EmailAddress(String);  
  
#[derive(Debug, PartialEq, Clone)]  
struct VerifiedEmailAddress(String);  
  
enum CustomerEmail {  
    Verified(VerifiedEmailAddress),  
    Unverified(EmailAddress),  
}  
  
struct EmailContactInfo {  
    email: VerifiedEmailAddress,  
}  
  
struct PostalContactInfo {  
    postal_code: String,  
}  
  
enum ContactInfo {  
    Email(EmailContactInfo),  
    Postal(PostalContactInfo),  
    Both(EmailContactInfo, PostalContactInfo),  
}  
  
struct Contact {  
    name: String,  
    contact_info: ContactInfo,  
}
#[derive(Debug, PartialEq, Clone)]  
struct UnvalidatedAddress(String);  
  
#[derive(Debug, PartialEq, Clone)]  
struct ValidatedAddress(String);  
  
fn address_validation_service(address: &UnvalidatedAddress) -> Option<ValidatedAddress> {  
    let address_str = address.clone().0;  
    if address_str.len() < 1 {  
        return None;  
    }  
    Some(ValidatedAddress(address_str))  
}  
  
struct UnvalidatedOrder {  
    sipping_address: UnvalidatedAddress,  
}  
  
struct ValidatedOrder {  
    sipping_address: ValidatedAddress,  
}

Consistency

Aggregate が一つの場合、Aggregateへの操作はatomicになるはずなのでその操作の中でConsistencyを保てるようなコードを書く必要がある 異なるコンテキストを跨ぐ場合、APIを使ってConsistencyを確認したりする。 スターバックスは2フェーズコミットを使わない にもあるように実際のビジネスの現場においてはスループット最大化のために厳格なConsistencyチェックが行えない場合もある

同じコンテキストのAggregateを跨ぐ場合 "トランザクション毎に一つAggregateしか更新されない" の原則を守ればいいが、A->Bに預金を移動させるような場合そうもいかない。そういう場合はAccountというAggregate2つを更新するのではなく。Money TransferのようなAggregateを作って2つのアカウントのIDのみを持たせるなどする。

Chapter7 Modeling Workflows as Piplines

Workflow Input

Command系のinputをどうモデリングしていくかの実例

Modeling an Order as a Set of States

Orderに対するworkflowを考えるときに、一つのモデルに複数のflagを持つようなモデリングより、state毎に型を変えてしまうようなモデリングの方が適しているという話

State Machines

操作によって状態が移り変わっていくものを表すのにstate machineを使う

  • Verified Email Address <-> Unverified Email Address
  • Empty Cart -> Actiive Cart -> Paid Cart

なぜstate machineを使うのか?

  • 各stateが異なる妥当な振る舞いを持つ場合
  • 全てのstateが明確にdocumentedな場合
  • 起こりうる全ての可能性を考慮するためのデザインツールとして

Rustでstate machineを作ってみる

struct Item {}  
  
struct ActiveCartData {  
    unpaid_items: Vec<Item>,  
}  
  
struct PaidCartData {  
    paid_items: Vec<Item>,  
    payment: f64,  
}  
  
enum ShoppingCart {  
    EmptyCart,  
    ActiveCart(ActiveCartData),  
    PaidCart(PaidCartData),  
}  
  
fn add_cart(cart: ShoppingCart, item: Item) -> ShoppingCart {  
    match cart {  
        ShoppingCart::EmptyCart => {  
            let mut items = Vec::new();  
            items.push(item);  
            ShoppingCart::ActiveCart(ActiveCartData {  
                unpaid_items: items,  
            })  
        }  
        ShoppingCart::ActiveCart(mut data) => {  
            data.unpaid_items.push(item);  
            ShoppingCart::ActiveCart(data)  
        }  
        ShoppingCart::PaidCart(data) => ShoppingCart::PaidCart(data),  
    }  
}  
  
fn make_payment(cart: ShoppingCart, payment: f64) -> ShoppingCart {  
    match cart {  
        ShoppingCart::EmptyCart => ShoppingCart::EmptyCart,  
        ShoppingCart::ActiveCart(data) => ShoppingCart::PaidCart(PaidCartData {  
            paid_items: data.unpaid_items,  
            payment,  
        }),  
        ShoppingCart::PaidCart(data) => ShoppingCart::PaidCart(data),  
    }  
}

Modeling Each Step in the Workflow with Types

  • validation step
    • Unvalidated Order -> Result<ValidatedOrder, ValidationError>
  • pricing step
    • ValidatedOrder -> PricedOrder
  • acknowledge order step
    • PricedOrder からacknowledgeを送る

Documenting Effects

  • 非同期処理などは Async<Result<T, E>> などで影響を表す

The Complete Pipline

main_api.rs

// -------------------------------------------  
// Input data  
// -------------------------------------------  
pub struct UnvalidatedOrder {  
    order_id: i32,  
    customer_info: UnvalidatedCustomer,  
    sipping_address: UnvalidatedAddress,  
}  
  
pub struct UnvalidatedCustomer {  
    name: String,  
    email: String,  
}  
  
pub struct UnvalidatedAddress {  
    street: String,  
    city: String,  
    state: String,  
    zip: String,  
}  
  
// -------------------------------------------  
// Input Command  
// -------------------------------------------  
pub struct Command<Data> {  
    data: Data,  
    timestamp: String,  
    user_id: String,  
}  
  
pub type PlaceOrderCommand = Command<UnvalidatedOrder>;  
  
// -------------------------------------------  
// Public API  
// -------------------------------------------  
  
pub struct OrderPlaced {  
    order_id: i32,  
}  
  
pub struct BillableOrderPlaced {  
    order_id: i32,  
}  
  
pub struct OrderAcknowledgementSent {  
    order_id: i32,  
}  
  
/// Success output of PlaceOrder workflow  
pub enum PlaceOrderEvent {  
    OrderPlaced(OrderPlaced),  
    BillableOrderPlaced(BillableOrderPlaced),  
    OrderAcknowledgementSent(OrderAcknowledgementSent),  
}  
  
/// Failure output of PlaceOrder workflow  
pub struct PlaceOrderError {  
    order_id: i32,  
    reason: String,  
}  
  
pub trait PlaceOrderWorkflow {  
    fn place_order(command: PlaceOrderCommand) -> Result<Vec<PlaceOrderEvent>, PlaceOrderError>;  
}

place_order_workflow

use crate::main_api::{  
    PlaceOrderCommand, PlaceOrderError, PlaceOrderEvent, UnvalidatedAddress, UnvalidatedCustomer,  
    UnvalidatedOrder,  
};  
  
// -------------------------------------------  
// Order life cycle  
// -------------------------------------------  
  
struct CustomerInfo {}  
  
struct Address {}  
  
// validate state  
struct ValidatedOrderLine {}  
struct ValidatedOrder {  
    order_id: String,  
    customer_info: CustomerInfo,  
    sipping_address: Address,  
    billing_address: Address,  
    order_lines: Vec<ValidatedOrderLine>,  
}  
  
// priced state  
struct PricedOrderLine {}  
struct PricedOrder {  
    order_id: String,  
    customer_info: CustomerInfo,  
    sipping_address: Address,  
    billing_address: Address,  
    order_lines: Vec<PricedOrderLine>,  
}  
  
// all states combined  
enum Order {  
    Unvalidated(UnvalidatedOrder),  
    Validated(ValidatedOrder),  
    Priced(PricedOrder),  
}  
  
// -------------------------------------------  
// Definitions of Internal Step  
// -------------------------------------------  
  
struct ProductCode {}  
  
// ----- Validate order -----  
  
// services used by validate_order  
trait CheckProductCodeExists {  
    fn check_product_code_exists(product_code: &ProductCode) -> bool;  
}  
  
struct AddressValidationError {}  
  
struct CheckedAddress {}  
  
trait CheckAddressExists {  
    fn check_address_exists(  
        address: &UnvalidatedAddress,  
    ) -> Result<CheckedAddress, AddressValidationError>;  
}  
  
struct ValidationError {}  
  
trait ValidateOrder {  
    fn validate_order(  
        check_product_code_exists: impl CheckProductCodeExists,  
        check_address_exists: impl CheckAddressExists,  
        order: UnvalidatedOrder,  
    ) -> Result<ValidatedOrder, Vec<ValidationError>>;  
}  
  
// ----- Price order -----  
  
// services used by price_order  
  
trait GetProductPrice {  
    fn get_product_price(product_code: &ProductCode) -> f32;  
}  
  
struct PricingError {}  
  
trait PriceOrder {  
    fn price_order(  
        get_product_price: impl GetProductPrice,  
        order: ValidatedOrder,  
    ) -> Result<PricedOrder, PricingError>;  
}