DDD ことはじめ ~値オブジェクト・集約・エンティティ・ファクトリ~

こんにちは、マットです。
都内ITベンチャーのエンジニアです。
Go/Next.js/GraphQLを使っています。

今回は以前から気になっていたDDDの技術書とGo×DDDの実装記事を読んだので振り返りをします。

今回読んだ書籍と記事は以下です。

本:

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

記事:

towardsdatascience.com

尚、DDDの概念や思想的な説明はあまりせず各トピックの目的と実装例を振り返る形でまとめていきたいと思います。

これらの記事はいずれも基礎入門で筆者は未だエリック・エヴァンスのドメイン駆動設計を読んでいません。 部分的なHow toになるのでご留意ください。

この記事では オンライン居酒屋サービス を実装していくのでそれらを構成するドメインが登場します。

ex) Customer, order, transaction(取引の意味)

値オブジェクト

値オブジェクトはシステム固有の値を表したオブジェクトです。

単にプリミティブな型に代入するのではなくオブジェクトとして定義される値です。

goではオブジェクトをstructとして表現します。

type Transaction struct {
    amount    int
    from      uuid.UUID
    to        uuid.UUID
    createdAt time.Time
}

さらに値オブジェクトは以下の性質を持ちます。

  • 不変である
  • 交換が可能である
  • 等価性によって比較される
不変である

ここではTransactionというstructの中にIDという識別子が存在しません。後述するエンティティにはID識別子が定義されます。 これはIDを元に永続化・参照させない事を意図しています。 その為 GetByID()のように取得して Update() メソッドを使用して変更するべき値ではないという事です。

*以下のように補足があります。

実際のアプリケーションでは利便性の為にIDを付与する

交換可能である

これは代入可能であるという意味ですが、Goでも当然可能ですね。

   transaction := &Transaction{
        //
    }
    transaction = &Transaction{
        //
    }

等価性によって比較される

オブジェクト同士の特定の値のみを比較するのではなく、オブジェクト自身に比較メソッドを持たせる方法です。

このようにする事でオブジェクトのフィールドが増えても変更箇所を少なくする事が出来ます。

func (t *Transaction) Equals(equaled *Transaction) bool {
    if t.amount != equaled.amount {
        return false
    }
    if t.from != equaled.from {
        return false
    }
    if t.to != equaled.to {
        return false
    }
    if t.createdAt != equaled.createdAt {
        return false
    }

    return true
}

値オブジェクトの生成

structにする事で生成時にバリデーションや特有のルールを適用する事ができます。

Equalsと同様にロジックの分散を防ぐ事ができます。

func NewTransaction(amount int, from, to uuid.UUID, createdAt time.Time) (*Transaction, error) {
    if amount >= 0 {
        return nil, errors.New("amount is invalid")
    }
    // if from  条件 {
    //     return nil, errors.New("from is invalid")
    // }
    // if to 条件 {
    //     return nil, errors.New("to is invalid")
    // }

    transaction := &Transaction{
        amount:    amount,
        from:      from,
        to:        to,
        createdAt: createdAt,
    }
    return transaction, nil
}

エンティティ

エンティティは値オブジェクトと対を為すオブジェクトです。

特徴

  • 可変である
  • 同じ属性であっても区別される
  • 同一性によって区別される

ここではPersonとItemというオブジェクトを参考に見ていきます。

type Person struct {
    ID uuid.UUID
    Name string
    Age int
}
type Item struct {
    ID          uuid.UUID 
    Name        string    
    Description string    
}
可変である

先ほどの値オブジェクトと違いエンティティの中身は変更可能です。

名前を変更するには以下の様に表現します。

*これは集約によって変更される可能性があります。

func (p *Person) ChangeName(name string) error {
    if name == "" {
        return errors.New("name is invalid")
    }

    p.Name = name

    return nil
}

同じ属性であっても区別される・同一性によって区別される

これはIDフィールドで区別され、google製の uuidパッケージを使用しています。

集約

データを変更する単位となるオブジェクトです。 全ての変更はこのルートオブジェクトを介してのみ行われるようにします。

以下のCustomerはルートオブジェクトです。

type Customer struct {
    person       *tavern.Person
    products     []*tavern.Item
    transactions []tavern.Transaction
}

各フィールド名を小文字で始める事でパッケージ外からアクセスできないようにします。

アクセスする時はセッターとゲッターを用います。

func (c *Customer) GetID() uuid.UUID {
    return c.person.ID
}

func (c *Customer) SetID(id uuid.UUID) {
    if c.person == nil {
        c.person = &tavern.Person{}
    }
    c.person.ID = id
}

func (c *Customer) SetName(name string) {
    if c.person == nil {
        c.person = &tavern.Person{}
    }

    c.person.Name = name
}

func (c *Customer) GetName() string {
    return c.person.Name
}

ファクトリ

ファクトリパターンはオブジェクトの生成に伴う複雑な処理をカプセル化する手法です。

func NewCustomer(name string) (Customer, error) {
    if name == "" {
        return Customer{}, ErrInvalidPerson
    }

    person := &tavern.Person{
        Name: name,
        ID:   uuid.New(),
    }

    return Customer{
        person:       person,
        products:     make([]*tavern.Item, 0),
        transactions: make([]tavern.Transaction, 0),
    }, nil
}

このファクトリはかなりシンプルな方であまり複雑なロジックは書かれていません。

単なるコンストラクタに近い気がします。

ドメイン駆動設計入門 では採番処理を行うファクトリを例に専用のinterfaceを作成しています。 そのinterfaceの実装ではDBなどインフラ層へのアクセスが行われ、mockを可能にしています。

ビジネスロジックが複雑になりDBへのアクセス等が発生するのであればinterfaceを作ったほうが良い気もしますが、単純なバリデーションのみで済むなら今回のようにコンストラクタ的な関数で十分だと思いました。

今回は以上となります。

次回はサービスパターン、レポジトリパターンを振り返りたいと思います。