Доступ к хранилищам из домена

14

Скажем, у нас есть система регистрации задач, когда задача регистрируется, пользователь указывает категорию, и задача по умолчанию имеет статус «Не выполнено». Предположим, что в этом случае Category и Status должны быть реализованы как объекты. Обычно я бы сделал это:

Уровень приложений:

public class TaskService
{
    //...

    public void Add(Guid categoryId, string description)
    {
        var category = _categoryRepository.GetById(categoryId);
        var status = _statusRepository.GetById(Constants.Status.OutstandingId);
        var task = Task.Create(category, status, description);
        _taskRepository.Save(task);
    }
}

Сущность:

public class Task
{
    //...

    public static void Create(Category category, Status status, string description)
    {
        return new Task
        {
            Category = category,
            Status = status,
            Description = descrtiption
        };
    }
}

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

Сущность:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        return new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };
    }
}

Хранилище состояния в любом случае внедряется в зависимость, поэтому реальной зависимости нет, и мне кажется, что именно домен принимает решение о том, что задача по умолчанию не выполнена. В предыдущей версии создается впечатление, что именно уровень приложений принимает это решение. Любой, почему контракты репозитория часто в домене, если это не должно быть возможным?

Вот более крайний пример, здесь домен решает срочность:

Сущность:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        var task = new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            task.Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

Нет способа передать все возможные версии Urgency, и нет способа вычислить эту бизнес-логику на прикладном уровне, поэтому, несомненно, это будет наиболее подходящим способом?

Так является ли это действительной причиной для доступа к репозиториям из домена?

РЕДАКТИРОВАТЬ: Это также может иметь место в случае нестатических методов:

public class Task
{
    //...

    public void Update(Category category, string description)
    {
        Category = category,
        Status = _statusRepository.GetById(Constants.Status.OutstandingId),
        Description = descrtiption

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}
Пол Т Дэвис
источник

Ответы:

8

Вы смешиваете

сущности не должны иметь доступ к репозиториям

(что является хорошим предложением)

и

слой домена не должен иметь доступ к репозиториям

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

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

РЕДАКТИРОВАТЬ: к вашему Updateпримеру: учитывая, что _urgencyRepositoryи statusRepository являются членами класса Task, определенными как некоторый интерфейс, теперь вам нужно внедрить их в любой Taskобъект, прежде чем вы сможете использовать его Updateсейчас (например, в конструкторе Task). Или вы определяете их как статические члены, но имейте в виду, что это может легко вызвать проблемы с многопоточностью или просто проблемы, когда вам нужны разные репозитории для разных объектов Task одновременно.

Такая конструкция делает создание Taskобъектов в отдельности немного сложнее , что затрудняет написание модульных тестов для Taskобъектов, затрудняет написание автоматических тестов в зависимости от объектов Task, и вы производите немного больше накладных расходов памяти, поскольку теперь каждому объекту Task необходимо провести две ссылки на репозитории. Конечно, это может быть терпимо в вашем случае. С другой стороны, создание отдельного служебного класса, TaskUpdaterкоторый хранит ссылки на правильные репозитории, может быть часто или, по крайней мере, иногда лучшим решением.

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

Док Браун
источник
Я отредактировал, чтобы показать, что это относится как к статическим методам, так и к статическим. Я никогда не думал, что фабричный метод не является частью сущности.
Пол Т Дэвис
@PaulTDavies: см. Мое редактирование
Док Браун
Я согласен с тем, что вы здесь говорите, но я бы добавил краткую статью, в которой подчеркивается точка зрения, Status = _statusRepository.GetById(Constants.Status.OutstandingId)являющаяся бизнес-правилом , которую вы могли бы прочитать как «Бизнес диктует, что первоначальный статус всех задач будет невыполненным», и именно поэтому эта строка кода не принадлежит внутри репозитория, единственной задачей которого является управление данными с помощью операций CRUD.
Джимми Хоффа
@JimmyHoffa: хм, никто здесь не предлагал поместить такую ​​линию в один из классов репозитория, ни OP, ни я - так в чем ваша точка зрения?
Док Браун
Мне очень нравится идея TaskUpdater как сервис домиан. Каким-то образом кажется немного выдумкой просто для того, чтобы сохранить принципы DDD, но это означает, что я могу избегать внедрения хранилища каждый раз, когда использую Task.
Пол Т Дэвис
6

Я не знаю, является ли ваш пример статуса реальным кодом или здесь просто для демонстрации, но мне кажется странным, что вы должны реализовывать Status как сущность (не говоря уже о Aggregate Root), когда его ID является константой, определенной в коде - Constants.Status.OutstandingId. Разве это не противоречит цели «динамических» статусов, которые вы можете добавить в базу данных, сколько хотите?

Я бы добавил, что в вашем случае создание Task(включая получение правильного статуса из StatusRepository, если это необходимо) может заслуживать, TaskFactoryа не оставаться Taskсамо по себе, поскольку это нетривиальная совокупность объектов.

Но :

Мне постоянно говорят, что объекты не должны иметь доступ к репозиториям

Это утверждение является в лучшем случае неточным и слишком упрощенным, в худшем - вводящим в заблуждение и опасным.

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

Может ли объект использовать хранилище для извлечения другого объекта ? В 90% случаев это не обязательно, так как сущности, в которых они нуждаются, обычно находятся в области его совокупности или могут быть получены путем обхода других объектов. Но бывают случаи, когда это не так. Например, если вы берете иерархическую структуру, сущности часто должны получить доступ ко всем своим предкам, конкретному внуку и т. Д. Как часть их внутреннего поведения. У них нет прямой ссылки на этих отдаленных родственников. Было бы неудобно передавать этих родственников им в качестве параметров операции. Так почему бы не использовать репозиторий, чтобы получить их - при условии, что они являются совокупными корнями?

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

Таким образом, доступ к хранилищу от сущности сам по себе неплохой , он может принимать различные формы, возникающие в результате различных проектных решений, от катастрофических до приемлемых.

guillaume31
источник
Я не согласен с тем, что объект должен использовать репозиторий для доступа к объекту, к которому он уже имеет отношение - вы должны иметь возможность просматривать граф объекта для доступа к этому объекту. Использование хранилища таким образом является абсолютным нет нет. То, что я обсуждаю здесь, - это побуждает к тому, что у организации еще нет ссылки на нее, но она должна быть создана в некоторых бизнес-условиях.
Пол Т Дэвис
Ну, если вы хорошо меня прочитали, мы полностью согласны с этим ...
guillaume31
2

Это одна из причин, по которой я не использую Enums или таблицы чистого просмотра в своем домене. Срочность и статус являются состояниями, и существует логика, связанная с состоянием, которое напрямую связано с этим состоянием (например, в какие состояния я могу переходить, учитывая мое текущее состояние). Кроме того, записывая состояние как чистое значение, вы теряете информацию, например, как долго задача находилась в данном состоянии. Я представляю статусы как иерархию классов. (В C #)

public class Interval
{
  public Interval(DateTime start, DateTime? end)
  {
    Start=start;
    End=end;
  }

  //To be called by internal framework
  protected Interval()
  {
  }

  public void End(DateTime? when=null)
  {
    if(when==null)
      when=DateTime.Now;
    End=when;
  }

  public DateTime Start{get;protected set;}

  public DateTime? End{get; protected set;}
}

public class TaskStatus
{
  protected TaskStatus()
  {
  }
  public Long Id {get;protected set;}

  public string Name {get; protected set;}

  public string Description {get; protected set;}

  public Interval Duration {get; protected set;}

  public virtual TNewStatus TransitionTo<TNewStatus>()
    where TNewStatus:TaskStatus
  {
    throw new NotImplementedException();
  }
}

public class OutStandingTaskStatus:TaskStatus
{
  protected OutStandingTaskStatus()
  {
  }

  public OutStandingTaskStatus(bool initialize)
  {
    Name="Oustanding";
    Description="For tasks that need to be addressed";
    Duration=new Interval(DateTime.Now,null);
  }

  public override TNewStatus TransitionTo<TNewStatus>()
  {
    if(typeof(TNewStatus)==typeof(CompletedTaskStatus))
    {
      var transitionDate=DateTime.Now();
      Duration.End(transitionDate);
      return new CompletedTaskStatus(true);
    }
    return base.TransitionTo<TNewStatus>();
  }
}

Реализация CompletedTaskStatus была бы почти такой же.

Здесь следует отметить несколько вещей:

  1. Я делаю конструкторы по умолчанию защищенными. Это так, что фреймворк может вызывать его при извлечении объекта из постоянства (и EntityFramework Code-first, и NHibernate используют прокси-серверы, полученные из ваших доменных объектов, для создания своей магии).

  2. Многие из установщиков свойств защищены по той же причине. Если я хочу изменить дату окончания интервала, я должен вызвать функцию Interval.End () (это является частью доменного дизайна, обеспечивающего значимые операции, а не анемичные доменные объекты).

  3. Я не показываю это здесь, но Задача также скрыла бы детали того, как она сохраняет свой текущий статус. У меня обычно есть защищенный список HistoricalStates, который я позволяю общественности запрашивать, если они заинтересованы. В противном случае я выставляю текущее состояние как метод получения, который запрашивает HistoricalStates.Single (state.Duration.End == null).

  4. Функция TransitionTo важна, потому что она может содержать логику о том, какие состояния допустимы для перехода. Если у вас просто перечисление, эта логика должна лежать в другом месте.

Надеюсь, это поможет вам немного лучше понять подход DDD.

Майкл Браун
источник
1
Это, безусловно, будет правильным подходом, если разные состояния будут вести себя по-разному, как в примере с вашим шаблоном состояний, и это, безусловно, решает и обсуждаемую проблему. Тем не менее, мне было бы трудно обосновать класс для каждого состояния, если бы они просто имели разные значения, а не разное поведение.
Пол Т Дэвис
1

Я пытался решить эту проблему в течение некоторого времени, я решил, что хочу иметь возможность вызывать Task.UpdateTask (), хотя я бы предпочел, чтобы это зависело от конкретного домена, в вашем случае, возможно, я бы назвал его Task.ChangeCategory (...) для обозначения действия, а не только CRUD.

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UnitTestProject2
{
    public class ClientCode
    {
        public void Main()
        {
            TaskFactory factory = new TaskFactory();
            Task task = factory.Create();
            task.UpdateTask(new Category(), "some value");
        }

    }
    public class Category
    {
    }

    public class Task
    {
        public Action<Category, String> UpdateTask { get; set; }

        public static void UpdateTaskAction(Task task, Category category, string description)
        {
            // do the logic here, static can access private if needed
        }
    }

    public class TaskFactory
    {      
        public Task Create()
        {
            Task task = new Task();
            task.UpdateTask = (category, description) =>
                {
                    Task.UpdateTaskAction(task, category, description);
                };

            return task;
        }

    }
}
Майк
источник