🛠️

【ドメイン駆動設計】リポジトリの実装方法

2023/11/04に公開

🎯目的

実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘」にて紹介されているリポジトリの実装方法についてわかりやすく解説し、本書に則ったリポジトリを実装できるようにする

🔈背景

最近、多くの企業でドメイン駆動設計を導入している印象を受けます。私自身もこれまでヤフー株式会社やBASE株式会社、その他の企業でドメイン駆動設計を用いた設計と実装を行ってきました。しかし、いくつかの企業ないしはチームでは、軽量DDDやビジネスロジックがほとんどドメインサービスに記述されてドメイン貧血症になっていたり、大きすぎる集約などドメイン駆動設計ではアンチパターンと呼ばれる誤った設計を見てきました。これらの問題は大抵「実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘」の通りに設計、実装すれば回避できます。

しかし、実際は「実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘」が難しくてなかなかベストな設計が行えていないのでは?と感じました。私も本書だけでは解釈を誤ったりして間違った実装をしてしまうことがありましたが、ヤフー在籍時代にドメイン駆動設計に詳しい先輩からペアプロやPRレビューを通じてわかりやすく解説してもらって理解できるようになりました。

そのため、この記事は、「実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘」の引用を用いて、根拠を示した上でできるだけわかりやすく実装方法を紹介します。

💡前提

💡リポジトリとは

ドメイン駆動設計におけるリポジトリとは、「集約をデータベースに保存させるようにする」「保存した集約をデータベースから取得できるようにする」ためのオブジェクトです。


実践DDD本 第12章「リポジトリ」~集約の永続化管理を担当~ (1/3)|CodeZine(コードジン)より引用

💡基本的な考え

実践ドメイン駆動設計で示しているリポジトリの基本的な考え方はわかりやすく言うと以下です。

  1. 集約のコレクションがメモリ上にあると錯覚させることができるようにリポジトリを実装する
  2. 集約はよく知られているインターフェースを経由してアクセスできるようにすること
  3. 集約の追加と削除を行うメソッドを提供すること
  4. 集約を取得できるファインダーメソッドを提供すること
  5. 集約に対してのみ、リポジトリを提供すること

※コレクション・・・PHPならarrayDs\Set、Javaならjava.utils.Setjava.utils.Mapなどの複数のオブジェクトを格納できる型のこと。

1.集約のコレクションがメモリ上にあると錯覚させることができるようにリポジトリを実装する

グローバルアクセスを必要とするオブジェクトの各型に対して、あるオブジェクトを生成し、その型のすべてのオブジェクトで構成されるコレクションが、メモリ上にあると錯覚させることができるようにすること
実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘

これはつまり、

  • リポジトリのメソッドの命名をよく知られているインターフェースに合わせる(次節で紹介します)
  • リポジトリから取得した集約を変更したときに、その集約がデータベースに反映されていること
    • PHPのarrayから取得したインスタンスを変更するとarray内の該当インスタンスも変更されています
    • ただし、データベースによってはこれを実現できないこともあります
リポジトリのメソッドの命名をよく知られているインターフェースに合わせる
$this->productRepository->add($product);
$this->productRepository->remove($product);
リポジトリから取得した集約を変更したときに、その集約がデータベースに反映されていること
$product = $this->productRepository->productOfId($productId);
$product->price = new Price(100, currency: Currency::Yen, taxRate: 0.1);

// $this->productRepository->add($product)をしなくともDBに保存されるようにする
Q. なぜ、メモリ上にあると錯覚せることができるようにする必要があるのか?

A. 利用するデータベースや永続化処理をドメイン層、アプリケーション層から秘匿し、開発者がビジネスロジックの実装、設計に集中できるようにするため

まず大前提として、ドメイン駆動設計では、ビジネスロジックを表現するドメインモデルが中心となります。このドメインモデルは、ビジネスの要求事項を正確に表現し、変更に対して柔軟かつ拡張可能であるべきです。そのため、ドメインモデルの設計は非常に重要です。

リポジトリは、ドメインモデルとデータベース(または他の永続化メカニズム)の間の仲介役として使用されます。メモリ上にあると錯覚させるようにすることで、ドメインモデルはデータベースの詳細から隔離され、純粋にビジネスロジックに集中できます。具体的な実装がデータベースと密接に結びついていると、ドメインモデルの変更が難しくなり、ビジネスロジックの柔軟性が損なわれる可能性があります。

2.集約はよく知られているインターフェースを経由してアクセスできるようにすること

よく知られているグローバルインターフェイスを経由してアクセスできるようにすること。
実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘

これは、リポジトリのメソッド名はよく知られているインターフェースのメソッド名を真似て定義するということです。例えばJavaならjava.util.Set、PHPならDs\Setなどのよく知られているインターフェースのメソッドです。これらのメソッドを真似ることで、「1.集約のコレクションがメモリ上にあると錯覚させることができるようにリポジトリを実装する」を表面上である程度実現できます。

❌標準のコレクションインターフェースではあまり知られていないメソッド名 ❌集約がメモリ上にあるように思えない
interface ProductRepository
{
    public function create(Product ...$product): void;
    public function update(Product ...$product): void;
    public function findById(ProductId $productId): Product;
    public function delete(ProductId ...$productIds): void;
}
⭕️よく知られたメソッド名 ⭕️集約がメモリ上にあるように思える
interface ProductRepository
{
    public function add(Product ...$product): void;
    public function get(ProductId $productId): Product; 
    public function remove(Product ...$products): void;
}

3.集約の追加と削除を行うメソッドを提供すること

オブジェクトの追加と削除を行うメソッドを提供すること
実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘

コレクション指向リポジトリ(≒RDBを使うリポジトリ)
interface ProductRepository
{
    public function add(Product ...$product): void;
    public function remove(Product ...$product): void;
}
永続指向リポジトリ(≒NoSQLを使うリポジトリ)
interface ProductRepository
{
    public function save(Product ...$product): void;
    public function remove(Product ...$product): void;
}

4.集約を取得できるファインダーメソッドを提供すること

ある条件に基づいてオブジェクトを選択し、属性値が条件に一致するような、完全にインスタンス化されたオブジェクトかオブジェクトのコレクションを戻すメソッドを提供すること
実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘

  • あくまで取得するのは基本的に「集約」「集約のコレクション」のみです
  • 集約を構成する一部の値オブジェクト(下記の例だとPrice)だけをリポジトリから取得する場合は、集約を経由したアクセスが許容できないほどのボトルネックになってしまっているなど、パフォーマンス上の問題に対応しなければいけない場合などくらいなので、そうでない場合は集約を取得して、アクセスするようにしましょう
  • クライアントの画面に集約のデータを表示する場合などで、別の異なる種類の集約の一部同士を組み合わせて表示させるときは、クエリサービスを利用する
    • ただし、このようなクエリサービスが多く出てきた場合は、おそらく集約の設計を誤った可能性が高いです
⭕️集約のみを取得している
interface ProductRepository
{
    public function productOfId(ProductId $productId): Product;
    /**
     * @return Product[] array;
     */
    public function productsOfShop(Shop $shop): array;
    /**
     * セール期間中の商品一覧を取得できる
     *
     * @return Product[] array;
     */
    public function saleProducts(Shop $shop, SalePeriod $salePeriod): array;
}
❌集約以外を取得している
interface ProductRepository
{
    public function priceOfProduct(ProductId $productId): Price;
    /**
     * @return Image[] array;
     */
    public function imagesOfProduct(ProductId $productId): array;
    /**
     * 直近の商品登録日時を返す
     *
     * @return DateTime[] array;
     */
    public function latestProductCreatedDateTime(Shop $shopId): array;
}

👉実装

✍️リポジトリの利用方法

  • リポジトリはアプリケーションサービスで利用する
コレクション指向リポジトリの利用
class ProductApplicationService
{
    // ...

    public function changePrice(ChangeProductPriceCommand $command): void
    {
        $productId = new ProductId($command->productId);
        $product = $this->productRepository->productOfId($productId);
	
	$newPrice = new Price(
	    $command->amount, 
	    Currency::valueOf($command->currency), 
	    $command->taxRate
	);
	$product->setPrice($newPrice);
    }

    // ...
}
永続指向リポジトリの利用
class ProductApplicationService
{
    // ...

    public function changePrice(ChangeProductPriceCommand $command): void
    {
        $productId = new ProductId($command->productId);
        $product = $this->productRepository->productOfId($productId);
	
	$newPrice = new Price(
	    $command->amount, 
	    Currency::valueOf($command->currency), 
	    $command->taxRate
	);
	$product->setPrice($newPrice);
	
	$this->productRepository->save($product);
	// 永続指向リポジトリの場合は、明示的にsaveする必要がある
    }

    // ...
}

🛠️設計パターン

コレクション指向リポジトリ

リポジトリは、Set の挙動を真似る必要がある。裏側の実装にどんな永続化メカニズムを使っていようが、同じオブジェクトを複数追加できるようにしてはいけない。また、リポジトリから取得したオブジェクトを変更したときに、それをリポジトリに「書き戻す」必要がないようにしなければいけない。
実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘

Java) java.util.Setを真似た設計にした
public interface ProductRepository
{
    public void add(Product product);
    public void addAll(Collection<Product> products);
    public void remove(Product product);
    public void removeAll(Collection<Product> products);
    public Product productOf(ProductId productId);
    public Collection<Product> productsOf(Shop shop);
}
PHP) Ds\Setクラスを真似た設計にした
interface ProductRepository
{
    public function add(Product ...$product): void;
    public function remove(Product ...$product): void;
    public function get(ProductId $productId): Product;
    /**
     * ショップが所有するすべての商品を返却する
     * @return Product[] array
     */
    public function filterProductsOwnedBy(Shop $shop): array;
}

永続指向リポジトリ

コレクション指向の方式ではうまくいかない場合は、永続指向のリポジトリを使う必要がある。使おうとしている永続化メカニズムに、オブジェクトの変更を検出・追跡する機能がない場合は、コレクション指向の方式が使えない。インメモリのデータファブリック(4) を使う場合や、NoSQL のキーバリューストアを使う場合などが、その一例だ。新しい集約のインスタンスを作ったり、既存のインスタンスを変更したりするたびに、save() メソッドなどを使ってそれをデータストアに書き込む必要がある。
実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘

Java) java.util.Mapを真似た設計にした
public interface ProductRepository
{
    public void put(Product product);
    public void putAll(Collection<Product> products);
    public void remove(Product product);
    public void removeAll(Collection<Product> products);
    public Product productOf(ProductId productId);
    public Collection<Product> productsOf(Shop shop);
}

実践ドメイン駆動設計 | ヴォーン・ヴァーノン, 髙木 正弘では、Oracle Coherenceのデータグリッドを使うために、putではなくsaveメソッドと定義している。

PHP) Ds\Mapクラスを真似た設計にした
interface ProductRepository
{
    public function put(Product ...$product): void;
    public function remove(Product ...$product): void;
    public function get(ProductId $productId): Product;
    /**
     * ショップが所有するすべての商品を返却する
     * @return Product[] array
     */
    public function filterProductsOwnedBy(Shop $shop): array;
}

🔗参考文献

Discussion