将业务层与数据层分离时对实体进行限制的位置

发布于 2024-08-20 04:58:12 字数 1102 浏览 1 评论 0原文

我正在尝试为我的大型 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 应该能够隔离所有这些。

提前致谢!

I am attempting to create the the business and data layers for my big ASP.NET MVC application. As this is the first time for me attempting a project of this scale I am reading some books and trying to take good care at separating things out properly. Usually my applications mix the business logic and data access layers, and multiple business entities are intertwined in the single class (which has confused me a few times when I was trying to figure out where to add things).

Most of what I have been reading is to separate out the business and data layers. This seems all fine and dandy, but I am having trouble visualizing exactly how to do this in some scenarios. For example, let's say I am creating a system that allows admins to add a new product to the system:

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

Then I separate out the data access by creating a repository

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

Let's say I want to require a product's name to have at least 4 characters. I can't see how to do this cleanly.

One idea I had was to expand the Name's set property and only set it if it's 4 characters long. However, there is no way for a method that is creating the product to know the name didn't get set except that Product.Name != whatever they passed in.

Another idea I had is to put it in the Add() method in the repository, but then I have my business logic right there with the data logic, which also means if the Add call fails I don't know if it failed for the business logic or because the DAL failed (and it also means I can't test it using mock frameworks).

The only thing I can think of is to put my DAL stuff in a 3rd layer that gets called from the Add() method in the repository, but I don't see this in any of the domain modelling examples in my book or on the web (that I've seen at least). It also adds to the complexity of the domain models when I am not sure it is needed.

Another example is wanting to make sure that a Name is only used by one product. Would this go in the Product class, ProductRepository Add() method, or where?

As a side note, I plan to use NHibernate as my ORM however, to accomplish what I want it (theoretically) shouldn't matter what ORM I am using since TDD should be able to isolate it all.

Thanks in advance!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

回首观望 2024-08-27 04:58:12

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

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

为了解耦所有这些,我使用了- 称为界面层 最后我有

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

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

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

使用NHibernate,...无论什么ORM
通过接口将 DAO 层与其他层完全分离,您可以使用背后的任何技术来访问底层数据库。您可以根据需要直接发出 SQL 查询或使用 NHibernate。好处是它完全独立于应用程序的其余部分。您可以从今天开始手动编写 SQL,明天将您的 DAO dll 与使用 NHibernate 的 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(...) 方法中的验证逻辑。

I usually approach this by using a layered architecture. How to do this? You basically have the following (ideally) VS projects:

  • Presentation layer (where the UI stuff resides)
  • Business layer (where the actual business logic resides)
  • Data access layer (where you communicate with your underlying DBMS)

For decoupling all of them I use so-called interface layers s.t. in the end I have

  • Presentation layer (where the UI
    stuff resides)
  • IBusiness layer (containing the interfaces for the
    business layer)
  • Business layer (where
    the actual business logic resides)
  • IDataAccess layer (containing the
    interfaces for the DAO layer)
  • Data access layer (where you communicate
    with your underlying DBMS)

This is extremely handy and creates a nicely decoupled architecture. Basically your presentation layer just accesses the interfaces and not the implementations itself. For creating the according instances you should use a Factory or preferably some dependency injection library (Unity is good for .Net apps or alternatively Spring.Net).

How does this impact on your business logic / testability of your app?
It is probably too long to write everything in detail, but if you're concerned about having a well testable design you should absolutely consider dependency injection libraries.

Using NHibernate,...whatever ORM
Having a DAO layer completely separated through the interfaces from the other layers you can use whatever technology behind for accessing your underlying DB. You could directly issue SQL queries or use NHibernate, as you wish. The nice thing is that it is totally independent from the rest of your app. You could event start today by writing SQLs manually and tomorrow exchange your DAO dll with one that uses NHibernate without a single change in your BL or presentation layer.
Moreover testing your BL logic is simple. You may have a class like:

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);
   }

   ...
}

Now you can easily test the validation logic in your SaveProduct(...) method by mocking out the ProductDao in your test case.

捎一片雪花 2024-08-27 04:58:12

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

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

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

Put things like the product name restriction in the domain object, Product, unless you want to allow products with fewer than 4 characters in some scenarios (in this case, you'd apply the 4-character rule at the level of the controller and/or client-side). Remember, your domain objects may be reused by other controllers, actions, internal methods, or even other applications if you share the library. Your validation should be appropriate to the abstraction you are modeling, regardless of application or use case.

Since you are using ASP .NET MVC, you should take advantage of the rich and highly extensible validation APIs included in the framework (search with keywords IDataErrorInfo MVC Validation Application Block DataAnnotations for more). There are lots of ways for the calling method to know that your domain object rejected an argument -- for example, throwing the ArgumentOutOfRangeException.

For the example of ensuring that product names are unique, you would absolutely not put that in Product class, because this requires knowledge of all other Products. This logically belongs at the persistence layer and optionally, the repository. Depending on your use case may warrant a separate service method that verifies that the name does not already exist, but you shouldn't assume that it will still be unique when you later try to persist it (it has to be checked again, because if you validate uniqueness and then keep it around a while longer before persisting, someone else could still persist a record with the same name).

九厘米的零° 2024-08-27 04:58:12

我就是这样做的:

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

Interface Item {
    bool Validate();
}

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

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

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

This is the way I do it:

I keep the validation code in the entity class, which inherits some general Item Interface.

Interface Item {
    bool Validate();
}

Then, in the repository's CRUD functions i call the appropriate Validate function.

This way all the logic paths are validating my values, but i need to look only in one place to see what that validation really is.

Plus, sometimes you use the entities outside the repository scope, for example in a View. So if the validation is separated, each action path can test for validation without asking the repository.

荒人说梦 2024-08-27 04:58:12

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

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

For restrictions I utilize the partial classes on the DAL and implement the data annotation validators. Quite often, that involves creating custom validators but that works great as it's completely flexible. I've been able to create very complex dependent validations that even hit the database as part of their validity checks.

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

习ぎ惯性依靠 2024-08-27 04:58:12

为了与 SRP(单一职责原则)保持一致,如果验证是与产品的逻辑分开。由于它是数据完整性所必需的,因此它可能应该更接近存储库 - 您只想确保验证始终运行而无需考虑。

在这种情况下,您可能有一个通用接口(例如 IValidationProvider),它通过 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 美元。

In keeping with the SRP (single responsibility principle), you might be better served if the validation is separate from the product's domain logic. Since it's required for data integrity, it should probably be closer to the repository - you just want to be sure that validation is always run without having to give it thought.

In this case you might have a generic interface (e.g. IValidationProvider<T>) that is wired to a concrete implementation through an IoC container or whatever your preference may be.

public abstract Repository<T> {

  IValidationProvider<T> _validationProvider;    

  public ValidationResult Validate( T entity ) {

     return _validationProvider.Validate( entity );
  }

}  

This way you can test your validation separately.

Your repository might look like this:

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;
   }
}

You could go a step further, if you intend on exposing this functionality via an external API, and add a service layer to mediate between the domain objects and the data access. In this case, you move the validation to the service layer and delegate data access to the repository. You may have, IProductService.Add( p ). But this can become a pain to maintain due to all of the thin layers.

My $0.02.

错々过的事 2024-08-27 04:58:12

通过松散耦合实现此目的的另一种方法是为您的实体类型创建验证器类,并将它们注册到您的 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< /code> 并调用 IsValid()

但是,当您需要更改验证逻辑时会发生什么?那么,您可以创建一个新的 ValidatorFor实现,并将其注册到 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实现,然后向 IoC 请求所有此类实现并循环运行它们。

Another way to accomplish this with loose coupling would be to create validator classes for your entity types, and register them in your IoC, like so:

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
    }
}

Now when it comes time to validate, ask your IoC for a ValidatorFor<Product> and call IsValid().

What happens when you need to change the validation logic, though? Well, you can create a new implementation of ValidatorFor<Product>, and register that in your IoC instead of the old one. If you are adding another criterion, however, you can use a decorator:

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;
    }
}

Update your IoC configuration and you now have a minimum and maximum length validation without opening up any classes for modification. You can chain an arbitrary number of decorators in this way.

Alternatively, you can create many ValidatorFor<Product> implementations for the various properties, and then ask the IoC for all such implementations and run them in a loop.

旧街凉风 2024-08-27 04:58:12

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

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

Alright, here is my third answer, because there are so very many ways to skin this cat:

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;
    }
}

With this implementation, you are able to add an arbitrary number of validators to the object without hardcoding the logic into the domain entity. You really need to be using IoC or at least a basic factory for this to make sense, though.

Usage is like:

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!");
莳間冲淡了誓言ζ 2024-08-27 04:58:12

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

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

U can use a other validation system. you can add a method to IService in service layer such as:

IEnumerable<IIssue> Validate(T entity)
{
    if(entity.Id == null)
      yield return new Issue("error message");
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文