领域服务与应用服务的区别

18-11-09 banq
         

在这篇文章中,我们将看一下领域域服务与应用服务有什么不同。

人们常说,领域服务是承载那些不自然地适合实体和值对象的领域知识。但是,还有另一个原因可能需要引入域服务。这个原因与领域模型隔离有关。

那么,领域服务与应用服务有何不同?这两个概念都假设无状态类可以在领域实体和值对象之上工作,但这几乎与它们的相似性有关。它们之间的主要区别在于领域服务包含领域逻辑而应用服务不包含领域逻辑。

领域逻辑是与业务策略相关的所有内容。因此,领域服务以与实体和值对象相同的方式参与策略实施过程(业务规则)。而应用服务是实现由实体和值对象所做出的编排方式编排。

我们来看这个例子:

public void WithdrawMoney(decimal amount)
{
    _atm.DispenseMoney(amount);
    decimal amountWithCommission =_atm.CalculateAmountWithCommission(amount);
 
   _paymentGateway.ChargePayment(amountWithCommission);
    _repository.Save(_atm);
}

方法withdrawMoney方法是应用服务的一部分,并且包括一个面向客户的API。它告诉ATM实体首先分配一些金额,然后要求它用佣金进行金额计算,将计算结果通过支付网关收费,最后将实体保存到数据库。

此应用程序服务看起来不错,还是应该将一些代码从其中提炼出来放到领域服务中呢?让我们来看看。

在前两行中,该方法使用Atm领域实体做出一些业务决策。在最后两行中,它将这些决定转换为可见的副作用:调用支付网关并修改数据库的状态。

这里的前两行是关于将决策过程委托给领域模型。是否需要将前两行有关领域知识的代码提炼到领领域服务中?例如,像这样:

public void WithdrawMoney(decimal amount)
{
    decimal amountWithCommission =_atmService.DispenseAndCalculateCommission(
        _atm, amount);
 
   _paymentGateway.ChargePayment(amountWithCommission);
    _repository.Save(_atm);
}
public sealed class AtmService // Domain service
{
    public decimal DispenseAndCalculateCommission(Atmatm, decimal amount)
    {
        atm.DispenseMoney(amount);
        returnatm.CalculateAmountWithCommission(amount);
    }
}

不能这样做,使用多个与领域模型相关的代码行这一事实并不构成领域知识。重要的是这些代码行是否负责做出业务决策。

在上面的示例中,DispenseAndCalculateCommission方法的圈复杂度为1(一种代码复杂度的衡量标准),没有类似if语句导致的分支以表示代码需要做出任何判断(这就代表业务决策),它只是要求领域实体做两件事。

而且代表做这两件事的两种方法的调用顺序也无关紧要。如果我们要重新安排它们的调用顺序,将佣金计算查询放到分配货币命令之前(这代表逻辑顺序变化,说明两者不是互为因果的逻辑关系,不是逻辑就不是领域知识核心),那么在Atm的不变量方面没有任何改变,它仍将保持有效。这是一个强有力的迹象,也没有泄漏的实施细节。

现在,让我们稍微更改代码示例,验证一下:

public void WithdrawMoney(decimal amount)
{
    if (!_atm.CanDispenseMoney(amount))
        return;
 
    _atm.DispenseMoney(amount);
    decimal amountWithCommission =_atm.CalculateAmountWithCommission(amount);
 
   _paymentGateway.ChargePayment(amountWithCommission);
    _repository.Save(_atm);
}

此示例中的圈复杂度高于1,因为我们现在有一个“if”语句。这不是说明应用服务现在已经包含领域知识吗?

不,实际的决策过程仍然存在于Atm。由实体单独决定是否可以分配任何资金。应用程序服务只是协调该决定,并继续执行或不执行。

只要Atm中的DispenseMoney方法有一个先决条件,说明CanDispenseMoney在分配现金之前必须保持为真,所有不变量都会受到保护。前提条件本身可以像这样简单地实现:

public void DispenseMoney(decimal amount)
{
    if (!CanDispenseMoney(amount))
        throw new InvalidOperationException();
 
    /* … */
}

因此,即使应用服务忽略CanDispenseMoney做出的决定,Atm实体也不会进入不一致状态。它将抛出一个例外。

何时提取到领域服务?

上面示例中的应用服务不做出任何业务决策,它将这些决策委托给领域模型。请注意,领域模型是隔离的:Atm实体不会将自身保存到数据库,也不会通过支付网关直接收费。我们在这里有一个很好的关注点分离:业务逻辑放入领域模型,而与外部世界的交互 - 应该放入应用服务。

您可以注意到大多数遵循此指南的代码库中的模式。他们的执行流程如下:

  • 准备业务操作所需的所有信息:从数据库加载参与实体并从其他外部源检索所需的所有数据。
  • 执行操作。该操作由领域模型做出的一个或多个业务决策组成。这些决定导致更改模型的状态,或生成一些结果(上面示例中的amountWithCommission值)。
  • 将操作结果应用于外界。

只有第1步和第3步涉及与外部依赖关系的工作。在第一步中检索到数据下接着实现第二步:接受的入参数及其生成的输出仅包含实体,值对象和基本类型。

请注意,在简单的CRUD应用程序中,没有第二步,因为没有做出决定的地方。在这种情况下,所有操作都可以仅由应用服务执行,无需将它们委托给领域模型。实际上,不存在任何富领域模型。在这种情况下,贫血领域模型也可以正常工作。

现在,让我们将代码示例修改为更现实的场景。假设由于余额不足而导致支付费用失败,如果发生这种情况,我们不应该分配任何现金。代码:

public void WithdrawMoney(decimal amount)
{
    if (!_atm.CanDispenseMoney(amount))
        return;
 
    decimal amountWithCommission =_atm.CalculateAmountWithCommission(amount);
    Result result =_paymentGateway.ChargePayment(amountWithCommission);
 
    if (result.IsFailure)
        return;
 
    _atm.DispenseMoney(amount);
    _repository.Save(_atm);
}

在这个版本中,我们引入的第二个“if”语句确实代表了领域逻辑。它决定我们是否会向用户分发现金。但是,与第一个条件运算符不同,不是Atm实体做出该决定。这是应用服务本身。

即使付款在此之前失败,现在也可以从Atm获取现金,领域实体不为我们保证此不变性了,并且在不违反实体隔离的情况下引入这样的不变性是不可能的,因为为了检查这个前提条件,它必须调用第三方服务。

那么,在这种情况下该怎么办?这是领域服务可以提供帮助的地方。你可以将任何需要来自外部世界的额外信息的业务决策放入领域服务,并且因为以下原因而无法放入实体和值对象中实现:

public void WithdrawMoney(decimal amount)
{
    Atm atm = _repository.Get();
    _atmService.WithdrawMoney(atm, amount);
    _repository.Save(_atm);
}
public sealed class AtmService // Domain service
{
    public void WithdrawMoney(Atm atm, decimal amount)
    {
        if (!atm.CanDispenseMoney(amount))
            return;
 
        decimal amountWithCommission = atm.CalculateAmountWithCommission(amount);
        Result result =_paymentGateway.ChargePayment(amountWithCommission);
 
        if (result.IsFailure)
            return;
 
        atm.DispenseMoney(amount);
    }
}

这里的领域服务是周边细节与复杂性/业务逻辑之间的中间地带。一方面,我们无法完全隔离此服务,因为它必须与支付网关一起工作才能完成其工作。另一方面,我们并没有将太多的领域逻辑放入其中,只有关于如何兑换现金以获得信贷的知识。

我们仍然将尽可能多的逻辑放入实体中。例如,分配现金的行为仍然是Atm的责任。我们仍然尝试尽可能地隔离域服务。例如,我们不会使它与存储库一起工作,因为它不需要做出业务决策。引入服务的杂质和领域逻辑在这里是最低限度的。足以让它正常工作。

这种提取可能看起来有问题。这不仅仅是责任的转移而没有实际的好处吗?但是,有一些好处。领域服务中的代码比应用程序服务中的代码更易于测试。它具有较少的外部依赖性,因此我们需要使用较少的测试双精度来对其进行单元测试。这项服务当然不像实体那样可测试,但仍然如此。第二个好处是,这样我们可以防止领域知识泄漏,并将所有领域逻辑保留在领域模型边界内,这可能有助于提高可读性。

我不会说在这个特定的例子中,这两个好处起了很大作用。在应用服务本身中保留一些不适合实体的逻辑并且不会每次都引入单独的领域服务,这是非常好的。但是,请确保此逻辑不会重复,并且不太复杂。如果您发现违反DRY原则或应用服务变得过于复杂,您需要明确引入领域服务。

将领域服务注入实体

我有时会听到的一个问题是:领域服务是否可以注入实体?

我个人区分两种类型的域服务:纯(隔离)和不纯(非隔离)。前者纯的意思表示实体和价值对象是闭合的,不依赖于外部世界。而AtmService是一个不纯的域服务的示例。

向实体注入不纯的领域服务会破坏隔离,因此我建议不要这样使用。另一方面,纯领域服务不会造成任何伤害,因此从您的实体和值对象中引用它们是完全正确的。

总结

  • 领域服务带有领域知识; 应用服务没有(理想情况下)。
  • 领域服务包含不自然地适合放入实体和值对象的领域逻辑。
  • 当你发现某些逻辑无法放入实体/值对象时,引入领域服务,否则会破坏它们与外界的隔离性。

         

1