Event-Driven Architecture in Golang読書メモ
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
以下の本を読んでいく
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
1章
概要
Event Driven Architecture(以下、「EDA」という。)には3つのパターンがある。組み合わせもあり得る。
- イベント通知
- イベント通知は識別子や正確なタイムスタンプのみを含む単純な構造体である。
- イベントは監査や、関連するその他のイベントの呼び出しのためにローカルに記述されがち
- Event-carried state transfer
- RESTの非同期版
- RESTはオンデマンドのpull型
- イベントにより伝達される状態遷移はpush型で、関係のあるコンポーネントからconsumeされる(=Pub/Subみたいな)
- イベントソーシング
- よくあるイベントソーシング。起きたことをすべて記録して、復元するやつ
Core Concepts
- Producer
- イベント発生時にキューにメッセージを投げる人
- Event
- イベントドリブンアーキテクチャの核であり、イベント通知、Event-carried state transfer、イベントソーシングのすべての根幹をなす
- キューの名前や何かしらのメタデータなどの、一意な識別子(今回はPaymentID)をもつ。
- Consumer
- バス、チャネル、ストリーム、トピックなどの名前で呼ばれる
- Queue
- 知っての通り
- Event Stream
- メッセージキューと異なり、最終的にempty stateに戻るらしい。
- イベントの時間切れとか、ストリームのキャパオーバーなどの外部要因によって削除されるまで、イベントストリームは肥大化し続けるらしい
- イベントが確認されたら消されるか、そうでないかの違いがMQとの違い?
- Event Store
- DBみたいなもん。イベントソーシングで使いがち
- コンシューマー
イベントをキューからsubscribeして処理する
作るアプリケーション
API Gateway(BFF), UI(WebSocket), API(gRPC)
EDAのメリット
- レジリエンシー
- イベントブローカーによってサービス間が疎結合になっているため、呼び出し先のサービスが動いていなくてもエラーが伝搬しない
- アジリティ
- 他のチームのコンポーネントを知らなくても自分のコンポーネントを開発できる
- イベントブローカーのサブスクライバーを足すだけで、APIを追加できる
- 他のチームのコンポーネントを知らなくても自分のコンポーネントを開発できる
- UX
- 各コンポーネント完了時に通知する等、ユーザーへのイベント通知が行いやすい
- 分析や監査
- 小さい変更を加えやすいので後からBIが必要になっても大丈夫
EDAの課題
- 結果整合性
- キューとDBへの2重書き込み(大事)
- キューに書き込めない場合に備えて、DBにも状態を書き込んでおく
- 分散非同期ワークフロー
- アプリケーションの状態が結果整合性なので確実なことがわからない
- どのような処理状況なのかユーザーが知るのが大変
- クライアントからのポーリング
- WebSocketによる状態通知
- どのような処理状況なのかユーザーが知るのが大変
- アプリケーションの状態が結果整合性なので確実なことがわからない
- コンポーネントの連携
- コレオグラフィー
- コンポーネントが次の処理を知っている
- オーケストレーション
- コンポーネントは次の処理を知らず、オーケストレーターのみが知っている
- コレオグラフィー
- デバッグしやすさ
- プロデューサー、コンシューマーが互いのことを知らないから、コンポーネントを跨ぐ処理の追跡が困難
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
2章
概要
DDDはEDAと相性がいい。もちろんDDDがなくてもEDAは実現できる。
DDDは複雑な業務の概念を理解して、ソフトウェアに落とし込むモデリングをするもの。
その鍵となるのはユビキタス言語と境界付けられたコンテキスト
DDDの説明
一般的なDDDの説明をしているだけなので、省略
Domain-centric architecture
ヘキサゴナルアーキテクチャの話。依存性の逆転とかを用いてテスタブルにしましょうという話
CQRS
- 読み取りと更新の責務を分離する
- イベントソーシングと相性がいい
- Task-basedなUIにすることで、コマンドの役割がわかりやすくなる
- ダメな例: UpdateUserでユーザーのプロフィールも、メールアドレスも更新する
- 良い例: UpdateProfileでユーザーのプロフィール、ChangeMailingAddressでメールアドレスを更新する
アプリケーションアーキテクチャ
- モノリス: シンプルだが、一部の更新が全体に影響を与えがち
- モジュラモノリス: 境界付けられたコンテキストごとにモジュールを分割するが、DBは同じ
- マイクロサービス: 最も粗結合だが最も複雑
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
3章
概要
アプリケーションの要件を定義して、仕様を決定する。以下の順序で実施する
- イベントストーミングで境界付けられたコンテクストと、ユビキタス言語を発見する
- 境界付けられたコンテキストを実行可能な仕様にする
- 境界付けられたコンテキストを実装するアーキテクチャを決める
イベントストーミング
- ステークホルダー全員を巻き込んで付箋に一連の業務を書き込んでいくアレ
※イベントストーミング自体の詳細なやり方は省略
Architecture decision record(ADR)
- アーキテクチャ上の意思決定を記録する。
- モジュラモノリスを用いる
- モノリスで開発に混乱を招くのも、マイクロサービスの複雑性に対処するのも嫌だから
- モジュラモノリスを用いる
- 本書ではADRは以下のようなフォーマットに従う。
# {RecordNum}. {Title}
## Context
What is the issue that we're seeing that is motivating this decision or change?
## Decision
What is the change that we're proposing and/or doing?
## Status
Proposed, Accepted, Rejected, Superseded, Deprecated
## Consequences
What becomes easier or more difficult to do because of this change
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
4章
モジュラモノリスでアプリケーションを構築していく。
モノリスであるために気をつけること
ディレクトリ構成
- ルートディレクトリは最小限にする
- モジュール名がわかりやすいディレクトリを切る
- controllersみたいな、汎用的なディレクトリは切らない
→screaming architectureを採用する - /cmd: 実質的なルートディレクトリ
- /internal: 親・兄弟ディレクトリのみから見える
- /docs
- /docker
- controllersみたいな、汎用的なディレクトリは切らない
モジュール内のコードの書き方
インターフェースを受け取って構造体を返す
基本的にAccept interfaces, return structsの考えに従う。
インターフェースを小さく保って、いろいろな実装を受け入れられるようにする
実装ではなくインターフェースを渡してテストや変更を容易にしましょうという、DI的なことを言っている。
Interface Checks
var _ TheContractInterface = (*TheContractImplementation)(nil)
上記のように書くと、TheContractImplementationがTheContractInterfaceを満たすことをコンパイル時点で検証することができる。
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
コンポジションルート
Adapterとか、DIとかのインフラを提供する
Goにおいて、DIのためのツールはGoogle WireとDigがあるが、依存関係が複雑になるまでは使う必要がない
ProtbufとgRPC
モジュール間通信はgRPCにする。
/stores/storespbみたいなディレクトリ構成にする。
外部とのインターフェースなので、モジュールの外部からの可視性も確保する
BUF
- bufを用いる
- 名前空間の衝突を避けるため、親のディレクトリ名をAPI名にする
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
ユーザーインターフェース
-
grpc-gatewayでgRPCサービスAPIを外部に公開する
- localhost:8080/api/*
モジュールの統合
モジュラモノリスでは次のような理由で境界付けられたコンテキストをまたいで処理をする必要が出てくる。
- データが他のコンテキストにある(=外部データの使用)
- 処理を完結させるのに複数コンテキストをまたぐ必要がある(=外部処理の使用)
外部データの使用
- DBそのものを共有
- 競合などの統制が難しく、選択すべきではない
- 連携先コンポーネントに対してのPush型のデータ共有
- 連携先コンポーネントを管理し続けるのがつらすぎる
- 必要になったときにデータをPullする
- 最良の選択肢だが、連携先コンポーネントが多すぎるとPullの負荷が高まる
- サーキットブレーカーとか、MQとか、リトライロジックでなんとか対処する
- 最良の選択肢だが、連携先コンポーネントが多すぎるとPullの負荷が高まる
外部処理の使用
- Push型
- Pull(Polling)型
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
イベントの種類
- ドメインイベント: 同期的。DDDから来るもの
- イベントソーシングイベント: すべての更新をINSERTで表すアレのイベント
- インテグレーションイベント: アプリケーションの状態遷移のイベント
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
実装
OrderがOrderCreatedイベント、NotifyToCustomerイベント、Rewardsイベントなどにまたがる集約になる。
なので、Orderは複数のイベントのスライスを持つ
Before
ID string
CustomerID string
PaymentID string
InvoiceID string
ShoppingID string
Items []*Item
Status OrderStatus
}
After
type Order struct {
ddd.AggregateBase
CustomerID string
PaymentID string
InvoiceID string
ShoppingID string
Items []*Item
Status OrderStatus
}
関数型の考え方だと、Orderという集約に状態をもたせるのではなく、Statusごとに状態遷移した型を持つことになりそう(CreatedOrder型とか、NotifiedOrder型とか)。関数型言語で書き直しても面白いかも
この本はコードのうちほんの一部しか解説してくれないことに気づいた。
とりあえずChapter03までのコードベースすべてを理解して、Chapter04で取り上げられている差分に注目するアプローチに切り替える。
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
(WIP)コードの分析
結論サマリ
- 完成版(Chapter11)のコードにもUIは含まれていないので、あくまでこれはAPIしか作っていない
- 各モジュールは<module名>/internalにモジュールの実装があり、<module名>/orderingpbに外部インターフェースの実装がある
コードの中身分析
対象コード
internal
配下に、共通モジュールのようなコードがある。
- internal/ddd
- aggregate.go: 複数のイベントのスライスをメンバとして持つ。Orderのメンバとして持たせることで、Orderに紐づく複数のイベントを管理する
- event.go: イベントのインターフェース
- internal/monolith
- monolith.go: モノリスアプリケーションのエントリポイントのインターフェースを定義
- internal/config
- config.go
- Postgresやアプリケーションのログレベルなどの設定
- config.go
- internal/web
- config.go
- WebサーバのHost/Portの設定
- config.go
- internal/rpc
- config.go
- grpcサーバのHost/Portの設定
- config.go
ordering/internal/domain
配下に、「注文」という境界付けられたコンテキスト特有のコードが含まれる
- ordering/internal/domain
- item.go: 商品エンティティの構造体
- order_events.go: Eventインターフェースを実装したOrder特有のイベント
- 関数型言語で状態に対応する型を作るのと同様、エンティティに特化したイベントを作成する
- order_status.go: newtypeイディオムみたいな感じで、ただのstring型をOrderStatus型として表現する
- RepositoryでDBからクエリして取得したstatusの文字列をOrderStatus型に変換する機能を含む
- order.go: 注文エンティティの構造体
- 注文のコンストラクタを含む。入力値のバリデーションはif文でゼロ値でないことを一つずつチェックする。ゼロ値でもいいものはチェックしない
- 関数型でPrimitive Obsession対策をするなら入力値ごとにnewtypeを定義してバリデーションしそう
*すべての入力値にnewtypeを定義するのはそれはそれで違うけど - ゼロ値が許容されるならOptional型でくるむとかもしないのがGoのスタイル
- 呼び出し側でOptional型のハンドリングをif文で行う?
- order.AddEventで、作成されたイベントを自身の集約に追加する
- OrderにCRUDがあるたびにこれを呼び出し、Orderのインスタンスは自身の過去のイベント履歴をすべて持つ
- Order自身の参照を書き換えるため、すべて副作用を伴う
- OrderにCRUDがあるたびにこれを呼び出し、Orderのインスタンスは自身の過去のイベント履歴をすべて持つ
- 関数型でPrimitive Obsession対策をするなら入力値ごとにnewtypeを定義してバリデーションしそう
- 注文のコンストラクタを含む。入力値のバリデーションはif文でゼロ値でないことを一つずつチェックする。ゼロ値でもいいものはチェックしない
- order_repository.go
- OrderをDBにクエリするRepositoryインターフェース。Find, Save, Update関数を定義する
- notification_repository.go
- 通知のリポジトリのインターフェースを提供する
- ordering/internal/appliation
- notification_handlers.go
- Orderの通知をハンドリングする
- イベントが発生のたら、メンバのNotificationRepositoryに通知処理を移譲する
- ignoreUnimplementedDomainEventsをメンバーに持つことによって、実装されていないメソッドがあってもNotificationRepositoryインターフェースを満たせるようになる
- Orderの通知をハンドリングする
- notification_handlers.go
- ordering/internal/handlers: EventPublisher/Subscriberに持たせるハンドラを定義する
- ordering/internal/postgres:DBとのI/Oを実装するRepository
- ordering/internal/grpc: gRPCのラッパーで、Contextのハンドリング設定を追加
- ordering/internal/application/commands: CQRSのCommand(=更新アクション)を実行する。Create, Cancel, Complete, Readyコマンドがある。
- コンストラクタの中でRepositoryを受取り、Repositoryのメソッドを呼び出す
- Repositoryのメソッド呼び出しがgRPCのサービス呼び出しに対応する
- ordering/internal/application/queries: CQRSのQuery(=参照アクション)を実行する。Getがある。
- ordering/orderingpb: gRPCの設定ファイルと、BUFによる自動生成ファイル
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
コード分析のまとめ
main関数の動き
色々なモジュールを呼び出してwait()する。
以下のようなヘルパーメソッドでgRPCサーバとWebサーバを立ち上げている。
しかし、なんでこれらの関数は引数を全く用いていないのか?
func initRpc(_ rpc.RpcConfig) *grpc.Server {
server := grpc.NewServer()
reflection.Register(server)
return server
}
func initMux(_ web.WebConfig) *chi.Mux {
return chi.NewMux()
}
肝心のモジュール呼び出しはmonolith.goの中で実行されている。
以下の関数では、
- サーバの起動と、サーバの終了に関する処理のgoroutineを並行で起動する。
- gCtx.Done()で、group.Goに渡したデータがerrorを返すか、Waitがreturnされたときにキャンセル処理を実行する
func (a *app) waitForWeb(ctx context.Context) error {
webServer := &http.Server{
Addr: a.cfg.Web.Address(),
Handler: a.mux,
}
group, gCtx := errgroup.WithContext(ctx)
group.Go(func() error {
fmt.Println("web server started")
defer fmt.Println("web server shutdown")
if err := webServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
})
group.Go(func() error {
<-gCtx.Done()
fmt.Println("web server to be shutdown")
ctx, cancel := context.WithTimeout(context.Background(), a.cfg.ShutdownTimeout)
defer cancel()
if err := webServer.Shutdown(ctx); err != nil {
return err
}
return nil
})
return group.Wait()
}
全体のポイント
- コンストラクタはインターフェースを受け取って、構造体(のポインタ)を返す
- SpringBootでDIするときと同じ。インターフェースを渡すシグネチャにすることで、何をインジェクションするかによって挙動を変えられる
- Preemptive Interface Anti-Pattern in Go
- 型チェックはこまめにやる
- var _ Interface名 = (*実装)(nil)
作り込みの順序
- ドメイン層( <モジュール名>/internal/domain配下)
- ドメインオブジェクト
- ドメインイベント
- リポジトリインターフェース
- アプリケーション層(<モジュール名>/internal/application配下)
- commands/queriesでアクションを実装
- ドメイン間の連携方式(internal/ddd配下)
- ドメインイベントの見直し
- イベントハンドラーの追加
- ディスパッチャーの追加
- リポジトリ層の実装(<モジュール名>/internal/grpc or <モジュール名>/internal/postgres)
- エントリーポイントの完成
- 3.のイベント連携のオブザーバー、リポジトリ(grpc, postgres)のインジェクション、イベントハンドラへの登録
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
1. 各domain層の実装
ドメインオブジェクト
- エラーのラッパーを作成。例えば、BadRequestにもさまざまな原因があるため、考えられるパターン分のエラー変数を定義
- ドメイン本体の構造体を作成
- IDを持たず、ID自体はddd.AggregateBaseから受け取る
- ドメインのコンストラクタを作成。この中で1のエラー変数でバリデーションする。
- コンストラクタはポインタを返しがち。コンストラクタ内のローカル変数の寿命がコンストラクタ内にとどまらないそうな。
- ドメインオブジェクトの状態を変更するメソッドを作成する。これはドメインイベントの数ぶん作成される
ドメインイベント
- ドメインイベントを作成。(ドメインオブジェクトとドメインイベントは相互に依存関係になる)
レポジトリインターフェース
- Repositoryのメソッドを取りまとめるインターフェースを作成する
2. 各application層(commands/queries)
- QueriesとCommandsにディレクトリ(パッケージ)を分けて、アクションごとにファイルを作る
Commands/Queries
- アクションの構造体を作成(eg. AddProduct)
- アクションハンドラの構造体を作成(eg. AddProductHandler)
- アクションハンドラのコンストラクタを作成(これはなぜか値を返す)
- アクションハンドラのメソッドとして、アクションを定義。ハンドラの要素のドメインリポジトリ経由で、必要なメソッドを呼び出す。
- (Commandsのみ)最後にdomainPublisher.Publishを呼ぶ
3. ドメイン間の連携方式(internal/ddd配下)
課題
Commandの中のアクションの結果を通知するとき、プリミティブな実装は以下のような感じ。
create_order.go
func (h CreateOrderHandler) CreateOrder(ctx context.Context, cmd CreateOrder) error {
// 注文をリポジトリでSave()する
if err = h.orders.Save(ctx, order); err != nil {
return errors.Wrap(err, "order creation")
}
// 顧客に通知する
if err = h.notifications.NotifyOrderCreated(
ctx, order.ID, order.CustomerID,
); err != nil {
return errors.Wrap(err, "customer notification")
}
しかし、注文にポイントを付与するみたいなことをやりたくなったらCreateOrderの中でPointのインターフェースにもメッセージを送信しないといけなくなる。
加えて、ポイントを付与しないパターンのためにCreateOrderAndAddPointみたいなメソッドを追加したりする懸念もあり、モジュールが肥大化する可能性がある。
そこで、メッセージングの部分を切り出し、モジュールを粗結合にしつつメソッドの責務を明確にする。
上記で言えば、OrderCreatedコマンドは注文が作成し、それのメッセージを送るだけ。顧客への連絡はそのメッセージを受け取った他のパーツが行う。
実装のために、以下の要素を持ち込む
- ドメインイベントを取りまとめるオブジェクト
- 状態遷移を伝えるドメインイベント
- イベントのディスパッチャー
- 後続の処理(顧客への通知みたいな)が上記をサブスクライブ
実装
ドメインオブジェクト・ドメインイベントの見直し
※ドメインオブジェクト内に実装
以下のように、ドメインオブジェクトのアクションの中で、以下のようにイベントのスライスにイベントを追加する。
つまり、ドメインオブジェクトの中で自身に発生したイベントをすべて持つようにする。さらに、その機能は色々なドメインで使いまわすためにドメインオブジェクトの中にddd.AggregateBaseという構造体を含める。
order.AddEvent(&OrderCreated{
Order: order,
})
※internal/dddに実装
ドメインイベントのために以下を追加する
- aggregate.go
- entity.go
- event.go
※ドメインオブジェクトはIDを持たず、ddd.AggregateBase経由でIDを受け取るようにする
- AggregateとEntityはGetD()メソッドを持つ。
- これは、AggregateであろうがEntityであろうが、それを扱うメソッドがIDを取得するときにそれを意識する必要がないようにするため。(→それ、IDフィールドがpublicであればよくない?Goに直和型があれば便利なのに。。。)
- 今回扱うイベントはドメインイベントなので、境界付けられたコンテキストをまたがないため、最小限の情報だけを含むイベントを作るーみたいなのはやらなくていい
イベントハンドラーの追加
- <モジュール名>/internal/application配下にかいていく
- ドメインイベントは境界付けられたコンテキストをまたがないため、domain_event_handlers.goみたいな名前で良い
- イベントの種類ごとに、それをハンドリングするメソッドを持つインターフェースを作成
- とはいえ、1つのハンドラですべてのイベントをハンドリングしたくはない。かと言ってインターフェースを満たすために使わないメソッドを実行するのは嫌だ
- だから、何もしないメソッドを定義してインターフェースを満たす
ignoreUnimplementedDomainEvents
を定義して、それをハンドラのフィールドに含めるというワークアラウンドを用いる
ディスパッチャーの追加
参照: https://zenn.dev/regmarmcem/articles/1e50be02798d4d
internal/ddd配下に作る
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
AggregateBaseの実装について思ったこと
- privateな値を取得する場合、 Getterを定義することは別におかしくもなんともないらしい。
- 一方、publicな値を取得する場合、Getterを用いるのはおかしいっぽい。
- 確かに、idというフィールドのGetterでID()は定義できるが、IDというフィールドに対するID()は定義できない。
- この本ではPublicな値に対して無駄にGetterを付与しているからGetをつけないといけないというおかしなことになっている。
- DDDの文脈で、ドメインオブジェクトは絶対にドメインルールを遵守させるために、バリデータ付きのコンストラクタの使用を強制しつつ、フィールドにはGetterとSetterを使ってアクセスするらしい
- Go言語100Tipsでは、必要ならGettterとSetterを使うことは否定されていない。ただしその場合GetterにはGetをつけないべき
-
不変性を担保するためのワークアラウンドとして、フィールドをprivateにしつつGetterだけ定義して、インターフェースを返却するコンストラクタを使う(オブジェクトは変更せず作り直す)ことで、一応要件は満たせそう
- Go言語100Tips(No.7)ではinterfaceを返すことは否定されているし、実際一般的なアンチパターンらしいが、そんなことを言っていたら進まないので、受け入れるようにする
(WIP)エラーハンドリングの伝搬
DBとの接続
pgxをDBのドライバーとして用いる
main.goで以下のようにインポートする
_ "github.com/jackc/pgx/v4/stdlib"
原著のメモ
- Order(order.go)はドメインイベントだから、境界付けられたコンテキストをまたがない。これは以下を意味する
- イミュータブルな状態を運ぶ構造体にしておけば、何でもその中に入れていい
- そのイベントのサブスクライバーを意識しなくていい
- 寿命が短く、永続化のためのシリアライズやバージョニングが不要
所感: Orderのためにドメインイベント(Create, Ready, Complete, Cancel)を実装したが、ボイラープレートが多くて、メソッド変更時に常にそれらを更新するのが面倒
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
(メモ)
Q. monolith.goのフィールドの多くがGetterを持つのはなぜか?フィールドを公開するのではだめなのか?
A. それぞれのmoduleがコンストラクタでDIコンテナを受け取って、moduleの初期化時にDBのコネクションなどを取り出すようにしたい。このとき、コンストラクタの引数として与えるべき型を、インターフェースで定義したい。
例えば、Orderingモジュールの初期化のとき、以下のようにDIコンテナからDBのコネクションを取得する。
(Module) Startup(<DIコンテナ>) {
orders := postgres.NewOrderRepository("ordering.orders", <DIコンテナ>.DB)
}
このとき、初期化するモジュールがOrderingでもStoreでも、受け取るDIコンテナはDBフィールドを持つことを保証したい。
この時とりうる手段は2つあると思う。
①グローバルな構造体を定義して、そこから値を取得する
↓
type Monolith struct {
DB
modules []Module
}
Monolithの中のmodulesスライスの要素の初期化にMonolith.DBを使うのはなんとなく気持ち悪い(けど理由が説明できない...)
コンストラクタの実装を考えてみる。
レシーバは使えないはずなので、以下のようにStartup()に引数でMonolithをあたえないといけない。
func (Module) Startup(m Monolith) error {
m.DB
return nil
}
このようにすると、以下のように呼び出しが循環する。
Monolith.modules[0].Startup(Monolith)
②必要な値を返す関数を持つインターフェースを定義する
以下のようにすると、Moduleがインターフェースに依存するから、DBのコネクションを差し替えたりできる(依存性の逆転)。
type Monolith interface {
DB() *sql.DB
}
依存性の逆転の観点から、②の実装のほうが素直なので②を採用する。
※①の実装だと依存性の逆転ができないかといわれると、適切に要素を分割すればできる気がする。結論ありきで考えてしまっている気がするので、もっといい理由が思いついたら追記する。
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
アプリケーションの初期化フロー
まとめ
呼出し順序
- grpc/server.go: ユーザーインターフェースのコントローラー。gRPCのリクエストを受け付けて、アプリケーションサービスを呼び出す
- application/application.go: アプリケーションサービスのラッパー(サービス層だがルーターのように振る舞う)
- application/create_article.go: アプリケーションサービスの実態
- domain/article.go: ドメインサービス
- domain/article_repository.go: リポジトリ
レシーバをポインタにするか構造体にするか
コンストラクタの返り値をポインタにするか構造体にするか
main関数
run()を呼び出し、エラーがあったらその内容を出力し、Exitコード1で終了する。
run関数
ざっくり以下のことをしている
- app構造体にconfigファイルの中身を突っ込む
- app構造体にDBのコネクションをもたせる
- app構造体でgRPCサーバーを立てる
- app構造体でREST APIサーバーを立てる
- app構造体にモジュールの配列を突っ込む
- app構造体のモジュールの配列の中身をすべて起動する(=gRPCにサービスを登録)
各モジュールの起動
ざっくり以下のことをしている
- postgresのリポジトリを初期化
- grpcのサービスをサーバーに登録
メモ(errorgroupの挙動)
monolith.goに以下のようなコードがあった
group, gCtx := errgroup.WithContext(ctx)
group.Go(func() error {
fmt.Println("rpc server started")
defer fmt.Println("rpc server shutdown")
if err := a.RPC().Serve(listener); err != nil && err != grpc.ErrServerStopped {
return err
}
return nil
})
Package errgroup provides synchronization, error propagation, and Context cancelation for groups of goroutines working on subtasks of a common task.
errorgroupは複数のgoroutineの処理を同期したりエラー伝搬したりするようなものらしい。
A Group is a collection of goroutines working on subtasks that are part of the same overall task.
メインのタスクの子タスクをグルーピングするためにGroupが用いられる。
WithContext returns a new Group and an associated Context derived from ctx.
WithContextはContextからそれに紐づくGroupを生成する
Go calls the given function in a new goroutine. It blocks until the new goroutine can be added without the number of active goroutines in the group exceeding the configured limit.
The first call to return a non-nil error cancels the group's context, if the group was created by calling WithContext. The error will be returned by Wait.
Goは新しいgoroutineを立ち上げて、与えられた関数を実行する。エラーが発生するとGroupのコンテキストをキャンセルする
Wait blocks until all function calls from the Go method have returned, then returns the first non-nil error (if any) from them.
Waitはグループのすべての関数呼び出しが終わるまでブロックする。
疑問
どのようなタイミングでcontext.Contextを使うべきか。自作の関数にctxを渡すかの判断はどのようにすればいいの?例えば利用するライブラリの関数にctxを使うものがあればそれを呼び出す関数でもctxを渡すみたいな?
![geibee](https://res.cloudinary.com/zenn/image/fetch/s--kfLxZ5Sc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1aebc7f4d5.jpeg)
DBには導出項目を含みたくないが、導出項目を含むドメインモデルをアプリケーションではどう表現するのがいいのか?
たとえば、注文を表すモデルがOrder, 商品がItemだとして、注文の価格はItemから導出できるとする。
ただ、注文の価格はUIに表現できるようにしたい。このとき、ドメインモデルはどういうフィールドを持つべきか?
多分、ドメインモデルは価格を持たせ、DBに永続化するタイミングやDBからクエリするタイミング
で関連のレコードをクエリしてドメインオブジェクトに持たせるのが良い?