【问题标题】:In domain-driven design, would it be a violation of DDD to put calls to other objects' repostiories in a domain object?在域驱动设计中,将调用其他对象的存储库放在域对象中会违反 DDD 吗?
【发布时间】:2009-02-27 19:35:34
【问题描述】:

我目前正在重构一个即将结束的项目的一些代码,我最终将大量业务逻辑放入服务类而不是域对象中。此时,大多数域对象只是数据容器。我决定在服务对象中编写大部分业务逻辑,然后将所有内容重构为更好、更可重用和更易读的形状。这样我就可以决定哪些代码应该放在域对象中,哪些代码应该分拆到它们自己的新对象中,以及哪些代码应该留在服务类中。所以我有一些代码:

public decimal CaculateBatchTotal(VendorApplicationBatch batch)
{
     IList<VendorApplication> applications = AppRepo.GetByBatchId(batch.Id);

     if (applications == null || applications.Count == 0)
          throw new ArgumentException("There were no applications for this batch, that shouldn't be possible");
     decimal total = 0m;
     foreach (VendorApplication app in applications)
          total += app.Amount;
     return total;
}

这段代码似乎可以很好地添加到域对象中,因为它唯一的输入参数是域对象本身。似乎是一些重构的完美候选者。但唯一的问题是这个对象调用了另一个对象的存储库。这让我想把它留在服务课上。

我的问题是这样的:

  1. 你会把这段代码放在哪里?
  2. 你会分解这个函数吗?
  3. 遵循严格的领域驱动设计的人会把它放在哪里?
  4. 为什么?

感谢您的宝贵时间。

编辑说明:不能在这个上使用 ORM,所以我不能使用延迟加载解决方案。

编辑注 2:我无法更改构造函数以接收参数,因为可能的数据层如何使用反射来实例化域对象(不是我的想法)。

编辑注3:我不认为批处理对象应该能够汇总任何应用程序列表,似乎它应该只能汇总该特定批次中的应用程序。否则,将函数留在服务类中对我来说更有意义。

【问题讨论】:

  • Pascal 大小写的局部变量。糟糕。

标签: c# design-patterns service domain-driven-design


【解决方案1】:

您甚至不应该从域对象访问存储库。

您可以做的是让服务为域对象提供适当的信息,或者在由服务或构造函数设置的域对象中拥有一个委托。

public DomainObject(delegate getApplicationsByBatchID)
{
    ...
}

【讨论】:

  • 同意,因此对于这种情况,我将完成需要在服务类中调用存储库的工作,然后在需要计算时将集合或单个对象传递给实体
  • 是的,域对象将使用它的委托来获取它需要的任何数据,委托将由创建域对象的任何内容设置。委托可以是服务方法或存储库方法。
  • 这个答案很好,但我想知道你对耦合影响的看法。当然,我可以在那里传递任何类型的委托函数,但它确实将域对象与数据检索结合起来。这是个坏主意吗?
  • 另外,我不能使用构造函数来传递委托,因为当前系统如何依赖反射来实例化对象。不是我的设计,但作为专业人士的一部分是处理其他人的错误约定
  • 是的,如果对对象有意义,可以将域对象与数据检索结合起来。无论哪种方式,它都需要检索数据吗?
【解决方案2】:

我不是 DDD 方面的专家,但我记得伟大的 Jeremy Miller 的一篇文章为我回答了这个问题。您通常需要与域对象相关的逻辑 - 在这些对象中,但您的服务类将执行包含此逻辑的方法。这帮助我将特定领域的逻辑推送到实体类中,并让我的服务类不那么笨重(因为我发现自己像你提到的那样在服务类中放入了很多逻辑)

编辑:示例

我使用企业库进行简单的验证,所以在实体类中我会设置一个属性如下:

 [StringLengthValidator(1, 100)]
 public string Username {
     get { return mUsername; }
     set { mUsername = value; }
 }

实体继承自具有以下“IsValid”方法的基类,该方法将确保每个对象都符合验证条件

     public bool IsValid()
     {
         mResults = new ValidationResults();
         Validate(mResults);

         return mResults.IsValid();
     }

     [SelfValidation()]
     public virtual void Validate(ValidationResults results)
     {
         if (!object.ReferenceEquals(this.GetType(), typeof(BusinessBase<T>))) {
             Validator validator = ValidationFactory.CreateValidator(this.GetType());
             results.AddAllResults(validator.Validate(this));
         }
         //before we return the bool value, if we have any validation results map them into the
         //broken rules property so the parent class can display them to the end user
         if (!results.IsValid()) {
             mBrokenRules = new List<BrokenRule>();
             foreach (Microsoft.Practices.EnterpriseLibrary.Validation.ValidationResult result in results) {
                 mRule = new BrokenRule();
                 mRule.Message = result.Message;
                 mRule.PropertyName = result.Key.ToString();
                 mBrokenRules.Add(mRule);
             }
         }
     }

接下来我们需要在服务类的save方法中执行这个“IsValid”方法,像这样:

 public void SaveUser(User UserObject)
 {
     if (UserObject.IsValid()) {
         mRepository.SaveUser(UserObject);
     }
 }

一个更复杂的例子可能是银行账户。存款逻辑将存在于帐户对象中,但服务类将调用此方法。

【讨论】:

    【解决方案3】:

    为什么不传入 IList&lt;VendorApplication&gt; 作为参数而不是 VendorApplicationBatch?对此的调用代码可能来自可以访问 AppRepo 的服务。这样一来,您的存储库访问权限将在其所属的位置进行,而您的域功能可以保持对数据的来源一无所知。

    【讨论】:

    • 如果我将总计一个批次的代码放入存储库中,而不是 getbybatchid,则无法保证传入的应用程序来自该批次。我觉得批处理应该只能汇总它包含的应用程序。
    • 在存储库中总计一个批次 -> 在批处理域对象中总计一个批次
    • 我不确定我是否理解。我的建议是保持代码原样,除了传递应用程序列表而不是传递 VendorApplicationBatch 之外。 GetByBatchID 仍在存储库中,总计的代码仍在域中。
    • public decimal CalculateBatchTotal(IList applications)
    • 好吧,我正在尝试决定是否应该将此函数放在 VendorBatchApplication 对象中。似乎一个批次应该只能汇总其中的应用程序。
    【解决方案4】:

    据我了解(没有足够的信息知道这是否是正确的设计)VendorApplicationBatch 应该在域对象内包含一个延迟加载的 IList,并且逻辑应该保留在域中。

    例如(航空代码):

    public class VendorApplicationBatch  {
    
        private IList<VendorApplication> Applications {get; set;};   
    
        public decimal CaculateBatchTotal()
        {
            if (Applications == null || Applications.Count == 0)
                throw new ArgumentException("There were no applications for this batch, that shouldn't be possible");
    
            decimal Total = 0m;
            foreach (VendorApplication App in Applications)
                Total += App.Amount;
           return Total;
        }
    }
    

    使用像 NHibernate 这样的 ORM 很容易做到这一点,我认为这将是最好的解决方案。

    【讨论】:

    • 我同意你的观点,这是最好的解决方案。不幸的是,当决定如何设计数据层(或缺少数据层)时,我不在身边,所以我不能使用 ORM,我在它的位置做了一个非常棒的存储库系统。
    • 那么答案是让服务获取 VendorApplication 列表并且 Batch 对象应该执行逻辑。例如:VendorApplicationBatch.CaculateBatchTotal(IList applications)
    • 我认为允许一个批次总计可能属于另一批次的应用程序是不正确的,必须有某种方法来保证一个批次只能总计它自己的供应商应用程序。因此,我不能真诚地将此功能放入批处理中。
    • VendorApplication 知道它属于哪个批次吗?你可以检查一下。或者,您可以从一开始就使用所有 VendorApplications 加载 Batch。不然就留在服务里,不能懒加载就很难做DDD了。
    • 是的,供应商应用程序有一个批次 ID,我想我可以检查一下。也许你说得对,这似乎有点不合时宜,但当然很多问题都没有完美的解决方案。
    【解决方案5】:

    在我看来,您的 CalculateTotal 是针对 VendorApplication 集合的服务,并且为 Batch 返回 VendorApplication 集合自然适合作为 Batch 类的属性。因此,其他一些服务/控制器/任何会从批处理中检索适当的 VendorApplication 集合并将它们传递给 VendorApplicationTotalCalculator 服务(或类似的东西)。但这可能会破坏一些 DDD 聚合根服务规则或一些我不知道的东西(DDD 新手)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-03
      • 2011-05-26
      • 2020-05-04
      • 1970-01-01
      相关资源
      最近更新 更多