4

我正在尝试为我的大型 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 应该能够将它全部隔离。

提前致谢!

4

8 回答 8

4

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

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

为了将它们全部解耦,我最终使用了所谓的接口层 st

  • 表示层(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);
   }

   ...
}

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

于 2010-02-02T20:42:00.373 回答
2

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

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

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

于 2010-02-02T20:58:09.807 回答
1

这就是我这样做的方式:

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

Interface Item {
    bool Validate();
}

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

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

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

于 2010-02-02T20:43:48.573 回答
0

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

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

于 2010-02-02T20:56:14.867 回答
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 美元。

于 2010-02-02T21:31:24.880 回答
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 询问 aValidatorFor<Product>和 call 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 询问所有此类实现并循环运行它们。

于 2010-02-02T21:38:16.063 回答
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!");
于 2010-02-02T22:05:10.697 回答
0

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

IEnumerable<IIssue> Validate(T entity)
{
    if(entity.Id == null)
      yield return new Issue("error message");
}
于 2011-08-28T20:00:34.203 回答