Доступ до сховищ з домену


14

Скажімо, у нас є система реєстрації завдань, коли завдання реєструється, користувач визначає категорію та завдання за замовчуванням до статусу "Видатний". Припустимо, що в цьому випадку категорія та статус мають бути реалізовані як сутності. Зазвичай я б це робив:

Шар програми:

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, і жодним чином ви не хотіли б обчислити цю ділову логіку на рівні додатків, так що, безумовно, це був би найбільш підходящий спосіб?

Отже, це поважна причина доступу до сховищ з домену?

EDIT: Це може бути й у випадку нестатичних методів:

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

Ви змішуєтесь

Суб'єкти не повинні мати доступ до сховищ

(що є гарною пропозицією)

і

рівень домену не повинен мати доступ до сховищ

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

Якщо ви не хочете вводити цю логіку створення в статичний метод класу особи, ви можете ввести окремі фабричні класи (як частина доменного шару!) І помістити туди логіку створення.

EDIT: на ваш Updateприклад: враховуючи, що це _urgencyRepositoryі statusRepository є членами класу Task, визначеними як якийсь інтерфейс, вам тепер потрібно ввести їх у будь-яку Taskсутність, перш ніж ви зможете використовувати Updateзараз (наприклад, у конструкторі завдань). Або ви визначаєте їх як статичні члени, але будьте обережні, що можуть легко викликати багатопотокові проблеми або просто проблеми, коли вам потрібні різні сховища для різних об'єктів завдань одночасно.

Така конструкція робить трохи складніше створювати Taskсуттєві утворення окремо, таким чином складніше писати одиничні тести для Taskсутностей, складніше писати автоматичні тести залежно від утворень Завдання, і ви створюєте трохи більше накладних витрат на пам'ять, оскільки кожен суб'єкт із завданнями тепер повинен вважають, що два посилання на репости. Звичайно, це може бути терпимим у вашому випадку. З іншого боку, створення окремого класу утиліт, TaskUpdaterякий зберігає посилання на потрібні сховища, може бути часто або принаймні іноді кращим рішенням.

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


Я відредагував, щоб показати, що це стосується нестатичних методів так само, як і статичних. Я ніколи не думав, що фабричний метод не є частиною сутності.
Пол Т Девіс

@PaulTDavies: дивіться мою редакцію
Doc Brown

Я погоджуюся з тим, що ви тут говорите, але я би додав стислий фрагмент, який малює точку, що Status = _statusRepository.GetById(Constants.Status.OutstandingId)є діловим правилом , яке можна прочитати як "Бізнес диктує, що початковий статус усіх завдань буде неперевершеним". цей рядок коду не належить до сховища, яке стосується лише управління даними за допомогою операцій CRUD.
Джиммі Хоффа

@JimmyHoffa: Хм, тут ніхто не запропонував перенести таку лінію в один із класів сховища, ані ОП, ані я - тож, що ви думаєте?
Doc Brown

Мені дуже подобається ідея TaskUpdater як доміанської служби. Це якимось чином здається, що якихось помилок лише для того, щоб зберегти принципи DDD, але це означає, що я можу уникати ін'єкцій сховища кожного разу, коли використовую завдання.
Пол Т Девіс

6

Я не знаю, чи ваш приклад статусу є реальним кодом чи тут просто для демонстрації, але мені здається дивним, що вам слід реалізувати статус як сутність (не кажучи вже про корінний корінь), коли його ідентифікатор є постійним визначенням в коді - Constants.Status.OutstandingId. Чи не переможе це з метою "динамічних" статусів, до яких можна додати стільки, скільки потрібно в базу даних?

Додам, що у вашому випадку побудова Task(включаючи завдання отримання потрібного статусу від StatusRepository, якщо це необхідно) може заслуговувати, TaskFactoryа не залишатися в Taskсобі, оскільки це нетривіальна збірка об'єктів.

Але:

Мені послідовно кажуть, що суб’єкти не повинні мати доступ до сховищ

Це твердження є в кращому випадку неточним та надмірним, хибним та в гіршому випадку небезпечним.

У архітектурах, орієнтованих на домени, прийнято вважати, що суб'єкт господарювання не повинен знати, як зберігати себе - ось принцип ігнорування стійкості. Тож жодних викликів до його сховища не додавати до сховища. Чи повинен він знати, як (і коли) зберігати інші об’єкти ? Знову ж таки, ця відповідальність, здається, належить іншому об'єкту - можливо, об’єкту, який усвідомлює контекст виконання та загальний хід поточного випадку використання, як-от послуга рівня додатків.

Чи може суб'єкт господарювання використовувати сховище для отримання іншої сутності ? У 90% часу це не повинно бути, оскільки сутність, яка йому потрібна, зазвичай знаходиться в межах її сукупності або може бути отримана шляхом обходу інших об'єктів. Але бувають випадки, коли їх немає. Наприклад, якщо ви приймаєте ієрархічну структуру, наприклад, суб'єктам часто потрібно звертатися до всіх своїх предків, конкретного онука тощо, як частина їх внутрішньої поведінки. Вони не мають прямого посилання на цих віддалених родичів. Було б незручно передавати цих родичів навколо них як параметри операції. То чому б не використовувати Репозиторій, щоб отримати їх - за умови, що вони є сукупними коренями?

Є ще кілька прикладів. Річ у тім, що іноді є поведінка, яку ви не можете розмістити в службі Домена, оскільки вона, здається, ідеально вписується в існуючу сутність. І все-таки цьому суб’єкту потрібно отримати доступ до сховища, щоб гідратавати корінь або колекцію коренів, які не можуть бути передані йому.

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


Я не погоджуюся з тим, що суб'єкт господарювання повинен використовувати сховище для доступу до об'єкта, до якого він вже має відношення, - ви повинні мати можливість перемістити графік об'єкта, щоб отримати доступ до цієї сутності. Використання сховища таким чином є абсолютним "ні". Що я тут обговорюю, - це те, що суб'єкт господарювання ще не має посилання на нього, але його повинен створити за певних умов бізнесу.
Пол Т Девіс

Добре, якщо ви мене добре прочитали, ми повністю з цим погоджуємось ...
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;
        }

    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.