はじめに
本記事では、初学者向けにドメイン駆動設計(domain-driven design)についての、基本的な考え方と実装における基本概念について解説を行います。
ドメイン駆動設計(domain-driven design)とは?
ドメイン駆動設計とは、その名の通り "ドメイン" の知識にフォーカスした設計手法です。
ここで言う "ドメイン" とは、「ソフトウェアを使って問題解決しようとしている領域」や「プログラムを適用する対象となる業務領域」のなどを指します。
具体的には、会計システムにおける「金銭」や「振込処理」、SNSにおける「投稿」や「ユーザー」などが該当します。
これらのドメインを含め、システムが扱う業務仕様やビジネスルールを軸に設計を行い、
最適な業務実現・課題解決をしていこうという手法をドメイン駆動設計と呼びます。
ざっくり言うと、良いシステムを構築するための設計のベストプラクティスに近い話です。
業務の要件やビジネスルールに重きをおいた設計をすることで、よりユーザーのニーズを満たした開発を行うことができます。
ドメイン駆動設計の原則
ドメイン駆動設計には以下の4つの原則が存在します。
- Focus on the core complexity and opportunity in the domain
- Explore models in a collaboration of domain experts and software experts
- Write software that expresses those models explicitly
- Use a ubiquitous language inside a bounded context
出典:https://www.zweitag.de/en/blog-article/ddd-europe-2016
ふわっと翻訳すると、
- 複雑なドメインの中核的な複雑性と機会に焦点を当て、課題を適切に理解しよう
- 業務の専門家・ソフトウェアの専門家と協力してドメインモデルを作ろう
- ドメインモデルを明示的に表現するコードを書こう
- 境界づけられたコンテキスト内で共通の言語を使おう
となります。
1つずつ解説すると、
複雑なドメインの中核的な複雑性と機会に焦点を当て、課題を適切に理解しよう
ドメイン(システム化する業務範囲のことでしたね)が抱える課題や問題を適切に理解しようということです。
これはドメイン駆動設計の重要項目でもあります。
ビジネスアプリケーションのソフトウェア開発には数多くの要件や機能が存在し、その中には複雑で重要な要件から、あまり重要でない要件もあります。
それらを適切に把握し、ソフトウェアモデルやアーキテクチャを構築することで、ビジネス価値を最大限に引き出す優れたアプリケーションを実現できます。
業務の専門家・ソフトウェアの専門家と協力してドメインモデルを作ろう
ビジネス側・開発側の専門家がしっかり繋がり、共通の認識を持った上で設計を進めようという内容です。
当たり前ですが、ビジネスドメインの専門家はビジネスプロセスや業界知識に深い理解を持ち、開発の専門家は技術面の優れた知識を持ちます。これらの知識をすり合わせ、最大限に活用することでビジネス要件をより深く理解し、優れたドメインモデルを構築することが可能になります。
ドメインモデルを明示的に表現するコードを書こう
ビジネスドメインを明確に表現するために、ソフトウェアのコードやアーキテクチャにドメインモデルを明示的に反映させようというものです。
これについては抽象度が高いため、後ほど詳しく説明します。
境界づけられたコンテキスト内で共通の言語を使おう
エンジニアも非エンジニアも共通の用語を用いることで、共通の認識を持ち正確な理解をしようという内容です。
ここで言う "境界づけられたコンテキスト" とは、ビジネスドメインを意味や役割ごとに分けたまとまりを指します。
例えば、ECサイトでの「顧客の注文と支払いの領域」「商品在庫に関する領域」「配送業務に関する領域」などがこれに該当します。
要するに、各領域で共通の用語を用いることで、認識の齟齬を産まないようにしようね ということです。
ちなみに、この共通の言語のことを "ユビキタス言語" などと呼ぶこともあります。
どの原則も、ドメインに対する理解を重要視していることがわかりますね。
ここがドメイン駆動設計の軸となる概念になります。
ドメインモデルとは
先程から何度か「ドメインモデル」という言葉が出てきました。
これを簡単に説明すると、「業務仕様やビジネスルールを図や文章で表現したもの」です。
ビジネスドメイン(特定の業務領域)を理解し、その本質的な要素を抽出して表現するものであり、システムを開発する際に、ビジネスドメインを正確にモデル化し、ソフトウェアの設計や実装に反映させるための基盤となります。
また、これは設計における共通言語にもなり得ます。
そのため、チーム全体がドメインモデルを共有し、背景の認識を揃えることも重要です。
ドメインモデルはプロジェクトの進行に合わせて進化し、柔軟に変更に対応することができるようになります。
モデリング
ドメインモデルを作成する際は、まずコンテキスト(ビジネスドメインを役割ごとに分けたもの)を特定し図や文章を使ってモデルの抽出を行います。
ドメイン駆動設計では、モデリングの手法として定められたものは無く、チームやプロジェクトによって様々です。
今回は、モデリング手法については省略しますが、詳しく学びたい方はこちらで紹介されている手法がわかりやすかったので参考にしてみてください。
実装
ドメイン駆動設計の原則にもあった、
Write software that expresses those models explicitly
を実現するためには、上記で作成したドメインモデルをそのままコードに落とし込む必要があります。
本記事では、基本概念の紹介として代表的なドメイン駆動設計の実装パターンを取り上げます。
なお、今回はECサイトを例に実装を行います。
ValueObject
語弊を恐れず一言で言うと、
オリジナルの便利な変数型・単位を作ってあげるようなイメージです。
// ValueObjectの場合
Price price;
ProductName productName;
// 従来のプリミティブ型
int price;
string productName;
ValueObjectの特徴として...
一意性を持たない
hoge_id
のような、自身を特定できるプロパティを持たないことを指しています。
つまり、別のオブジェクトであっても、値が等しければ同一と判断されます。
※逆に、一意性を持つものを、次に出てくる「Entity」と呼びます。
イミュータブルオブジェクトである
イミュータブル、つまり不変であることを意味します。
オブジェクトが生成されたタイミングで値が固定され、その後に状態は変化しません。
Price price = new Price(100);
// これはNG
price.value = 120;
// 値を変更する時は、再生成
price = new Price(200);
副作用を持たない
何かしらの操作により、状態が変化することを副作用を持つと言います。
ValueObjectはイミュータブルなオブジェクトなので、副作用を持つことはNGです。
public class Price
{
private int amount;
// これはNG
public void addAmount(Price other)
{
this.amount += other.amount;
}
}
Price price1 = new Price(100);
Price price2 = new Price(200);
// ここで値が変化してしまっている
price1.addAmount(price2);
ValueObjectの概念を使って、ECサイト内の"価格"モデルを実装すると
import java.util.Objects;
public class Price {
private int amount;
public Price(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("価格は0円以上で指定してください。");
}
this.amount = amount;
}
public int getAmount() {
return amount;
}
public boolean equals(Price other) {
return this.amount == other.getAmount();
}
}
/*----- main -----*/
Price price1 = new Price(1000);
Price price2 = new Price(1000);
if (price1.equals(price2)) {
System.out.println("価格は同じです。"); // 値の等価性によりこちらが表示される
} else {
System.out.println("価格は異なります。");
}
コンストラクタ内で、価格に不正な値が入らないよう制約をつけているのも大事なポイント。
プリミティブ型(intとか)で指定する場合よりも、自由に制約を付けられるので安全性が高いですね。
Entity
Entityは、ドメインモデルの中で特定の識別子(ID)によって区別されるオブジェクトを指します。
ValueObjectとは対称に、一意なIDを持ち、同じIDを持つ別のEntityとは異なるものとして識別されます。
重要なのは、Entityはその属性の値が同じであっても別々のインスタンスとして扱われることです。
Entityの概念を使って、ECサイトの"商品"を実装すると
public class Product {
private ProductID id;
private ProductName name;
private Price price;
// 他の商品属性もここに追加する可能性があります
public Product(ProductID id, ProductName name, Price price) {
this.id = id;
this.name = name;
this.price = price;
}
public ProductID getId() {
return id;
}
public ProductName getName() {
return name;
}
public Price getPrice() {
return price;
}
// 他の商品に関するメソッドもここに追加することができます
}
Repository
リポジトリの役割は、ドメインモデル(Entity)をデータベースなどの永続的なデータストレージとやり取りすることです。永続化の詳細やデータベース操作に関する責務を持つことで、使う側(Entity)がデータベースの詳細を意識することなくビジネスロジックを実装できるようにします。
また、異なるデータベースや永続化メカニズムに対応するため、リポジトリインターフェースを実装する具象クラスにはデータベースアクセスの詳細を記述することになります。これにより、ドメインモデルとデータベースのコードを分離し、より柔軟なアーキテクチャを実現します。
public interface ProductRepository {
Product findById(long productId);
List<Product> findAll();
void save(Product product);
void delete(Product product);
}
// Productリポジトリの具象クラス
public class ProductRepositoryImpl implements ProductRepository {
private List<Product> productList = new ArrayList<>();
@Override
public Product findById(long productId) {
return productList.stream()
.filter(product -> product.getId() == productId)
.findFirst()
.orElse(null);
}
@Override
public List<Product> findAll() {
return productList;
}
@Override
public void save(Product product) {
productList.add(product);
}
@Override
public void delete(Product product) {
productList.remove(product);
}
}
※便宜上、この具象クラスでは、一時的なリストを使用して商品データを管理していますが、実際のアプリケーションではデータベースとのやり取りを行うことが一般的です。
今回の例では、在庫管理に別のサービスを用いているため、具象クラスのsaveメソッドには在庫管理システムのデータベースにアクセスするような何かしらの処理が入ることが予想できますね。
Service
サービスは、EntityやValueObjectだけでは表現しきれない複雑な処理やドメイン間の連携を実装する場合に使用されます。
ざっくり言うと、この処理Entityに実装する?ValueObjectに実装する?どちらも微妙だよな... ってなったときに切り出してあげたやつです。
public class ProductService {
private ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public void createProduct(long id, String name, int priceAmount) {
Price price = new Price(priceAmount);
Product product = new Product(id, name, price);
productRepository.save(product);
}
public List<Product> getAllProducts() {
return productRepository.findAll();
}
}
ProductServiceは、商品(Product)に関連するビジネスロジックや複雑な操作を担当しています。具体的には、商品の作成(createProductメソッド)と全ての商品の取得(getAllProductsメソッド)を提供しています
今回の場合、Productエンティティ自身に商品の作成・取得メソッドを実装するのは不自然ですし、かといって何かしらのValueObjectに実装するのも違和感がありますよね。そこで、Serviceとして切り出すことで責任範囲を適切に分けることができます。
コードの持つ役割・責任範囲を明確にすることで、可読性と保守性が向上し、ドメイン駆動設計において意図したドメインモデルが適切に構築されます。
DDDのメリット
ドメイン駆動設計には、主に以下のようなメリットがあります。
- コードの保守改修が容易になる
- コードを読むだけで業務知識を把握できる
- 設計段階から、ユーザーの抱える課題を適切に理解できる
ドメイン駆動設計は、業務仕様をそのままコードに落とし込んで実装するため、
明示的に業務ルールを表します。
コードをまるで業務仕様書の様に扱える点がいいですね。
逆に、コード量が増えてしまうというデメリットもあります。
まとめ
ドメイン駆動設計とは...
「システムの対象とする業務領域にフォーカスした手法。 ビジネス側・開発側が協力し、共通の言語を用いて背景の認識を合わせながらドメインモデルを作成し、それを基にコードに落とし込んでいくことで、より効果的に課題解決を達成する設計手法」
でした。
最後に
今回は、ドメイン駆動設計における基礎的な概念について学びました。
本記事では、基本的な概念についてのみ記載しましたが、本来はもっと複雑で奥の深いものでもあります。
少しでもDDDの良さを感じていただけたなら、ご自身で今後も学んでみてください。
詳しくドメイン駆動設計について学びたい方は、「エリック・エヴァンスのドメイン駆動設計」という書籍をチェック。
おまけ
世の中には、DDD(Deadline Driven Development)というものも存在します。
またの名を「締め切り駆動開発」です。
納期はしっかり守りましょう。