ありあけこういち’s diary

SaaS企業Webエンジニア|HSK6級|福岡ラバー|木管楽器|ITについて、明快な読み物を目指しています。

ドメインサービスについて

はじめに

DDDにおけるドメインサービスについてまとめたいと思います。

ドメインサービスの概要

そもそも「サービス」とは何か

サービスは、エンティティ、値オブジェクト、集約といった「ドメインオブジェクト」に記載するのが不自然なビジネスロジックをサービスに実装します。

以下は引用です。

DDDでは、エンティティ、値オブジェクト、集約といった「ドメインオブジェクト」だけではなく、それらの外に記述したほうがよいロジックも存在します。そのようなときに、状態を持たないステートレスな「サービス」を使用できます。 -- 実践DDD本 第7章「ドメインサービス」~複数の物を扱うビジネスルール~

サービスは主に「ドメインサービス」と「アプリケーションサービス」で分類されます。

ドメインサービスとは

ドメイン層において、複数のエンティティや値オブジェクトを利用して、ビジネスロジックを実現するためのオブジェクトです。
主に、複数の集約*1が必要な計算ロジック、もしくは一つの集約で使えるが、複雑な場合に「ドメインサービス」が使えます。
ドメインサービスはステートレスであり、計算に必要な情報は引数として取得します。ドメインの意味がわかるようなユビキタス言語*2でメソッド名とタイプを表現するとよりコードの可読性があがります。

アプリケーションサービスとは

外部からの入力を受け取り、必要な処理をドメイン*3に依頼し、ドメイン層のオブジェクトを利用してビジネスロジックを実行し返された結果を加工して外部に返す役割を担います。
アプリケーションサービス内には基本的にビジネスロジックを直接実装せず、ドメイン層のオブジェクトを組み合わせて、外部からの入力に対する処理を実現します。
このように、ドメインサービスとアプリケーションサービスは、異なる役割を持つオブジェクトであり、それぞれの責務を分離することで、ビジネスロジックの再利用性や柔軟性を高めることができます。

PHPコードの例

下記PHPコード例では、商品管理システムのドメインサービスとアプリケーションサービスを表現しています。
ProductDomainServiceというドメインサービスで、下記のビジネスロジックを表現しています。

  • 商品が存在しているか否か
  • 同じ名前の商品が存在しているか否か

下記のProductApplicationServiceというアプリケーションサービス、外部から引数を受け取り、商品を閲覧、登録、変更、削除するユースケースCRUD処理)を表現していて、ドメインサービスにあるビジネスロジックを確認してから処理しています。

<?php
declare(strict_types=1);

// ドメインサービス
class ProductDomainService
{
    public function isExistProduct(ProductId $productId): bool {

        // 当該商品の存在チェック
        // ...
        return true;
    }

    public function isExistSameProductName(ProductName $productName): bool {

        // 同じ名前の商品がすでに存在しているか調べる
        // ...
        return false;
    }
}

// 商品を管理(閲覧、登録、変更、削除)するアプリケーションサービス
class ProductApplicationService
{
    private ProductDomainService $productDomainService;
    private ProductFactory $productFactory;
    private ProductRepository $productRepository;

    public function __construct(ProductDomainService $productDomainService, ProductFactory $productFactory, ProductRepository $productRepository)
    {
        $this->productDomainService = $productDomainService;
        $this->productFactory = $productFactory;
        $this->productRepository = $productRepository;
    }

    public function readProduct(ProductId $productId): Product {
        if (!$this->productDomainService->isExistProduct($productId))
        {
            throw new Exception("存在しない商品です。");
        }

        // DBから商品情報を取得
        // ...

        return $product;
    }

    public function registerProduct(ProductName $productName, Price $price): void {
        if ($this->productDomainService->isExistSameProductName($productName))
        {
            throw new Exception("同じ商品名の商品が既に存在しています。");
        }
        $product = $this->productFactory->create($productName, $price);
        $this->productRepository->save($product);
    }

    public function changeProductName(Product $product, ProductName $productName): void {
        if (!$this->productDomainService->isExistProduct($product->getProductId()))
        {
            throw new Exception("存在しない商品です。");
        }
        if ($this->productDomainService->isExistSameProductName($productName))
        {
            throw new Exception("同じ商品名の商品が既に存在しています。");
        }
        $product->setProductName($productName);
        $this->productRepository->save($product);
    }

    public function changeProductPrice(Product $product, Price $price): void {
        if (!$this->productDomainService->isExistProduct($product->getProductId()))
        {
            throw new Exception("存在しない商品です。");
        }
        $product->setPrice($price);
        $this->productRepository->save($product);
    }

    public function deleteProduct(Product $product): int {
        $productId = $product->getProductId();
        if (!$this->productDomainService->isExistProduct($product->getProductId()))
        {
            throw new Exception("存在しない商品です。");
        }
        return $this->productRepository->delete($productId);
    }
}

// ファクトリ
class ProductFactory
{
    public function create(ProductName $productName, Price $price): Product {

        $productId = new ProductId(uniqid());
        // ...

        return new Product($productId, $productName, $price);
    }
}

// リポジトリ
class ProductRepository
{
    public function findById(ProductId $id): Product
    {
        // IDでProduct取得処理
        // ...

        return $product;
    }

    public function save(Product $product): void
    {
        // Product保存処理
        // ...
    }

    public function delete(ProductId $productId): int
    {
        // Product削除処理
        // ...

        return $rowCount;
    }
}

// 商品エンティティ
class Product
{
    private ProductId $productId;
    private ProductName $productName;
    private Price $price;

    public function __construct(
        ProductId $productId,
        ProductName $productName,
        Price $price
    ){
        $this->productId = $productId;
        $this->productName = $productName;
        $this->price = $price;
    }

    /**
     * @return ProductId
     */
    public function getProductId(): ProductId
    {
        return $this->productId;
    }

    /**
     * @return ProductName
     */
    public function getProductName(): ProductName
    {
        return $this->productName;
    }

    /**
     * @param ProductName $productName
     */
    public function setProductName(ProductName $productName): void
    {
        $this->productName = $productName;
    }

    /**
     * @return Price
     */
    public function getPrice(): Price
    {
        return $this->price;
    }

    /**
     * @param Price $price
     */
    public function setPrice(Price $price): void
    {
        $this->price = $price;
    }
}

// 値オブジェクト:商品名
class ProductName
{
    /**
     * @var string
     */
    private string $value;

    /**
     * @param string $value
     */
    public function __construct(string $value)
    {
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->value;
    }
}

// 値オブジェクト:商品ID
class ProductId
{
    /**
     * @var string
     */
    private string $value;

    /**
     * @param string $value
     */
    public function __construct(string $value)
    {
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function __toString(): string
    {
        return $this->value;
    }
}

// 値オブジェクト:商品価格
class Price
{
    /**
     * @var int
     */
    private int $value;

    /**
     * @param int $value
     */
    public function __construct(int $value)
    {
        $this->value = $value;
    }

    /**
     * @return int
     */
    public function toInt(): int
    {
        return $this->value;
    }
}

// 使用例
$productId = new ProductId("4b3403665fea6");
$productDomainService = new ProductDomainService();
$productFactory = new ProductFactory();
$productRepository = new ProductRepository();
$productApplicationService = new ProductApplicationService($productDomainService, $productFactory, $productRepository);

// 商品登録
$productApplicationService->registerProduct(new ProductName("ハンドクリーム"), new Price(500));

// 商品閲覧
$product = $productApplicationService->readProduct($productId);

// 商品名変更
$potato = new Product(new ProductName("じゃがいも"), new Price(100));
$productApplicationService->changeProductName($potato, new ProductName("ジャガイモ"));

// 商品価格変更
$chicken = new Product(new ProductName("チキン"), new Price(400));
$productApplicationService->changeProductPrice($chicken, new Price(500));

// 商品削除
$chair = new Product(new ProductName("椅子"), new Price(1000));
$productApplicationService->deleteProduct($chair);
?>

参考記事、書籍

*1:集約については、現在の記事では具体的には説明しませんが、軽く触れておくとオブジェクトのまとまりで整合性を保ちながらデータを更新する単位であり、関連エンティティと値オブジェクトを概念的に束ねたものだと筆者は理解しています。一つの集約に属したエンティティは基本的に他の集約には属しません。集約にドメインサービスを渡すのは、アプリケーションサービスの責務になります。

*2:プロジェクトには認識の齟齬や翻訳にコストをかけないためにも共通言語を作ることが求められます。そういったプロジェクトにおける共通言語のことをユビキタス言語といいます。ユビキタスは「いつでもどこでも存在する」といった意味です。つまり、ユビキタス言語には、プロジェクトのいたるところで使われなくてはならないという意味が込められています。ドメインエキスパートとの会話はもちろん、開発者同士の会話においてもユビキタス言語は使われ、そしてコードにもユビキタス言語は現れます。

成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.518). Kindle 版.

*3:ドメイン層には 1. エンティティ 2. 値オブジェクト 3. 集約 4. リポジトリ 5. ドメインサービス があります。引用元: * 최범균, 도메인 주도 개발 시작하기, 한빛미디어, 2022, p80.