DDD и использование геттеров и сеттеров

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

Но этот принцип меня до сих пор смущает. Например, что произойдет, если мне нужно изменить значение переменной-члена объекта? Например, если имя человека изменилось, как я могу отразить это в модели? Сначала я подумал, а почему бы не иметь функцию под названием «ChangeName», которая позволяет мне передать новое имя, а она, в свою очередь, может изменить внутреннюю переменную «name». Ну .... это же сеттер, не так ли!

Что мне нужно уточнить - если бы я полностью исключил сеттеры, то в ситуациях, подобных описанной выше, должен ли я полагаться исключительно на параметры конструктора? Должен ли я передать новое значение атрибута вместо старого значения атрибута через конструктор, после чего я могу сохранить изменения, передав объект в любую имеющуюся у меня инфраструктуру сохранения?

Эти две статьи полезны в этом обсуждении:

  1. http://kellabyte.com/tag/ddd/
  2. http://typicalprogrammer.com/?p=23

person Chris    schedule 28.11.2011    source источник


Ответы (2)


Что ж, это классическая дискуссия. Здесь, в Stack Overflow, есть несколько других тем по этому поводу.

Но. Get / Set (Автоматические свойства?) Не так уж и плохи. Но они, как правило, заставляют вас создавать свои объекты как «мертвые» контейнеры данных, которые имеют только свойства, а не методы. Признаки этого часто называют анемической областью - и они имеют очень слабое поведение. Моя рекомендация:

  1. Постарайтесь как можно меньше использовать опору.
  2. Попытайтесь найти группы данных, которые принадлежат друг другу и ДОЛЖНЫ быть вместе, как например. Имя, отчество и фамилия. Другой пример - Почтовый индекс, Город, Улица. Эти данные лучше задать через метод. Это сводит к минимуму вероятность того, что ваша сущность станет недействительной.
  3. Часто данные, которые принадлежат друг другу, могут быть сгруппированы как объект Value.
  4. Объекты большего количества значений имеют тенденцию вызывать более описательные методы из вашей сущности, которые являются «глаголами», а не вашими обычно сущностями «существительными».
  5. Также открывается больше методов для ваших объектов значений для добавления большего количества поведения и, возможно, уменьшения ваших «жирных» сервисов (возможно, у вас нет сервисов со слишком большой утечкой бизнес-логики ...).

Здесь есть что сказать ... но короткий ответ. О настройке данных в конструкторе: я делаю это только в том случае, если этот объект не может «жить» / существовать без этих данных. Для Entity Person я бы сказал, что Name, возможно, не так уж и важен. Но номер социального страхования может быть кандидатом на данные конструктора. Или объект Employee должен иметь Company в конструкторе просто потому, что сотрудник должен принадлежать к компании.

person Magnus Backeus    schedule 28.11.2011

Я думаю, нам следует взглянуть на принципы DDD и получить оттуда правильный ответ.

Публичные геттеры / установщики автоматических свойств в C # являются функционально просто общедоступными свойствами. Использование автоматических методов получения / установки свойств по своей сути неплохо, если нет бизнес-правил, касающихся правильных значений соответствующих свойств, и нет событий домена, которые должны запускаться при изменении этих свойств.

Кроме того, не следует создавать агрегаты или объекты, которые имеют только общедоступные автоматические свойства, поскольку это ведет к анемичной модели и анемичной области. Такой агрегат является не фактическим агрегатом, а скорее DTO или объектом значения.

Лично я считаю, что если мы будем использовать средства доступа к свойствам (get / set) с телами для интеграции бизнес-логики, мы сможем сделать наш код немного более читаемым и, вероятно, намного менее подробным.

Например:

// Instead of this:

public DemoAggregate : IAggregate
{
  public string Name { get; private set; }

  public void ChangeName(string newName)
  {
    Name = Check.MinMaxLength(newName, 1, 100,
      $"{nameof(newName)} length must be between 1 and 100 characters.");
  }
}

/* MinMaxLength throws a business exception
 * if the new name is outside of the accepted range
 * otherwise it returns the value unchanged.
 */

// ...you can write this to get one method less:

public AltDemoAggregate : IAggregate
{
  private string _name;

  public string Name
  {
    get => _name;
    set => value = Check.MinMaxLength(newName, 1, 100,
      $"{nameof(newName)} length must be between 1 and 100 characters.");
  }
}

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

С другой стороны, если вы используете что-то вроде Entity Framework, я думаю, вы можете настроить его для гидратации новых экземпляров, вызывая свойства (не поддерживающие поля), тем самым предотвращая загрузку недопустимого агрегата из базы данных (скажем, если вы импортировали некоторые объемные данные, которые могут содержать мусор). Однако я не проверял это.

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

Можно использовать геттер с выражением, подобный приведенному выше, потому что строки имеют семантику значений в C #, так что выражение возвращает копию _name, тем самым не открывая ссылку на внутреннюю переменную.

Обратите внимание, что, например, с записями C # 9 у вас есть только семантика равенства на основе значений. Запись по-прежнему передается по ссылке! Поскольку в идеале записи должны быть неизменяемыми (только для инициализации), вы можете возвращать ссылки на такие записи (что более производительно) и пропустить необходимость клонирования (что просто для мелких клонов, но сложно для глубоких клонов).

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

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

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

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

ИЗМЕНИТЬ:

Я просто понял, что мой пример не совсем правильный. Такие (частные?) Аксессоры с логикой можно использовать для простой логики, например, чтобы убедиться, что при установке конечной даты она не раньше даты начала, но в сложных случаях установка конечной даты, например, может происходить по ряду причин. которые должны быть смоделированы как глаголы, например terminateContract(DateTime finalDay, string reason) или closeContract(DateTime closedEarlyDate), они должны быть более явными по причине установки даты окончания. В любом случае, в этом случае общая логика, которую следует применять всегда, может находиться в методе доступа установщика (это обеспечивает дедупликацию кода), а логика для каждого случая для каждого действия может жить в конкретном методе действия.

person Paul-Sebastian Manole    schedule 04.06.2021