Где наложить ограничения на сущности при отделении бизнес-уровня от уровня данных - PullRequest
4 голосов
/ 02 февраля 2010

Я пытаюсь создать бизнес-уровни и уровни данных для моего большого приложения 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 символов. Я не вижу, как это сделать чисто.

Одна идея, которая у меня была, заключалась в том, чтобы расширить свойство set для Name и установить его, только если оно длиной 4 символа. Однако у метода, создающего продукт, нет способа узнать, что имя не задано, за исключением того, что Product.Name! = Что бы они ни передавали.

Еще одна идея, которая у меня была, - поместить ее в метод Add () в хранилище, но затем у меня есть бизнес-логика с логикой данных, которая также означает, что если вызов Add не удастся, я не знаю, если это не удалось из-за бизнес-логики или из-за сбоя DAL (и это также означает, что я не могу проверить его с помощью фиктивных сред).

Единственное, о чем я могу думать, - это поместить мои DAL-объекты в 3-й слой, который вызывается из метода Add () в репозитории, но я не вижу этого ни в одном из примеров моделирования предметной области в моей книге. или в Интернете (что я видел по крайней мере). Это также увеличивает сложность моделей предметной области, когда я не уверен, что это необходимо.

Другим примером является желание убедиться, что Имя используется только одним продуктом. Будет ли это в классе Product, методе AddRepository Add () или где?

В качестве дополнительного примечания я планирую использовать NHibernate в качестве своего ORM, однако для достижения того, чего я хочу (теоретически), не должно иметь значения, какой ORM я использую, поскольку TDD должен быть способен изолировать все это.

Заранее спасибо!

Ответы [ 8 ]

4 голосов
/ 02 февраля 2010

Обычно я подхожу к этому, используя многоуровневую архитектуру. Как это сделать? У вас в основном есть следующие (в идеале) VS проекты:

  • Уровень представления (где находится пользовательский интерфейс)
  • Бизнес-уровень (где находится актуальная бизнес-логика)
  • Уровень доступа к данным (где вы общаетесь со своей базовой СУБД)

Для их разделения я использую так называемые интерфейсные слои s.t. в конце концов, у меня есть

  • Уровень представления (где пользовательский интерфейс материал находится)
  • Уровень IBusiness (содержащий интерфейсы для бизнес уровень)
  • Бизнес уровень (где фактическая бизнес-логика находится)
  • слой IDataAccess (содержащий интерфейсы для уровня DAO)
  • Уровень доступа к данным (где вы общаетесь с вашей базовой СУБД)

Это очень удобно и создает красиво отделенную архитектуру. По сути, ваш уровень презентации просто обращается к интерфейсам, а не к самим реализациям. Для создания соответствующих экземпляров вы должны использовать Factory или, предпочтительно, некоторую библиотеку внедрения зависимостей ( Unity подходит для приложений .Net или, наоборот, Spring.Net).

Как это влияет на вашу бизнес-логику / тестируемость вашего приложения?
Вероятно, слишком долго писать все подробно, но если вы беспокоитесь о наличии хорошо тестируемого дизайна, вам следует обязательно рассмотреть библиотеки внедрения зависимостей.

Использование NHibernate, ... независимо от ORM
Имея уровень DAO, полностью отделенный через интерфейсы от других уровней, вы можете использовать любую технологию для доступа к вашей основной БД. Вы можете напрямую выдавать SQL-запросы или использовать NHibernate, как пожелаете. Приятно то, что он полностью независим от остальной части вашего приложения. Вы можете начать мероприятие сегодня, написав SQL вручную, а завтра обменяйте свою DAO dll на ту, которая использует NHibernate без единого изменения в вашем 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 в вашем тестовом примере.

2 голосов
/ 02 февраля 2010

Поместите такие вещи, как ограничение имени продукта в объекте домена, Product, если вы не хотите разрешать продукты с количеством символов менее 4 в некоторых сценариях (в этом случае вы должны применить правило 4 символов на уровне контроллера и / или на стороне клиента). Помните, что ваши доменные объекты могут повторно использоваться другими контроллерами, действиями, внутренними методами или даже другими приложениями, если вы используете библиотеку совместно. Ваша проверка должна соответствовать абстракции, которую вы моделируете, независимо от приложения или варианта использования.

Поскольку вы используете ASP .NET MVC, вы должны воспользоваться расширенными и расширяемыми API-интерфейсами проверки, включенными в платформу (для поиска используйте ключевые слова IDataErrorInfo MVC Validation Application Block DataAnnotations). У вызывающего метода есть много способов узнать, что ваш объект домена отклонил аргумент - например, выбрасывая ArgumentOutOfRangeException.

В качестве примера обеспечения уникальности названий продуктов вы бы просто не поместили бы это в класс Product, поскольку для этого требуется знание всех других Product s. Это логически относится к постоянному слою и, возможно, к хранилищу. В зависимости от вашего варианта использования может потребоваться отдельный метод обслуживания, который проверяет, что имя еще не существует, но вы не должны предполагать, что оно будет все еще уникальным, когда вы позже попытаетесь сохранить его (оно должно проверить еще раз, потому что если вы подтвердите уникальность, а затем сохраните ее еще некоторое время перед сохранением, кто-то еще может сохранить запись с тем же именем).

1 голос
/ 02 февраля 2010

Вот как я это делаю:

Я сохраняю код проверки в классе сущности, который наследует некоторый общий интерфейс элементов.

Interface Item {
    bool Validate();
}

Затем в функциях репозитория CRUD я вызываю соответствующую функцию Validate.

Таким образом, все логические пути проверяют мои значения, но мне нужно посмотреть только в одном месте, чтобы увидеть, что это за проверка на самом деле.

Кроме того, иногда вы используете сущности вне области репозитория, например, в представлении. Поэтому, если валидация разделена, каждый путь действия может проверяться на валидацию, не запрашивая хранилище.

0 голосов
/ 29 августа 2011

U может использовать другую систему проверки. Вы можете добавить метод к IService на уровне сервиса, такой как:

IEnumerable<IIssue> Validate(T entity)
{
    if(entity.Id == null)
      yield return new Issue("error message");
}
0 голосов
/ 03 февраля 2010

Хорошо, вот мой третий ответ, потому что есть очень много способов снять шкуру с этой кошки:

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!");
0 голосов
/ 03 февраля 2010

Еще один способ сделать это с помощью слабой связи - создать классы валидаторов для ваших типов сущностей и зарегистрировать их в вашем 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 обо всех таких реализациях и запустить их в цикле.

0 голосов
/ 03 февраля 2010

В соответствии с SRP (принцип единой ответственности), вам может быть лучше обслуживаться, если валидация отделена от логики domain продукта. Поскольку это требуется для обеспечения целостности данных, вероятно, оно должно быть ближе к хранилищу - вы просто хотите быть уверены, что проверка всегда выполняется без необходимости думать об этом.

В этом случае у вас может быть общий интерфейс (например, 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.

0 голосов
/ 02 февраля 2010

Для ограничений я использую частичные классы в DAL и реализую валидаторы аннотаций данных.Довольно часто это включает в себя создание пользовательских валидаторов, но это прекрасно работает, поскольку это абсолютно гибко.Мне удалось создать очень сложные зависимые проверки, которые даже попадают в базу данных как часть их проверок достоверности.

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

...