Закон Деметры с объектами модели данных

Вчера я вернулся на работу из отпуска, и в нашем ежедневном выступлении мои товарищи по команде упоминали, что они рефакторировали все объекты модели в нашем 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

Модели данных представляют собой контейнеры с некоторыми данными внутри них, которые должны отображаться снаружи. Это их роль, и кроме этого у них нет другого поведения.