【问题标题】:DDD inject repository on domain service VS orchestrate flow on application serviceDDD 在域服务上注入存储库 VS 在应用程序服务上编排流程
【发布时间】:2021-10-12 15:36:22
【问题描述】:

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

据我所知:

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

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

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

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

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

- 选项 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 所说:

一个好的服务具有三个特征:

-该操作与不是实体或值对象的自然部分的域概念有关

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

-操作是无状态的

但我想把我的领域逻辑推到我无法做到的深度

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

Do you inject a Repository into Domain Objects?

Should domain objects have dependencies injected into them?

【问题讨论】:

    标签: dependency-injection repository domain-driven-design


    【解决方案1】:

    选项 1

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

    在您的情况下,您有一个编排层

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

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

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

    选项 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;
    

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

    选项 2

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

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

    总结

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

    【讨论】:

    • 您好,Kit,首先感谢您的完整回答,您已使其易于阅读和逐步理解。我将编写您的选项1 /方案1,甚至效率低一点,真实模型中的行数并不多,因此,我更喜欢将域逻辑放在一个域服务中;示例中的“CarCreator”。谢谢
    猜你喜欢
    • 2016-10-15
    • 2011-09-29
    • 2015-01-03
    • 1970-01-01
    • 2018-05-27
    • 1970-01-01
    • 1970-01-01
    • 2013-06-04
    • 1970-01-01
    相关资源
    最近更新 更多