Domain-Driven Design with Golang を読んだ

learning.oreilly.com

Goがある程度わかってる人向けのドメイン駆動設計の入門書って感じだった。ページ数が少ない方なので内容も浅め。DDDの構成要素であるエンティティやリポジトリについて解説しながら、それらをGoで実装していく場合の考え方やパターンを紹介している。

Chapter 1: A Brief History of Domain-Driven Design

導入としてGoFデザインパターンやEvansのDDDの話から。

DDDは何にでも適用するべきじゃなくて複雑性の高い領域じゃないと逆に生産性は低くなる。 単純なCRUDだけのユースケースとかまでやるのはオーバーキル。

後で読む learn.microsoft.com

Chapter 2: Understanding Domains, Ubiquitous Language, and Bounded Contexts

ドメインエキスパートとの会話ではユビキタス言語を使って継続的に育ててコードにも反映しよう。

境界付けられたコンテキストを超えたモデルの共有はしない。 Open Host Service、Published language, Anti-corruption layerで明確にコミュニケーション方法を定義する。 例えばgRPCやOpen APIなど。

書籍上でgRPCにprotocではなくbufを使ってるのは初めて見た。

Chapter 3: Entities, Value Objects, and Aggregates

エンティティは一意性のあるやつ。 ドメインモデルで表現できる属性があるならそれを使えばいい。 スケールするシステムではintだとオーバーフローすることもあるから将来性も考えて採用する。 UUIDとか。

ドメイン貧血症にならないように注意。 getter/setterしかなかったり、いろんなクライアントにビジネスロジックを丸投げしたりするとそうなる。 自らのビジネスロジックとして一貫性や堅牢性を保証しよう。 ORMを使う使わないにかかわらず、アダプターを嚙ませるなどしてデータアクセス層はエンティティとは切り離したほうがよい。

RailsActiveRecordに慣れてるとこの辺の考え方がちょっと難しいかなって思う。 Railsは逆に敢えて強結合することによって生産性を上げるっていう思想だからそれはそうで、それ自体が問題なわけではないけど。

バリューオブジェクトは同値性のあるやつ。 Goだと同じ型の構造体が同値なら勝手になる。ただしファクトリ関数でポインタを返さぬように。 あとはいつものイミュータブルすること、ドメインに即したものにすることなど。

集約はわかるようでまだよくわかってない感ある。 設計ミスって逆に生産性低くなったり一貫性なくなったりするのはそうだねって感じだ。 結果整合性じゃなくて即時性のあるトランザクション境界の単位を探すのもひとつだと。

Chapter 4: Exploring Factories, Repositories, and Services

ファクトリは生成を単純化できるのと不正状態を作らないことを強制できるのが良い。 とはいえGoだとNewXxx関数を作ること多いからあんま考えないような。 ただしエンティティのファクトリはIDをそこで生成するか引数で取るかは設計次第。

リポジトリはデータアクセスをカプセル化する。 ありがちな間違いとしてデータベースのテーブルごとに構造体を作ることがある。 これは避けるべきで集約ごとに構造体を作ることを狙おう。

なるほど。集約がトランザクション境界になるならそっちのが自然か。データ構造にも依存しなくなるし。

ドメインサービスは使いどころがトリッキーになりがち。 ドメイン内の特定の部分の書き込みビジネスロジックドメインオブジェクトの別ドメインへの変換、2つ以上のドメインオブジェクトのプロパティを使った計算など。 もちろん境界付けられたコンテキスト内でユビキタス言語を使用して表現する。

サンプルコードだと

type Product struct { ... }
type ShoppingCart struct { ... }
func (s *ShoppingCart) AddToCart(p Product) bool { ... }

のようにエンティティ同士を関連付けるのではなく

type CheckoutService struct {
   shoppingCart *ShoppingCart
}
func NewCheckoutService(shoppingCart *ShoppingCart) *CheckoutService { ... }
func (c CheckoutService) AddProductToBasket(p *Product) error { ... }

のようにドメインサービスを介すべきだとしてる。

Railsだとこういうのモデルに書いちゃいがちだし、これは集約とは違うんとか思ったりして、わかるようなわからんような。

アプリケーションサービスは他のサービスとリポジトリをまとめてトランザクションを保証する。 ドメインロジックは持つべきではない。 通常は非常に薄い層で、ほかの層に押し付けたロジックをコーディネートするだけ。

セキュリティ関係もここでやることが多い。 あとはメール送ったりだとか支払いしたりだとか。

Chapter 5: Applying Domain-Driven Design to a Monolithic Application

まずはモノリスな状態から作っていく。

internalディレクトリの下に掘っていくことで外部ドメインから参照されないようにしている。 その中で各サブドメインごとにパッケージを分割して、それぞれの仕事をDDDの構成要素を用いて行う。

ただ、そのサブドメインのサービスを介しているとはいえ、パッケージ間の参照を割とカジュアルにやっているあたりが若干引っかかる。

Chapter 6: Building a Microservice Using DDD

ドメインサービスをapplication service、anti-corruption layer、open host serviceなどで隔離してロジックを分離する例なだけだった。 詰め替えが多くて冗長になるのはまぁしょうがないか。

github.com/hashicorp/go-retryablehttpは覚えておこう github.com

Chapter 7: DDD for Distributed Systems

一般的な分散システムの簡単な話。

CAP定理、CQRS、EDA、2フェーズコミット、サーガパターン、メッセージバスなど。 だいたい知ってることだったので見どころなし。

Chapter 8: TDD, BDD, and DDD

gomockを使ったTDD。 ひとつのfunc Test_...に、そこそこ長いユースケースの文章が書かれたt.Runを複数並べるのは初めて見た。

BDDはgo-bddを使うとこんな感じですよとチラッと紹介してるくらい。