Закон Деметры с объектами модели данных
Вчера я вернулся на работу из отпуска, и в нашем ежедневном выступлении мои товарищи по команде упоминали, что они рефакторировали все объекты модели в нашем Java-коде, чтобы удалить все геттеры и сеттеры и сделать поля модели все общедоступными объектами вместо этого, вызывая Закон Деметры как причина для этого, потому что
чтобы облегчить нашу приверженность закону Деметры: модуль не должен знать о внутренностях "объектов", которые он манипулирует. Поскольку данные структуры не содержат поведения, они, естественно, раскрывают их внутреннюю структуру. Поэтому в этом случае Demeter не применяется.
Я признаю, что должен был освежить мои знания о LoD, но для жизни меня я не могу найти ничего, чтобы указать, что это находится в духе закона. Ни один из геттеров/сеттеров в наших моделях не содержит никакой бизнес-логики, что является его оправданием для этого, поэтому клиентам этих объектов не нужно понимать, выполняется ли какая-либо бизнес-логика в методах get/set.
Я думаю, что это неправильное понимание того, что значит "внутреннее знание структуры объектов", или, по крайней мере, воспринимает это слишком буквально и нарушает довольно стандартное соглашение в этом процессе.
Итак, мой вопрос заключается в том, действительно ли имеет смысл разоблачить внутреннюю структуру объектов модели напрямую, а не через getters/setters в имени LoD?
Ответы
Ответ 1
Существует книга под названием "Чистый код" Роберта Мартина, которая охватывает это.
В главе 6 (Объекты и структуры данных) он рассказывает о фундаментальных различиях между объектами и структурами данных.
Объекты извлекают выгоду из инкапсуляции, структуры данных этого не делают.
Существует раздел о Законе Деметры:
Известная эвристика называется Закон Деметры, в которой говорится, что модуль не должен знать о внутренностях объектов, которыми он манипулирует. Как мы видели в последнем разделе, объекты скрывают свои данные и выставляют операции. Это означает, что объект не должен выставлять свою внутреннюю структуру через аксессоров, потому что для этого нужно выставлять, а не скрывать свою внутреннюю структуру.
Точнее, Закон Деметры говорит, что метод f класса C должен вызывать только методы из них:
- С
- Объект, созданный f
- Объект, переданный как аргумент f
- Объект, хранящийся в переменной экземпляра C
Метод не должен вызывать методы для объектов, возвращаемых любой из разрешенных функций. Другими словами, поговорите с друзьями, а не с незнакомцами.
Дядя Боб приводит пример нарушения LoD:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
Является ли это нарушением Деметры, зависит от того, являются ли ctxt, Options и ScratchDir объектами или структурами данных. Если они являются объектами, то их внутренняя структура должна быть скрыта, а не раскрыта, и поэтому знание их внутренностей является явным нарушением Закона Деметры. С другой стороны, если ctxt, Options и ScratchDir - это просто структуры данных без какого-либо поведения, то они естественно выставляют свою внутреннюю структуру, и поэтому Demeter не применяется.
Использование функций доступа сглаживает проблему. Если бы код был написан следующим образом, мы, вероятно, не спрашивали о нарушениях Деметры.
final String outputDir = ctxt.options.scratchDir.absolutePath;
Итак, это, вероятно, то, откуда происходят ваши сотрудники. Я думаю, что аргумент "мы должны сделать это, потому что LoD" в лучшем случае является неточным. Центральная проблема - это не LoD так много, как API состоит из объектов или структур данных. Это похоже на ненужное и подверженное ошибкам изменение, которое нужно проталкивать, когда есть более насущные дела.
Ответ 2
Мне не кажется, что это изменение имеет какое-либо отношение к Law of Demeter. Закон, по сути, заключается в кодировании структуры вашего объектного графа в ваш код путем вызова методов через целую цепочку других объектов. Например, предположим, что в заявке на страхование автомобиля, что у клиента есть политика, а у политики есть транспортные средства, а транспортные средства имеют назначенные им водители, а водители имеют даты рождения и, таким образом, возрасты. Вы можете представить следующий код:
public boolean hasUnderageDrivers(Customer customer) {
for (Vehicle vehicle : customer.getPolicy().getVehicles()) {
for (Driver driver : vehicle.getDrivers()) {
if (driver.getAge() < 18) {
return true;
}
}
}
return false;
}
Это нарушит Закон Деметры, потому что у этого кода теперь есть знание внутренних дел, которое ему не нужно знать. Он знает, что водители назначаются на транспортные средства, а не просто назначаются страховому полису в целом. Если в будущем страховая компания решит, что водители просто будут в политике, вместо того, чтобы быть привязанным к конкретным транспортным средствам, тогда этот код должен измениться.
Проблема заключается в том, что он вызывает метод своего параметра getPolicy()
, а затем другой, getVehicles()
, а затем другой, getDrivers()
, а затем другой, getAge()
. Закон Деметры говорит, что метод класса должен вызывать только методы:
- Сам
- Его поля
- Его параметры
- Объекты, которые он создает.
(Последнее может быть проблемой для модульного тестирования, где вы можете захотеть иметь объекты, которые были введены или созданы на фабриках, а не создаются непосредственно локально, но не относятся к Закону Деметры.)
Чтобы устранить проблему с помощью hasUnderageDrivers()
, мы можем передать объект Policy
, и у нас может быть метод на Policy
, который знает, как определить, имеет ли политика несовершеннолетние драйверы:
public boolean hasUnderageDrivers(Policy policy) {
return policy.hasUnderageDrivers();
}
Вызов одного уровня вниз, customer.getPolicy().hasUnderageDrivers()
, вероятно, хорошо -— Закон Деметры - это эмпирическое правило, а не твердое правило. Вероятно, вам также не нужно беспокоиться о вещах, которые вряд ли будут меняться; Driver
, вероятно, всегда будет иметь дату рождения и метод getAge()
.
Но вернувшись к вашему делу, что произойдет, если мы заменим все эти геттеры на публичные поля? Это совсем не помогает закону Деметры. У вас все еще может быть такая же проблема, как в первом примере. Рассмотрим:
public boolean hasUnderageDrivers(Customer customer) {
for (Vehicle vehicle : customer.policy.vehicles) {
for (Driver driver : vehicle.drivers) {
if (driver.age < 18) {
return true;
}
}
}
return false;
}
(Я даже преобразовал driver.getAge()
в driver.age
, хотя, вероятно, это будет расчет, основанный на дате рождения, а не простое поле.)
Обратите внимание, что при написании кода с публичными полями, а не с геттерами, возникает одна и та же проблема с вложением знаний о том, как объединяется граф объекта (у клиента есть политика, в которой есть транспортные средства с драйверами). Проблема связана с тем, как элементы собраны вместе, а не с тем, вызываются ли геттеры.
Кстати, нормальная причина предпочитать геттеры над (окончательными?) общедоступными полями состоит в том, что вам может понадобиться позже добавить некоторую логику. Возраст заменяется расчетом, основанным на дате рождения и сегодняшней дате, или сеттер должен иметь некоторую проверку (бросает, если вы передаете, например, null
), связанные с ней. Я еще не слышал, что Закон Деметры был использован в этом контексте.
Ответ 3
Закон Деметры о работе с объектами, а не структурой данных, в вашем случае DTO, насколько я понимаю.
Закон Деметры объясняет, что вы можете вызывать методы объектов, которые:
- Передано как аргументы
- Скрыто локально внутри метода
- Переменные экземпляра (поля объекта)
- Global
Модели данных представляют собой контейнеры с некоторыми данными внутри них, которые должны отображаться снаружи. Это их роль, и кроме этого у них нет другого поведения.