架构 - 域驱动设计与严格的业务逻辑实施

时间:2010-01-13 14:03:05

标签: unit-testing architecture domain-driven-design

我的业务对象使用以下架构编码:

  • 如果不符合业务逻辑,任何传入数据的验证都会在setter中引发异常。
    • 属性不能是损坏/不一致的状态,除非现有的默认值/ null无效
  • 业务对象只能由业务模块通过静态工厂类型方法创建,该方法接受与业务对象共享的接口实现,以便复制到业务对象中。
    • 强制依赖容器,ui和持久层不能创建无效的Model对象或将其传递到任何地方。
  • 此工厂方法捕获验证字典中的所有不同验证异常,以便在验证尝试完成时,调用者提供的字典填充字段名称和消息,如果任何验证没有,则抛出异常通过。
    • 使用相应的错误消息轻松地回显到UI字段
  • 业务对象上没有数据库/持久性类型方法
  • 所需的持久性行为是通过业务模块中的存储库接口定义的。

示例业务对象接口:

public interface IAmARegistration
{
  string Nbk { get; set; } //Primary key?
  string Name { get; set; }
  string Email { get; set; }
  string MailCode { get; set; }
  string TelephoneNumber { get; set; }
  int? OrganizationId { get; set; }
  int? OrganizationSponsorId { get; set; }
 }

业务对象存储库接口:

 /// <summary>
 /// Handles registration persistance or an in-memory repository for testing
 /// requires a business object instead of interface type to enforce validation
 /// </summary>
 public interface IAmARegistrationRepository
 {
  /// <summary>
  /// Checks if a registration record exists in the persistance mechanism
  /// </summary>
  /// <param name="user">Takes a bare NBK</param>
  /// <returns></returns>
   bool IsRegistered(string user); //Cache the result if so

  /// <summary>
  /// Returns null if none exist
  /// </summary>
  /// <param name="user">Takes a bare NBK</param>
  /// <returns></returns>
   IAmARegistration GetRegistration(string user);

   void EditRegistration(string user,ModelRegistration registration);

   void CreateRegistration(ModelRegistration registration);
  }

然后,实际的业务对象如下所示:

public class ModelRegistration : IAmARegistration//,IDataErrorInfo
{
    internal ModelRegistration() { }
    public string Nbk
    {
        get
        {
            return _nbk;
        }
        set
        {
            if (String.IsNullOrEmpty(value))
                throw new ArgumentException("Nbk is required");
            _nbk = value;
        }
    }
    ... //other properties omitted
    public static ModelRegistration CreateModelAssessment(IValidationDictionary validation, IAmARegistration source)
    {

        var result = CopyData(() => new ModelRegistration(), source, false, null);
        //Any other complex validation goes here
        return result;
    }

    /// <summary>
    /// This is validated in a unit test to ensure accuracy and that it is not out of sync with 
    /// the number of members the interface has
    /// </summary>
    public static Dictionary<string, Action> GenerateActionDictionary<T>(T dest, IAmARegistration source, bool includeIdentifier)
where T : IAmARegistration
    {
        var result = new Dictionary<string, Action>
            {
                {Member.Name<IAmARegistration>(x=>x.Email),
                    ()=>dest.Email=source.Email},
                {Member.Name<IAmARegistration>(x=>x.MailCode),
                    ()=>dest.MailCode=source.MailCode},
                {Member.Name<IAmARegistration>(x=>x.Name),
                    ()=>dest.Name=source.Name},
                {Member.Name<IAmARegistration>(x=>x.Nbk),
                    ()=>dest.Nbk=source.Nbk},
                {Member.Name<IAmARegistration>(x=>x.OrganizationId),
                    ()=>dest.OrganizationId=source.OrganizationId},
                {Member.Name<IAmARegistration>(x=>x.OrganizationSponsorId),
                    ()=>dest.OrganizationSponsorId=source.OrganizationSponsorId},
                {Member.Name<IAmARegistration>(x=>x.TelephoneNumber),
                    ()=>dest.TelephoneNumber=source.TelephoneNumber}, 

            };

        return result;

    }
    /// <summary>
    /// Designed for copying the model to the db persistence object or ui display object
    /// </summary>
    public static T CopyData<T>(Func<T> creator, IAmARegistration source, bool includeIdentifier,
        ICollection<string> excludeList) where T : IAmARegistration
    {
        return CopyDictionary<T, IAmARegistration>.CopyData(
            GenerateActionDictionary, creator, source, includeIdentifier, excludeList);
    }

    /// <summary>
    /// Designed for copying the ui to the model 
    /// </summary>
    public static T CopyData<T>(IValidationDictionary validation, Func<T> creator,
        IAmARegistration source, bool includeIdentifier, ICollection<string> excludeList)
         where T : IAmARegistration
    {
        return CopyDictionary<T, IAmARegistration>.CopyData(
            GenerateActionDictionary, validation, creator, source, includeIdentifier, excludeList);
    }

我在编写隔离测试时遇到问题的示例存储库方法:

    public void CreateRegistration(ModelRegistration registration)
    {
        var dbRegistration = ModelRegistration.CopyData(()=>new Registration(), registration, false, null);

       using (var dc=new LQDev202DataContext())
       {
           dc.Registrations.InsertOnSubmit(dbRegistration);
           dc.SubmitChanges();
       }
    }

的问题:

  • 添加新成员时,必须至少更改8个地方(db,linq-to-sql designer,model Interface,model property,model copy dictionary,ui,ui DTO,unit test
  • 可测性
    • 测试硬编码的db方法依赖于没有公共默认构造函数的确切类型,并且需要通过另一个方法,使得单独测试变得不可能,或者需要介入业务对象才能生成它更容易测试。
    • 使用InternalsVisibleTo以便BusinessModel.Tests可以访问内部构造函数,但是我需要为任何其他持久层测试模块添加它,使其扩展性非常差
  • 要使复制功能通用,业务对象必须具有公共设置器
    • 我更喜欢模型对象是不可变的
  • 用户界面需要DTO才能尝试任何数据验证

我正在使用其他持久性机制和ui机制(Windows窗体,asp.net,asp.net mvc 1等)来完全重用此业务层。此外,团队成员可以轻松地针对此业务层/架构进行开发。

有没有办法强制执行不可变的经过验证的模型对象,或强制说ui或persistance层在没有这些麻烦的情况下都无法获得无效的模型对象?

2 个答案:

答案 0 :(得分:2)

这对我来说非常复杂。

IMO,域对象应该是保护其不变量的POCO。您不需要工厂来执行此操作:只需在构造函数中请求任何必要的值,并为其余值提供有效的默认值。

如果您拥有属性设置器,请通过调用您已经执行的验证方法来保护它们。简而言之,您绝不允许实例处于不存在的状态 - 工厂或没有工厂。

在C#中编写不可变对象很简单 - 只需确保使用readonly关键字声明所有字段。

但请注意,如果您关注Domain-Driven Design,则Domain对象往往属于三个存储桶

  • 实体。具有长期身份的可变对象
  • 价值对象。没有身份的不可变对象
  • 服务。无状态

根据这个定义,只有值对象应该是不可变的。

答案 1 :(得分:0)

我过去采用了不同的方法。我没有使用验证异常保护属性设置器,而是采用以下约定:A)所有域对象提供按需验证自身的方法(例如,验证方法),以及B)存储库断言持久性操作所在的所有对象的验证请求(例如,通过调用Validate方法并在失败时抛出异常)。这意味着使用存储库是信任存储库维护此约定

这在.NET世界中的优势至少是一个易于使用的域表面和更简洁和可读的代码(没有巨大的try-catch块或迂回异常处理机制)。这也与ASP.NET MVC验证方法很好地集成。

我会说在域对象暴露的非属性操作中(例如Demeter法对组合集合所暗示的那些),我倾向于返回一个提供即时验证反馈的ValidationResult。 Validate方法也返回此反馈。我这样做的原因是对消费代码的结构或可读性没有任何不利影响。如果调用者不想检查返回值,则他们不必;如果他们愿意,他们可以。在不影响消费代码结构的情况下(使用try-catch块),属性设置器无法实现这样的好处。同样,域服务返回ValidationResults。只有存储库抛出ValidationExceptions。

总的来说,这确实允许您将域对象置于无效状态,但它们与调用者的执行上下文(调用者的“工作空间”)隔离,并且无法持久保存到系统中。