在将业务层与数据层分离时,对实体施加限制的位置

时间:2010-02-02 20:35:44

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

我正在尝试为我的大型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()方法调用的第3层中,但我在本书的任何域建模示例中都没有看到这一点或者在网上(我至少看过)。当我不确定是否需要时,它还增加了域模型的复杂性。

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

作为旁注,我计划使用NHibernate作为我的ORM,然而,为了实现我想要的东西(理论上),无论我使用什么样的ORM,都应该无关紧要,因为TDD应该能够将它全部隔离开来。

提前致谢!

8 个答案:

答案 0 :(得分:4)

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

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

为了解耦所有这些,我使用所谓的接口层s.t.最后我有

  • 表示层(UI的位置) 东西居住)
  • IBusiness层(包含用于的接口) 业务层)
  • 业务层(其中 实际的业务逻辑驻留)
  • IDataAccess图层(包含 DAO层的接口)
  • 数据访问层(您进行通信的地方) 与您的基础DBMS)

这非常方便,可以创建一个很好的分离架构。基本上,您的表示层只访问接口而不是实现本身。要创建相应的实例,您应该使用Factory或最好使用一些依赖注入库(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(...)方法中的验证逻辑。

答案 1 :(得分:2)

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

由于您使用的是ASP .NET MVC,因此您应该利用框架中包含的丰富且高度可扩展的验证API(使用关键字IDataErrorInfo MVC Validation Application Block进行搜索{{1更多)。调用方法有很多方法可以知道您的域对象拒绝了参数 - 例如,抛出DataAnnotations

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

答案 2 :(得分:1)

这就是我这样做的方式:

我将验证代码保存在实体类中,该实体类继承了一些通用的Item Interface。

Interface Item {
    bool Validate();
}

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

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

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

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美元。

答案 5 :(得分:0)

使用松散耦合实现此目的的另一种方法是为您的实体类型创建验证器类,并在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<Product>并致电IsValid()

但是,当您需要更改验证逻辑时会发生什么?好吧,您可以创建ValidatorFor<Product>的新实现,并在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<Product>实现,然后向IoC询问所有此类实现并在循环中运行它们。

答案 6 :(得分:0)

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

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!");

答案 7 :(得分:0)

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

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