概要
こんにちは。サーバーサイドの窪田です。 今回はヤプリの開発現場のプロジェクトで実際に戦略的DDDを実践してみたのでその紹介をしようと思います。
ヤプリではアプリケーションの設計パターンにDDDを採用し、実装しています。 一方で別言語で書かれた処理をGoにリプレイスした経緯や開発体制の事情でモデリング等はエンジニアの頭の中でほとんど行われていました。 その結果、コードとしては責務を持った関数群によって実装することができ、戦術的DDDの面では成功しました。 一方で、エンジニア独自のモデリングによりDB構成を知らないと理解できない仕様なども多く存在し、戦略的DDDの面では恩恵を十分に享受できていない部分もありました。
ちょうど、一部新機能を作るプロジェクトが立ちましたので、今回は戦略的DDDについても取り組んでみました。
取り組み
言葉の定義とコンテキストの境界をエンジニアと開発ディレクターで決める
『実践ドメイン駆動設計』ではドメインエキスパートと開発者で共通の認識を持った言葉(いわゆるユビキタス言語)を使う重要性について述べられています。 この言葉の定義をエンジニアだけでなく、開発ディレクターやデザイナーも含めて行いました。 具体的な言葉はぼかしますが、FigJam上で議論して一つずつ定義しました。
また、同時にコンテキストの境界も定義しました。 DDDにおけるコンテキストとは、言葉の定義の共通認識がとれている範囲を示します。 例えば、YappliというノーコードCMSのプロダクトについて、「アプリ」という言葉が出てきます。 しかし、コンテキストによってアプリはデザインを設定するという振る舞いを持ったり、App Storeに申請するという振る舞いを持ったりします。 このように実際の業務や人によって同じ「アプリ」という言葉の意味がずれます。 その共通で認識をとれる範囲が一つのコンテキストになり、その境界が重要になるため、本には「境界づけられたコンテキスト」と表現されます。
『実践ドメイン駆動設計』では一つの境界づけられたコンテキストは一つの開発チームが開発することを推奨しています。その粒度で、今回のプロジェクトの範囲で境界を決め、定義した言葉が通用する範囲と決めました。
ドメインモデル図をかく
言葉の定義ができるとドメインモデル図をかくことができます。 ドメインモデル図では
- ドメインモデル(entityと値オブジェクト)
- ドメインモデルの属性と型
- それぞれのドメインモデルの関係
- 集約の範囲
を表現することができます。 例を示します。
User entity, Item entityがあり、Userが複数のItemを処理できるドメインがあったとして、それぞれのentityを定義します。 また、それぞれのentityの属性(name等)を定義します。UserとItemの関係も示します。 具体的なドメインモデル図の書き方は『ドメイン駆動設計 モデリング/実装ガイド』等にわかりやすく書かれています。 これらを元にドメインモデル図を作成しました。 ぼかしますが、以下のようになりました。
登場するモデルとそれらの関係、集約境界、コンテキスト境界を表現しています。
ER図をかく
ドメインモデルとその関係が決まったのち、システム的に最適化したデータ設計を考えて行きました。 このプロセスは完全にエンジニアのみで完結します。 ドメインモデルの存在は強く意識せず、システムの都合でデータベース定義を考えました。
ドメインモデル図をかくメリット
ドメインモデル図をかくメリットを述べます。
ビジネスサイドとエンジニア間での共通認識がとれる
DDDの中で、ビジネスエキスパートとエンジニアとの共通認識は一つのテーマになっています。 その明文化されたアウトプットとしてドメインモデル図が良い役割を果たします。 例えば、考慮もれや仕様変更がある時は必ず図を元にMeetingやコミュニケーションを取ります。 その結果、どの部分をどのように変えるかまでをMeetingの中で決めることができるようになりました。
ドキュメントになる
上記のように、共通認識を取るためのドキュメントとしても役立ちます。 開発時点ではプロジェクト内での共有された情報になりますが、最終的には明文化された仕様として残り続けます。
実装が決まる
個人的にはこれが一番大きいメリットだと思っています。 ドキュメントや図を書く時間がないから省略して実装するという話がよくあります。 しかし、ドメインモデル図を書くことで実は実装の一部の作業も進んでいます。
ドメインモデル図によって決まること
- entity、値オブジェクトの種類
- entityの属性と型
- repositoryのインターフェイス
- repositoryの実装
まず、どんなentityや値オブジェクトを実装するかが決まりま。また、そのモデルの属性も決まるので実装ができます。
// domain/entity/user.go package entity type User struct { ID int Name string } func NewUser(id int, name string) *User { ... }
までは実装できます。 repositoryに関してもドメインモデル図上で集約の境界が決まるので同時にrepositoryの種類が決まります。 インターフェイスと
// domain/repository/user_repository.go package repository type UserRepository interface { Find(ctx context.Context, id int) (*User, error) Save(ctx, context.Context, e *User) error }
そのインターフェイスを満たすインフラ層の実装をかくことができます。
// infrastructure/repository/impl/user_repository_impl.go package impl type userRepository {} func NewUserRepository() repository.UserRepository { return &userRepository{} } func (u *userRepository) Find(ctx context.Context, id int) (*entity.User, error) { ... } func (u *userRepository) Save(ctx context.Context, e *entity.User) error { ... }
ドメインモデル図によって決まること
- entityの振る舞い(method)
- userCase
一方でまだ実装できない部分もあります。useCaseです。 クライアントサイドでアプリケーション上でどんな行動をするのかなどは詳細なuseCase分析で細かく仕様にした段階でやっとそれを実装できます。useCase上でドメインモデルがどのように振る舞うかもこの時点で実装すれば良いと思います。
以上のことから、実はドメインモデルは実装の方針の大部分を決める作業でもあります。 その点でメリットを感じることができました。
残された課題
この戦略的DDDの取り組みは単体で見ると成功しました。 しかし、ドメインモデル図を長く仕様書のように扱い、メンテナンスされている状態を作り、ビジネスサイドとエンジニアで共通認識を持ち続けるということがやはり課題になってきます。 単純に実装方針だけでなく組織の運営や情報の管理のことまで考えると難しい部分はあります。 今回は1プロジェクトで半ば試行的に戦略的DDDに取り組んだということで、長期戦略としてどのようなバリューを発揮してくれるかは未知です。
まとめ
戦略的DDDは体系化し辛く、プロダクトによって方針が大きく変わります(だから本や記事も少ない)。 その中で開発・ビジネスサイドで連携して取り組めたことでDDDの恩恵を最大限享受できる方向に進み始めたことはとても良かったです。 一方で、上で述べたような課題も多くあります。DDDと向き合いながらプロダクト開発していくことにサーバーサイドエンジニアは日々向き合っています。 少しでもこの領域に興味があれば、カジュアル面談にお越しください!