DDD:逻辑应该在哪里测试实体的存在?

时间:2018-01-11 05:32:49

标签: domain-driven-design ddd-repositories ddd-service

我正在重构一个应用程序,并试图弄清楚某些逻辑应该适合的位置。例如,在注册过程中,我必须根据用户的电子邮件地址检查用户是否存在。因为这需要测试用户是否存在于数据库中,所以似乎该逻辑不应该与模型相关联,因为它的存在是由它在数据库中所决定的。

但是,我将在存储库上有一个方法,负责通过电子邮件等方式获取用户。这将处理有关用户检索的部分(如果存在)。从用例的角度来看,注册似乎是一个用例场景,因此似乎应该有一个UserService(应用程序服务),它有一个调用存储库方法的注册方法,然后执行逻辑,以确定返回的用户实体是否是是否为空。

就DDD而言,我采用这种方法走在正确的轨道上吗?我是否以错误的方式查看这种情况?如果是这样,我应该如何修改我的想法呢?

此链接是作为可能的解决方案Where to check user email does not already exits?提供的。它确实有帮助,但它似乎没有关闭问题的循环。我在本文中似乎缺少的是谁将负责调用CreateUserService,应用程序服务或聚合根上的方法,其中CreateUserService对象将与任何其他相关参数一起注入方法?

如果答案是应用程序服务,似乎您通过将域服务从域层中取出而丢失了一些封装。另一方面,走另一条路将意味着必须将存储库注入域服务。这两个选项中的哪一个更可取,更符合DDD?

4 个答案:

答案 0 :(得分:2)

我认为最适合这种行为的是域名服务。 DS可以访问持久性,以便您可以检查是否存在或唯一性。 查看this blog entry了解详情。

即:

public class TransferManager
    {
        private readonly IEventStore _store;
        private readonly IDomainServices _svc;
        private readonly IDomainQueries _query;
        private readonly ICommandResultMediator _result;

        public TransferManager(IEventStore store, IDomainServices svc,IDomainQueries query,ICommandResultMediator result)
        {
            _store = store;
            _svc = svc;
            _query = query;
            _result = result;
        }

        public void Execute(TransferMoney cmd)
        {
            //interacting with the Infrastructure
            var accFrom = _query.GetAccountNumber(cmd.AccountFrom);

            //Setup value objects
            var debit=new Debit(cmd.Amount,accFrom);

            //invoking Domain Services
            var balance = _svc.CalculateAccountBalance(accFrom);
            if (!_svc.CanAccountBeDebitted(balance, debit))
            {
                //return some error message using a mediator
                //this approach works well inside monoliths where everything happens in the same process 
                _result.AddResult(cmd.Id, new CommandResult());
                return;
            }

            //using the Aggregate and getting the business state change expressed as an event
            var evnt = Transfer.Create(/* args */);

            //storing the event
            _store.Append(evnt);

            //publish event if you want
        }
    }

来自http://blog.sapiensworks.com/post/2016/08/19/DDD-Application-Services-Explained

答案 1 :(得分:0)

您面临的问题称为基于设置的验证。有很多文章描述了可能的解决方案。我将在这里给出one的摘录(上下文是CQRS,但它可以在某种程度上应用于任何DDD架构):

<强> 1。锁定,交易和数据库约束

锁定,事务和数据库约束是用于维护数据完整性的经过试验和测试的工具,但它们需要付出代价。代码/系统通常很难扩展,编写和维护也很复杂。但是他们的优点是可以通过大量的例子来充分理解。通过暗示,这种方法通常使用基于CRUD的操作来完成。如果您想保持事件采购的使用,那么您可以尝试混合方法。

<强> 2。混合锁定字段

您可以采用锁定字段方法。使用唯一约束在标准数据库中创建注册表或查找表。如果您无法插入行,则应放弃该命令。在发出命令之前保留地址。对于这些类型的操作,最好使用最终不一致的数据存储,并且可以保证约束(在这种情况下是唯一性)。额外的复杂性是这种方法的明显缺点,但不太明显的是知道操作何时完成的问题。读取端更新通常在不同的线程或进程中执行,甚至可以在命令机器上执行,并且可能会发生许多不同的操作。

第3。依靠最终一致的阅读模型

对某些人来说,这听起来像是矛盾的,然而,这是一个相当巧妙的想法。系统中始终存在不一致的事情。事件采购允许您处理这些不一致。而不是以数据一致性的名义抛出异常并失去某人的工作。只需记录事件并稍后修复。

另外,您如何知道一致的数据库是否一致?它没有记录用户试图执行的失败操作。如果我尝试更新自我读取后已更新的表中的行,那么我很可能会丢失该数据。这为DBA提供了数据一致性的假象,但尝试向恼怒的用户解释这一点!

接受这些事情并允许业务恢复,可以带来真正的竞争优势。首先,您可以有意识地假设这些问题不会发生,从而使您能够更快/更便宜地交付系统。只有当它们确实发生并且只有它具有商业价值时,才会添加功能来弥补问题。

<强> 4。重新检查域模型

让我们以一个简单的例子来说明如何解决问题所需的全局变化。基本上,我们在检查聚合根的唯一性或基数时遇到问题,因为只有聚合强制执行一致性。一个例子可能是足球队的守门员。门将是一名球员。在任何时候你都可以在球场上每队有1个守门员。数据驱动的方法可能在播放器上有一个“IsGoalKeeper”标志。如果守门员被罚下并且外场球员进入球门,则需要从守门员中移除守门员旗帜并将其添加到其中一个外场球员。您需要制定约束以确保助理经理不会意外地分配不同的球员,从而导致2名守门员。在这种情况下,我们可以对Team,OutFieldPlayers或Game聚合上的IsGoalKeeper属性建模。这样,保持基数变得微不足道。

答案 2 :(得分:0)

你似乎是在正确的方式,我唯一没有得到的是你UserService.register所做的事情。
它应该将所有值注册为用户输入,验证它们(使用存储库检查电子邮件的存在),如果输入有效则存储新用户。

验证涉及复杂查询时可能会出现问题。在这种情况下,您可能需要创建一个具有特殊索引的辅助存储,这些索引适用于您可以对您的域模型执行的查询,因此您必须管理两个可能不同步的存储(用户存在于一个,但它没有在另一个复制,但)。

当您将聚合存储在类似于键值存储的地方时,会发生这种问题,您只能使用聚合的ID进行搜索,但是如果您使用的是允许使用您的实体进行搜索的SQL数据库字段,你可以通过简单的查询做很多事情 你需要注意的唯一事情是避免混合查询逻辑和命令逻辑,在你的例子中你需要做的查找很容易,只是一个字段,结果是一个布尔值,有时它可能比时间操作更难,或者跨越多个表聚合结果的查询,在这些情况下,最好让您的(命令)服务使用(查询)服务,它提供了一个简单的api来进行计算,如:

interface UserReportingService {
    ComplexResult aComplexQuery(AComplexInput input);
}

您可以使用使用您的存储库的类实现,或者直接执行数据库查询的实现(sql或其他)。
不同之处在于,如果您使用存储库,那么您会认为&#34;就你的域对象而言,如果直接用你的数据库抽象(在sql的情况下为表/集,在mongo情况下的文档等等)中直接编写查询。一个或另一个取决于您需要执行的查询。

答案 3 :(得分:0)

  1. 可以将存储库注入域中。 存储库应具有简单的接口,以便域对象可以将其用作简单的集合或存储。存储库的主要思想是在简单明了的界面下隐藏数据访问。

  2. 我从usecase调用域服务时没有看到任何问题。 Usecase被认为是archestrator。域服务就是行动。用usecase触发域操作很好(甚至是不可避免的)。

  3. 要做出决定,您应该分析此限制来自何处?

    是商业规则吗?或者用户根本不应该成为模型的一部分? Usualy“用户”是指授权和认证,即行为,我认为应该放在用例中。我愿意为域(例如买方)创建单独的实体,并将其与usecase的用户联系起来。因此,当新用户注册时,可以触发新买家的创建。

相关问题