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

jon*_*nyc 5 domain-driven-design dependency-injection repository

我目前正在使用 DDD,我有一个关于应用程序服务 VS 域服务 VS 存储库接口的问题

我所知:

  • 应用程序服务用于处理用例流,包括域之上所需的任何其他问题。

  • 领域服务用于封装不适合单个领域对象的行为。

因此,考虑到以下用例:

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

所以问题是:我应该在哪里进行检查和/或注入存储库接口?

- 选择 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

- 选择 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
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);
    }

}
Run Code Online (Sandbox Code Playgroud)

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

良好的服务具有三个特点:

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

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

- 操作是无状态的

但我想将我的领域逻辑推得尽可能深

而且,正如我在其他 StackOverflow 帖子中读到的那样,不允许/建议在域对象中注入存储库:

您是否将存储库注入域对象中?

域对象是否应该注入依赖项?

Kit*_*Kit 2

选项1

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

就您而言,您有一个编排层

  • 已注入汽车存储库
  • 从存储库加载汽车名称
  • 使用DDD来验证新车的名称是否在现有汽车的名称中等。
  • 如果是:在域中创建汽车;如果否:域验证失败
  • 使用存储库来保存域上的状态更改(在本例中,使用存储库保存新车)
  • 返回结果(如果是请求/回复场景)

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

因此,请仔细考虑您更喜欢哪个。

选项 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;
Run Code Online (Sandbox Code Playgroud)

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

选项 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;
Run Code Online (Sandbox Code Playgroud)

不过,存储库上的 IsCarUnique 方法隐藏了一些域逻辑!

选项2

我们将忽略此选项,因为我们只是不希望非域关注点成为域模型的依赖项。这就是避免这种情况的总原因。当您采用非域关注点并将其设为依赖项时,您的域模型将变得更难以测试。

更糟糕的是,我见过交错关注点的代码。想象一下这样的情况:编排层通过存储库获取一些实体,对域进行一些更改,保存一些实体,加载更多实体,将存储库注入到域中,以便它可以使用存储库加载更多实体,最后节省。这是一个无法测试且难以阅读/维护的混乱!

总之

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