Архитектура - доменно-управляемый дизайн против строгого применения бизнес-логики - PullRequest
2 голосов
/ 13 января 2010

Мои бизнес-объекты имеют следующую архитектуру:

  • проверка любых входящих данных вызывает исключение в установщике, если оно не соответствует бизнес-логике.
    • свойство не может быть повреждено / несовместимо, если существующее значение по умолчанию / null недопустимо
  • бизнес-объекты могут создаваться бизнес-модулем только через метод статического фабричного типа, который принимает реализацию интерфейса, которая используется совместно с бизнес-объектом, для копирования в бизнес-объект.
    • Обеспечивает, чтобы контейнеры зависимостей, пользовательский интерфейс и персистентные слои не могли создавать недопустимый объект Model или передавать его куда-либо.
  • Этот фабричный метод перехватывает все различные исключения проверки в словаре проверки, так что, когда попытки проверки завершены, словарь, предоставленный вызывающей стороной, заполняется именами полей и сообщениями, и возникает исключение, если какая-либо из проверок не произвела проходить.
    • легко сопоставляется с полями пользовательского интерфейса с соответствующими сообщениями об ошибках
  • В бизнес-объектах нет методов типа базы данных / персистентности
  • необходимые поведения персистентности определяются через интерфейсы репозитория в бизнес-модуле

Пример интерфейса бизнес-объекта:

public interface IAmARegistration
{
  string Nbk { get; set; } //Primary key?
  string Name { get; set; }
  string Email { get; set; }
  string MailCode { get; set; }
  string TelephoneNumber { get; set; }
  int? OrganizationId { get; set; }
  int? OrganizationSponsorId { get; set; }
 }

интерфейс хранилища бизнес-объектов:

 /// <summary>
 /// Handles registration persistance or an in-memory repository for testing
 /// requires a business object instead of interface type to enforce validation
 /// </summary>
 public interface IAmARegistrationRepository
 {
  /// <summary>
  /// Checks if a registration record exists in the persistance mechanism
  /// </summary>
  /// <param name="user">Takes a bare NBK</param>
  /// <returns></returns>
   bool IsRegistered(string user); //Cache the result if so

  /// <summary>
  /// Returns null if none exist
  /// </summary>
  /// <param name="user">Takes a bare NBK</param>
  /// <returns></returns>
   IAmARegistration GetRegistration(string user);

   void EditRegistration(string user,ModelRegistration registration);

   void CreateRegistration(ModelRegistration registration);
  }

Тогда фактический бизнес-объект выглядит следующим образом:

public class ModelRegistration : IAmARegistration//,IDataErrorInfo
{
    internal ModelRegistration() { }
    public string Nbk
    {
        get
        {
            return _nbk;
        }
        set
        {
            if (String.IsNullOrEmpty(value))
                throw new ArgumentException("Nbk is required");
            _nbk = value;
        }
    }
    ... //other properties omitted
    public static ModelRegistration CreateModelAssessment(IValidationDictionary validation, IAmARegistration source)
    {

        var result = CopyData(() => new ModelRegistration(), source, false, null);
        //Any other complex validation goes here
        return result;
    }

    /// <summary>
    /// This is validated in a unit test to ensure accuracy and that it is not out of sync with 
    /// the number of members the interface has
    /// </summary>
    public static Dictionary<string, Action> GenerateActionDictionary<T>(T dest, IAmARegistration source, bool includeIdentifier)
where T : IAmARegistration
    {
        var result = new Dictionary<string, Action>
            {
                {Member.Name<IAmARegistration>(x=>x.Email),
                    ()=>dest.Email=source.Email},
                {Member.Name<IAmARegistration>(x=>x.MailCode),
                    ()=>dest.MailCode=source.MailCode},
                {Member.Name<IAmARegistration>(x=>x.Name),
                    ()=>dest.Name=source.Name},
                {Member.Name<IAmARegistration>(x=>x.Nbk),
                    ()=>dest.Nbk=source.Nbk},
                {Member.Name<IAmARegistration>(x=>x.OrganizationId),
                    ()=>dest.OrganizationId=source.OrganizationId},
                {Member.Name<IAmARegistration>(x=>x.OrganizationSponsorId),
                    ()=>dest.OrganizationSponsorId=source.OrganizationSponsorId},
                {Member.Name<IAmARegistration>(x=>x.TelephoneNumber),
                    ()=>dest.TelephoneNumber=source.TelephoneNumber}, 

            };

        return result;

    }
    /// <summary>
    /// Designed for copying the model to the db persistence object or ui display object
    /// </summary>
    public static T CopyData<T>(Func<T> creator, IAmARegistration source, bool includeIdentifier,
        ICollection<string> excludeList) where T : IAmARegistration
    {
        return CopyDictionary<T, IAmARegistration>.CopyData(
            GenerateActionDictionary, creator, source, includeIdentifier, excludeList);
    }

    /// <summary>
    /// Designed for copying the ui to the model 
    /// </summary>
    public static T CopyData<T>(IValidationDictionary validation, Func<T> creator,
        IAmARegistration source, bool includeIdentifier, ICollection<string> excludeList)
         where T : IAmARegistration
    {
        return CopyDictionary<T, IAmARegistration>.CopyData(
            GenerateActionDictionary, validation, creator, source, includeIdentifier, excludeList);
    }

Пример репозитория, с которым у меня возникают проблемы при написании изолированных тестов:

    public void CreateRegistration(ModelRegistration registration)
    {
        var dbRegistration = ModelRegistration.CopyData(()=>new Registration(), registration, false, null);

       using (var dc=new LQDev202DataContext())
       {
           dc.Registrations.InsertOnSubmit(dbRegistration);
           dc.SubmitChanges();
       }
    }

Вопросы:

  • Когда добавляется новый член, необходимо внести как минимум 8 мест (дБ, конструктор linq-to-sql, интерфейс модели, свойство модели, словарь копирования модели, пользовательский интерфейс, пользовательский интерфейс DTO, модульный тест
  • Тестируемость
    • тестирование методов БД, которые жестко запрограммированы на зависимость от точного типа, который не имеет общедоступного конструктора по умолчанию и должен проходить через другой метод, что делает тестирование изолированным либо невозможным, либо потребуется вмешательство в бизнес-объект для создания это более проверяемое.
    • Использование InternalsVisibleTo, чтобы у BusinessModel.Tests был доступ к внутреннему конструктору, но мне нужно было бы добавить это для любого другого модуля тестирования слоя постоянства, что делает его очень плохо масштабируемым
  • , чтобы сделать функцию копирования общей, бизнес-объектам требовались публичные сеттеры
    • Я бы предпочел, чтобы объекты модели были неизменными
  • DTO требуются для того, чтобы пользовательский интерфейс предпринял попытку проверки данных

Я стремлюсь к полному повторному использованию этого бизнес-уровня с другими механизмами персистентности и механизмами пользовательского интерфейса (windows forms, asp.net, asp.net mvc 1 и т. Д.). Кроме того, члены команды могут развиваться в соответствии с этим бизнес-уровнем / архитектурой с минимальными трудностями.

Есть ли способ применить неизменные проверенные объекты модели или обеспечить, чтобы ни пользовательский интерфейс, ни уровень персистентности не могли удержать недопустимый уровень без этих головных болей?

Ответы [ 2 ]

2 голосов
/ 13 января 2010

Это выглядит очень сложно для меня.

IMO, доменные объекты должны быть POCO, которые защищают их инварианты. Для этого вам не нужна фабрика: просто потребуйте все необходимые значения в конструкторе и предоставьте действительные значения по умолчанию для остальных.

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

Написать неизменный объект на C # легко - просто убедитесь, что все поля объявлены с ключевым словом readonly.

Однако имейте в виду, что если вы будете следовать доменно-управляемому дизайну , доменные объекты, как правило, попадают в три сегмента

  • Сущности. Изменчивые объекты с долгоживущими идентичностями
  • Объекты значения. Неизменяемые объекты без идентичности
  • Услуги. Stateless

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

0 голосов
/ 13 января 2010

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

Преимущество этого, по крайней мере, в мире .NET, заключается в более простой в использовании поверхности домена и более лаконичном и читабельном коде (без огромных блоков try-catch или обходных механизмов обработки исключений). Это также хорошо интегрируется с подходом валидации ASP.NET MVC.

Я скажу, что в неимущественных операциях, предоставляемых объектами домена (например, те, которые подразумеваются законом Деметры над составными коллекциями), я склонен возвращать ValidationResult, который обеспечивает немедленную обратную связь проверки. Этот отзыв также возвращается методами Validate. Причина, по которой я это делаю, заключается в том, что нет никакого вредного влияния на структуру или читаемость потребляемого кода. Если вызывающая сторона не хочет проверять возвращаемое значение, это не обязательно; если они хотят, они могут. Такое преимущество невозможно с установщиками свойств, не влияя также на структуру потребляющего кода (с блоками try-catch). Аналогично, доменные службы возвращают ValidationResults. Только репозитории выбрасывают ValidationExceptions.

В целом, это позволяет вам переводить доменные объекты в недопустимые состояния, но они изолированы от контекста выполнения вызывающего («рабочего пространства» вызывающего) и не могут быть сохранены в системе в целом.

...