Я думаю, нам следует взглянуть на принципы 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