【问题标题】:Where to put restrictions on entities when separating Business layer from Data Layer将业务层与数据层分离时在哪里对实体进行限制
【发布时间】:2010-02-02 20:35:43
【问题描述】:

我正在尝试为我的大型 ASP.NET MVC 应用程序创建业务层和数据层。因为这是我第一次尝试这种规模的项目,所以我正在阅读一些书籍,并努力小心地将事情正确地分开。通常我的应用程序混合了业务逻辑和数据访问层,多个业务实体交织在一个类中(当我试图弄清楚在哪里添加东西时,这让我有几次困惑)。

我一直在阅读的大部分内容是将业务层和数据层分开。这看起来一切都很好而且很花哨,但我无法准确地想象在某些情况下如何做到这一点。例如,假设我正在创建一个允许管理员向系统添加新产品的系统:

public class Product
{ 
   public int Id { get; private set; }
   public string Name { get; set; }
   public decimal Price { get; set; }
}

然后我通过创建一个存储库来分离数据访问

public class ProductRepository
{
   public bool Add(Product product);
}

假设我想要求产品名称至少包含 4 个字符。我看不出如何干净地做到这一点。

我的一个想法是扩展 Name 的 set 属性,并且仅在它的长度为 4 个字符时才设置它。但是,创建产品的方法无法知道名称没有被设置,除了 Product.Name != 无论他们传入什么。

我的另一个想法是把它放在存储库的 Add() 方法中,但是我的业务逻辑和数据逻辑就在那里,这也意味着如果 Add 调用失败我不知道是不是业务逻辑失败或因为 DAL 失败(这也意味着我无法使用模拟框架对其进行测试)。

我唯一能想到的是将我的 DAL 东西放在第三层,该层从存储库中的 Add() 方法调用,但我在书中的任何域建模示例中都没有看到这一点或在网络上(至少我见过)。当我不确定是否需要它时,它还会增加域模型的复杂性。

另一个示例是要确保名称仅由一个产品使用。这会放在 Product 类、ProductRepository Add() 方法中,还是放在哪里?

附带说明一下,我计划使用 NHibernate 作为我的 ORM,但是,为了完成我想要的(理论上)我使用什么 ORM 并不重要,因为 TDD 应该能够将它全部隔离。

提前致谢!

【问题讨论】:

  • 谢谢大家。一些好东西。我将使用 Juri 的 SoC 解决方案(即实体 -> 服务层 -> 数据层)并将其与 Jay 的第三个验证答案相结合(我喜欢该解决方案的灵活性和清洁度),然后将 AddValidation()暂时调用服务层,直到我为他们找到更好的位置。最后我还把我的 AddValidation 和 IsValid 调用放到了 BaseEntity 类中。

标签: business-logic separation-of-concerns data-access


【解决方案1】:

我通常通过使用分层架构来解决这个问题。这个怎么做?你基本上有以下(理想情况下)VS项目:

  • 表示层(UI 内容所在的位置)
  • 业务层(实际业务逻辑所在的位置)
  • 数据访问层(与底层 DBMS 通信的地方)

为了将它们全部解耦,我使用所谓的接口层 s.t.最后我有

  • 表示层(UI 东西驻留)
  • IBusiness 层(包含 业务层)
  • 业务层(其中 实际的业务逻辑驻留)
  • IDataAccess 层(包含 DAO 层的接口)
  • 数据访问层(通信的地方 与您的底层 DBMS)

这非常方便,并创建了一个很好的解耦架构。基本上,您的表示层只访问接口而不是实现本身。要创建相应的实例,您应该使用工厂或最好使用一些依赖注入库(Unity 适用于 .Net 应用程序或 Spring.Net)。

这对您应用的业务逻辑/可测试性有何影响?
详细编写所有内容可能太长了,但如果您担心设计的可测试性良好,您绝对应该考虑依赖注入库。

使用 NHibernate,...无论 ORM
通过接口将 DAO 层与其他层完全分离,您可以使用任何背后的技术来访问您的底层数据库。您可以根据需要直接发出 SQL 查询或使用 NHibernate。好消息是它完全独立于您的应用程序的其余部分。您可以通过手动编写 SQL 来开始今天的活动,明天将您的 DAO dll 与使用 NHibernate 的 DAO dll 交换,而无需在您的 BL 或表示层中进行任何更改。
此外,测试您的 BL 逻辑很简单。您可能有这样的课程:

public class ProductsBl : IProductsBL
{

   //this gets injected by some framework
   public IProductsDao ProductsDao { get; set; }

   public void SaveProduct(Product product)
   {
      //do validation against the product object and react appropriately
      ...

      //persist it down if valid
      ProductsDao.PersistProduct(product);
   }

   ...
}

现在,您可以通过在测试用例中模拟 ProductDao 来轻松测试 SaveProduct(...) 方法中的验证逻辑。

【讨论】:

  • 这是有道理的。我已经计划将 Castle Winsor 用于 IoC。谢谢。
  • 在您的示例中, Product 对象在哪里?另外,假设您要显示所有产品,难道这个 Product 类不需要在每个表示层、业务层和数据层中都可以访问吗?
  • @Stewart 抱歉,之前没有看到您的评论。 Product 对象位于一个单独的垂直“层”中,它跨越了上述所有层。这是因为您的 Product 对象是您的数据传输实体。
【解决方案2】:

在域对象Product 中放置产品名称限制等内容,除非您希望在某些情况下允许少于 4 个字符的产品(在这种情况下,您需要在级别应用 4 个字符的规则控制器和/或客户端)。请记住,如果您共享库,您的域对象可能会被其他控制器、操作、内部方法甚至其他应用程序重用。无论应用程序或用例如何,您的验证都应该适合您正在建模的抽象。

由于您使用的是 ASP .NET MVC,因此您应该利用框架中包含的丰富且高度可扩展的验证 API(使用关键字搜索 IDataErrorInfoMVCValidation Application BlockDataAnnotations 了解更多信息)。调用方法有很多方法可以知道你的域对象拒绝了一个参数——例如,抛出ArgumentOutOfRangeException

对于确保产品名称唯一的示例,您绝对将其放在Product 类中,因为这需要了解所有其他Products。这在逻辑上属于持久层和可选的存储库。根据您的用例,可能需要一个单独的服务方法来验证该名称不存在,但您不应该假设它仍然在您以后尝试持久化它时是唯一的(它有再次检查,因为如果您验证唯一性,然后在持久化之前将其保留一段时间,其他人仍然可以持久化具有相同名称的记录)。

【讨论】:

    【解决方案3】:

    这就是我的做法:

    我将验证码保留在实体类中,继承了一些通用的Item接口。

    Interface Item {
        bool Validate();
    }
    

    然后,在存储库的 CRUD 函数中,我调用相应的 Validate 函数。

    这样所有逻辑路径都在验证我的值,但我只需要在一个地方查看验证的真正含义。

    另外,有时您会使用存储库范围之外的实体,例如在视图中。因此,如果验证是分开的,则每个操作路径都可以在不询问存储库的情况下测试验证。

    【讨论】:

      【解决方案4】:

      对于限制,我使用 DAL 上的部分类并实现数据注释验证器。很多时候,这涉及创建自定义验证器,但由于它完全灵活,所以效果很好。我已经能够创建非常复杂的依赖验证,甚至可以将数据库作为其有效性检查的一部分。

      http://www.asp.net/(S(ywiyuluxr3qb2dfva1z5lgeg))/learn/mvc/tutorial-39-cs.aspx

      【讨论】:

        【解决方案5】:

        根据SRP(单一责任原则),如果验证与产品的 逻辑分开,您可能会得到更好的服务。由于它是数据完整性所必需的,因此它可能应该更靠近存储库 - 您只想确保始终运行验证而无需考虑。

        在这种情况下,您可能有一个通用接口(例如IValidationProvider<T>),它通过 IoC 容器或您的偏好连接到具体实现。

        public abstract Repository<T> {
        
          IValidationProvider<T> _validationProvider;    
        
          public ValidationResult Validate( T entity ) {
        
             return _validationProvider.Validate( entity );
          }
        
        }  
        

        这样您可以单独测试您的验证。

        您的存储库可能如下所示:

        public ProductRepository : Repository<Product> {
           // ...
           public RepositoryActionResult Add( Product p ) {
        
              var result = RepositoryResult.Success;
              if( Validate( p ) == ValidationResult.Success ) {
                 // Do add..
                 return RepositoryActionResult.Success;
              }
              return RepositoryActionResult.Failure;
           }
        }
        

        如果您打算通过外部 API 公开此功能,您可以更进一步,并添加一个服务层以在域对象和数据访问之间进行调解。在这种情况下,您将验证移至服务层并将数据访问委托给存储库。你可能有,IProductService.Add( p )。但是由于所有的薄层,这可能会变得难以维护。

        我的 0.02 美元。

        【讨论】:

          【解决方案6】:

          通过松耦合实现此目的的另一种方法是为您的实体类型创建验证器类,并将它们注册到您的 IoC 中,如下所示:

          public interface ValidatorFor<EntityType>
          {
              IEnumerable<IDataErrorInfo> errors { get; }
              bool IsValid(EntityType entity);
          }
          
          public class ProductValidator : ValidatorFor<Product>
          {
              List<IDataErrorInfo> _errors;
              public IEnumerable<IDataErrorInfo> errors 
              { 
                  get
                  {
                      foreach(IDataErrorInfo error in _errors)
                          yield return error;
                  }
              }
              void AddError(IDataErrorInfo error)
              {
                  _errors.Add(error);
              }
          
              public ProductValidator()
              {
                  _errors = new List<IDataErrorInfo>();
              }
          
              public bool IsValid(Product entity)
              {
                  // validate that the name is at least 4 characters;
                  // if so, return true;
                  // if not, add the error with AddError() and return false
              }
          }
          

          现在需要验证时,请向您的 IoC 索要 ValidatorFor&lt;Product&gt; 并致电 IsValid()

          但是,当您需要更改验证逻辑时会发生什么?好吧,您可以创建ValidatorFor&lt;Product&gt; 的新实现,并将其注册到您的 IoC 而不是旧的。但是,如果您要添加其他标准,则可以使用装饰器:

          public class ProductNameMaxLengthValidatorDecorator : ValidatorFor<Person>
          {
              List<IDataErrorInfo> _errors;
              public IEnumerable<IDataErrorInfo> errors 
              { 
                  get
                  {
                      foreach(IDataErrorInfo error in _errors)
                          yield return error;
                  }
              }
              void AddError(IDataErrorInfo error)
              {
                  if(!_errors.Contains(error)) _errors.Add(error);
              }
          
              ValidatorFor<Person> _inner;
          
              public ProductNameMaxLengthValidatorDecorator(ValidatorFor<Person> validator)
              {
                  _errors = new List<IDataErrorInfo>();
                  _inner = validator;
              }
          
              bool ExceedsMaxLength()
              {
                  // validate that the name doesn't exceed the max length;
                  // if it does, return false 
              }
          
              public bool IsValid(Product entity)
              {
                  var inner_is_valid = _inner.IsValid();
                  var inner_errors = _inner.errors;
                  if(inner_errors.Count() > 0)
                  {
                      foreach(var error in inner_errors) AddError(error);
                  }
          
                  bool this_is_valid = ExceedsMaxLength();
                  if(!this_is_valid)
                  {
                      // add the appropriate error using AddError()
                  }
          
                  return inner_is_valid && this_is_valid;
              }
          }
          

          更新您的 IoC 配置,您现在可以验证最小和最大长度,而无需打开任何类进行修改。您可以通过这种方式链接任意数量的装饰器。

          或者,您可以为各种属性创建许多 ValidatorFor&lt;Product&gt; 实现,然后向 IoC 询问所有此类实现并循环运行它们。

          【讨论】:

            【解决方案7】:

            好的,这是我的第三个答案,因为有很多方法可以给这只猫剥皮:

            public class Product
            {
                ... // normal Product stuff
            
                IList<Action<string, Predicate<StaffInfoViewModel>>> _validations;
            
                IList<string> _errors; // make sure to initialize
                IEnumerable<string> Errors { get; }
            
                public void AddValidation(Predicate<Product> test, string message)
                {
                    _validations.Add(
                        (message,test) => { if(!test(this)) _errors.Add(message); };
                }
            
                public bool IsValid()
                {
                    foreach(var validation in _validations)
                    {
                        validation();
                    }
            
                    return _errors.Count() == 0;
                }
            }
            

            使用此实现,您可以向对象添加任意数量的验证器,而无需将逻辑硬编码到域实体中。不过,您确实需要使用 IoC 或至少一个基本的工厂才能使其有意义。

            用法如下:

            var product = new Product();
            product.AddValidation(p => p.Name.Length >= 4 && p.Name.Length <=20, "Name must be between 4 and 20 characters.");
            product.AddValidation(p => !p.Name.Contains("widget"), "Name must not include the word 'widget'.");
            product.AddValidation(p => p.Price < 0, "Price must be nonnegative.");
            product.AddValidation(p => p.Price > 1, "This is a dollar store, for crying out loud!");
            

            【讨论】:

              【解决方案8】:

              您可以使用其他验证系统。您可以在服务层向 IService 添加一个方法,例如:

              IEnumerable<IIssue> Validate(T entity)
              {
                  if(entity.Id == null)
                    yield return new Issue("error message");
              }
              

              【讨论】:

                猜你喜欢
                • 2010-12-14
                • 2010-11-09
                • 1970-01-01
                • 2011-12-27
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-05-23
                • 2013-11-27
                相关资源
                最近更新 更多