はじめに
過去に以下の記事を書いております。
個人開発者が実践したNext.js - T3stack - DDD な感じのアーキテクチャ 【その1 使用技術と全体の構成】
こちらの記事を書いた時点ではまだDDDの理解が浅い状態で、勘違いしている点・見逃していた考え方等が多くありました。
ここ半年程度で更にDDDの学びを深め、実際に実装しアップデートを行なってきた中で気づいた、
✅勘違いしやすい・見逃しやすいポイントを紹介したいと思います。
※ この記事内で扱う言葉や定義等は「ドメイン駆動設計 モデリング/実装ガイド」を参考にしていることが多いです。
※みんなで作る木の図鑑を運営しています。なので木工品を例にした話が出ます。
見逃していたこと:CQRS
CQRSとは、Command Query Responsibility Segregation
の略で、『コマンドクエリ責務分離』と呼ばれます。
DDDでは、エンティティや値オブジェクト、リポジトリ、などデータの更新系の処理に対して様々なクラスが登場し、多くの書籍や記事ではこれらのクラスがよく紹介されています。
しかし、DDDにおけるCQRSの考えでは、参照系はQuery Serviceクラス等が登場するだけで、✅更新系で使われるクラスは使用されません。
実際にアプリケーションを開発する時には、更新系のAPIだけでなく参照系のAPIも作成します。
このCQRSの概念を知っておかないと、本来更新系で使うべきクラスを参照系にも適用してしまい、必要以上に複雑な参照系の実装をしてしまうことがあります。
更新系と参照系は登場するクラスの違い
・更新系に登場するクラスと呼び出しのフロー
・参照系に登場するクラスと呼び出しのフロー
※私のプロジェクトはt3Stack(tRPC)を使用しているので、エンドポイントはrouterファイルで定義しています。
このように参照系は、更新系に比べてかなりシンプルな構成で済ませられます。
更新系のクラスを使って参照系の実装をすると、制約の多いクラス達に阻まれて実装上の課題が発生しやすくなります。
私の知る限り、DDDの書籍や記事などでCQRSについて言及されていることは多いですが、詳細に説明されていることは少ない印象で、見逃す・理解が浅くなりやすいのではないかと思っています。
勘違いしていた事:DB駆動設計ではない
DDD以外の開発に慣れた開発者は、設計をする際に無意識に脳内にDBのテーブル構造を組み立てていることが多いと思います。
その勢いで、1つのテーブルを1つのエンティティとして扱い実装を進めるのは、DDDとは違うアプローチです。
私の過去の実装はこのDB駆動になっており、各所にDBの知識が流出していました。
本来のDDDでは、
- ドメインについての理解を深める
- モデルを改善する
- モデルをソフトウェアに反映する
というループを回すことで、実際のドメイン領域の課題に対して、改善のアプローチをしていきます。
この中の「ドメインについての理解を深める」という段階は、何をやるでしょうか?
実際の業務に関わっている方やドメインエキスパートから、課題があることを伝えてもらい、その改善を考えます。
その際に、課題を感じている方々の口から「XXテーブルのデータがOOだから〜」みたいな言葉は基本的に出てこないはずです。
「ドメインについての理解を深める」ために、エンジニアもこれらの人や考え方・言葉に合わせることが大切です。
そこから、実際のドメインの中で出てくるユースケースや言葉を、モデル・アプリケーションとして表現して、ドメインの中で使われる言葉通りのモデリング・実装をしていくことによって、DB駆動ではなくドメイン駆動にシフトできます。
見逃していたこと:集約とリポジトリ
過去に私がやっていた実装では、集約という概念はなく、エンティティを直に操作しデータの作成・更新に利用していました。
ドメイン駆動での開発が進むにつれ、集約の概念がないとコードにドメインを反映させることが難しくなるということが分かりました。
集約のルールとして、
- 強い整合性確保が必要なもの
- 同一のトランザクションで処理する
というものがあります。
例えば、「木工品を登録する」というユースケースでは、「木工品」という集約を扱います。
この「木工品」という言葉に含まれる内部のデータは、複数のエンティティや複数のテーブルにまたがっていても問題ありません。
ただ、これらは整合性を持った一つの集約のデータとして扱います。
つまり「木工品を登録する」を表すために、「木工品」の集約をまるっと同一トランザクションで登録する。ということが実現できればOKになります。
「木工品を登録する」というドメインの言葉をそのままコードで表現しているということです。
そして、この「木工品集約」は、この集約のままリポジトリクラスに引き渡され、その内部で複数のエンティティや複数のテーブルにまたがっていても、同一のトランザクションで永続化されることになります。
なので、リポジトリクラスは集約単位で作成すると扱いやすい実装になります。
トランザクションの記述位置
またトランザクションの記述位置についてもメリットがあり、
過去の実装では、集約がなくエンティティのみを扱っていたので、ユースケース層の処理内にトランザクションを張らざるを得ませんでしたが、集約+集約に対応したリポジトリのおかげで、トランザクションやPrismaの依存をリポジトリ内に閉じ込めることができました。
以上
今回の記事は、自身の過去の勘違いや失敗を元にアップデートした内容をテキストでまとめてみました。
追々、実際のコードについても紹介する記事を書きたいと思っています。
それでは今回はここまでです。