DDD 在域服务上注入存储库 VS 在应用程序服务上编排流程

DDD inject repository on domain service VS orchestrate flow on application service

我目前正在使用 DDD,我对应用程序服务 VS 域服务 VS 存储库接口有疑问

据我所知:

因此,考虑到例如这个用例:

"当你在系统中创建一个新的汽车实体(uuid,名称)时,汽车名称必须是唯一的(没有更多的汽车存在这个名称)或者汽车名称不能包含数据库中的另一个汽车名称作为子字符串”,例如,这只是一个用例示例,它迫使我在创建对象时查看存储库中的其他实体

所以问题是:我应该在哪里检查and/or注入存储库接口?

- 选项 1) 在应用程序服务中,注入 RepositoryCarInterface,进行检查并保存 Car:

class CreateCarApplicationService
{
    private carRepositoryInterface $carRepository;

    public function __construct(CarRepositoryInterface $carRepository)
    {
        $this->carRepository = $carRepository;
    }

    public function __invoke(string $carUuid, string $carName): void
    {
        $this->ensureCarNameIsUnique($CarName);
        $car = new Car($carUuid,$carName);
        $this->carRepository->save($car);
    }

    private function ensureCarNameIsUnique(string $carName): void
    {
        $CarSameName = $this->carRepository->findOneByCriteria(['name' => $carName]);
        if ($carSameName) {
            throw ExceptionCarExists();
        }
    }
}

- 选项 2) 将此逻辑创建到域服务中(目的是使域逻辑靠近域对象)并从更简单的应用程序服务中调用它它最终负责保存与数据库交互的模型:

class CreateCarDomainService
{
    private carRepositoryInterface $carRepository;


    public function __construct(carRepositoryInterface $carRepository)
    {
        $this->carRepository = $carRepository;
    }

    public function __invoke(string $carUuid, string $carName): Car
    {
        $this->ensureCarNameIsUnique($CarName);
        return new Car($carUuid,$carName);
    }

    private function ensureCarNameIsUnique(string $carName): void
    {
        $CarSameName = $this->carRepository->findOneByCriteria(['name' => $carName]);
        if ($carSameName) {
            throw ExceptionCarExists();
        }
    }
}
class CreateCarApplicationService
{
    private carRepositoryInterface $carRepository;
    private CreateCarDomainService $createCarDomainService;

    public function __construct(CarRepositoryInterface $carRepository)
    {
        $this->carRepository = $carRepository;
        $this->createCarDomainService = new CreateCarDomainService($carRepository)
    }

    public function __invoke(string $carUuid, string $carName): void
    {
        $car = $this->createCarDomainService($carUuid,$carName);
        $this->carRepository->save($car);
    }

}

我不太确定将存储库接口注入域服务的事实,因为正如 Evans 所说:

好的服务具有三个特点:

-操作涉及的领域概念不是实体或值对象的自然组成部分

-接口是根据域模型的其他元素定义的

-操作是无状态的

但我想尽可能深地推进我的领域逻辑

而且,正如我在其他 Whosebug 帖子中读到的那样,在域对象中注入存储库不是 allowed/recommended:

Do you inject a Repository into Domain Objects?

Should domain objects have dependencies injected into them?

选项 1

理想情况是存储库仅供您的编排(应用程序)层使用,与您的领域模型(领域层)完全无关。因此,您的存储库将被注入到您的协调器中,而不是注入到您的域模型中(选项 1)。

在你的例子中,你有一个编排层

  • 已将汽车存储库注入其中
  • 从存储库加载汽车名称
  • 使用 DDD 来验证新车的名称不在现有汽车的名称中等
  • 如果是:在域中创建汽车;如果否:域验证失败
  • 使用 repo 在域上保存状态更改(在这种情况下,使用 repo 保存新车)
  • returns 结果(如果 request/reply 场景)

虽然这有一个小问题。您可能会争辩说,获取汽车名称并将其传递给针对存储库的查询以查看该名称是否唯一会更有效。的确如此,但代价是您的某些域逻辑(检查唯一性)已从域移出到存储库和编排层。

所以,请仔细考虑您更喜欢哪个。

选项 1,场景 1:尽可能 DDD

// inefficient, but we're done with the repo immediately
var carNames = repo.GetCarNames();
// all the following calls are on our domain, easily testable
var carCreator = new CarCreator(names);
var carCreationResult = carCreator.TryCreateCar(carNames, newCar);
if (carCreationResult.Failed) return carCreationResult.Errors;
// finally save and return
repo.Save(carCreationResult.Car);
return carCreationResult.Car;

在上面,TryCreateCar 可以作为对 carCreator 内部字典的简单检查来实现——完全在域内,可测试,并且不依赖于 repo。

选项 1,场景 2:高效

// uniqueness check requires repo; mixes in domain concept of uniqueness with a repo query
var canCreateCar = repo.IsCarUnique(newCar.Name)
if (!canCreateCar) return error;
// creation separated from uniqueness check; wouldn't have to check uniqueness in TryCreateCar (it was checked above)
var carCreator = new CarCreator(newCar);
var carCreationResult = carCreator.TryCreateCar(carNames, newCar);
if (carCreationResult.Failed) return carCreationResult.Error;
// finally save and return
repo.Save(carCreationResult.Car);
return carCreationResult.Car;

回购上的 IsCarUnique 方法隐藏了一些域逻辑!

选项 2

我们将取消此选项,因为我们只是不希望非域关注点成为我们域模型的依赖项。这就是避免这种情况的全部原因。当您将非域关注点设为依赖项时,您的域模型将变得更难测试。

更糟糕的是,我看到了交错问题的代码。想象一下编排层的情况,它通过 repo 获取一些实体,对域进行一些更改,保存一些实体,加载更多实体,将 repo 注入域以便它可以使用 repo 加载更多实体,然后最后救球。这是一个无法测试且难以 read/maintain 的混乱局面!

总结

选项 1 场景 1 允许我们将所有领域关注点放在一起并封装起来。这是非常值得的。如果规则发生变化,我们只需修改我们的领域模型的数据和行为,保持编排不变。