7

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

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

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

此链接是作为一种可能的解决方案提供的,在哪里检查用户电子邮件尚未退出?. 它确实有帮助,但似乎并没有结束这个问题的循环。我似乎从本文中遗漏的事情是,谁将负责调用 CreateUserService、应用程序服务或聚合根上的方法,其中 CreateUserService 对象将与任何其他相关参数一起注入到方法中?

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

4

4 回答 4

4

我认为最适合这种行为的是域服务。DS 可以访问持久性,因此您可以检查是否存在或唯一性。检查此博客条目以获取更多信息。

IE:

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

于 2018-01-11T08:28:20.087 回答
1

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

1. 锁定、事务和数据库约束

锁定、事务和数据库约束是用于维护数据完整性的久经考验的工具,但它们是有代价的。通常代码/系统难以扩展,并且编写和维护起来很复杂。但它们的优势是可以通过大量示例来很好地理解。暗示,这种方法通常使用基于 CRUD 的操作来完成。如果您想保持使用事件溯源,那么您可以尝试混合方法。

2. 混合锁场

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

3. 依赖最终一致的读取模型

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

顺便说一句,你怎么知道一致的数据库是一致的?它不记录用户尝试执行的失败操作。如果我尝试更新自读取后已更新的表中的一行,那么我很可能会丢失该数据。这让 DBA 产生了数据一致性的错觉,但请尝试向被激怒的用户解释这一点!

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

4.重新审视领域模型

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

于 2018-01-11T08:47:04.917 回答
0
  1. 将存储库注入域很好。 存储库应该具有简单的接口,以便域对象可以将其用作简单的集合或存储。Repositories 的主要思想是将数据访问隐藏在简单明了的界面下。

  2. 我没有看到从用例调用域服务的任何问题。用例应该是archestrator。域服务是动作。按用例触发域操作是可以的(甚至是不可避免的)。

要决定,你应该分析这个限制来自哪里?

是商业规则吗?或者也许用户根本不应该成为模型的一部分? 通常,“用户”是指授权和身份验证,即行为,我认为应该放在用例中。我准备为域(例如买方)创建单独的实体并将其与用例的用户相关联。因此,当新用户注册时,可能会触发新买家的创建。

于 2018-01-12T14:53:44.943 回答
0

您似乎走对了路,我唯一没有得到的东西就是您的所作所为UserService.register
它应该将所有值注册为输入,验证它们(使用存储库检查电子邮件的存在),如果输入有效,则存储新用户。

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

当您将聚合存储在诸如键值存储之类的东西时会发生这种问题,您可以在其中仅使用聚合的 id 进行搜索,但是如果您使用的是允许使用实体字段进行搜索的 sql 数据库之类的东西,您可以用简单的查询做很多事情。
您唯一需要注意的是避免混合查询逻辑和命令逻辑,在您的示例中,您需要做的查找很容易,只是一个字段,结果是布尔值,有时它可能像时间操作一样更难,或查询跨越多个表聚合结果,在这些情况下,最好让您的(命令)服务使用(查询)服务,该服务提供简单的 api 来执行计算,例如:

interface UserReportingService {
    ComplexResult aComplexQuery(AComplexInput input);
}

您可以使用使用您的存储库的类来实现,或者直接在数据库(sql 或其他)上执行查询的实现。
不同之处在于,如果您使用您根据域对象“认为”的存储库,如果您直接编写您根据数据库抽象考虑的查询(在 sql 的情况下为表/集,在 mongo 的情况下为文档等..)。一个或另一个取决于您需要执行的查询。

于 2018-01-11T23:17:34.970 回答