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

Вчера я вернулся на работу из отпуска, и в нашей ежедневной встрече мои товарищи по команде упомянули, что они рефакторят все объекты модели в нашем коде Java, чтобы удалить все геттеры и сеттеры и вместо этого сделать поля модели всеми публичными объектами, ссылаясь на Закон Деметра как причина для этого, потому что

чтобы облегчить наше соблюдение закона Деметры: модуль не должен знать о внутренностях «объектов», которыми он манипулирует. Поскольку структуры данных не содержат поведения, они естественным образом раскрывают свою внутреннюю структуру. Так что в этом случае Деметра не применяется.

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

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

Итак, мой вопрос: имеет ли смысл раскрывать внутреннюю структуру объектов модели напрямую, а не через геттеры/сеттеры во имя LoD?


person alexD    schedule 24.09.2014    source источник
comment
Похоже, вашим товарищам по команде не хватает работы.   -  person Sneftel    schedule 24.09.2014
comment
Да, в том-то и проблема... у нас куча работы, и это огромный рефакторинг, затрагивающий около 100 файлов. Я собираюсь отступить, и я просто хочу убедиться, что у меня есть нечто большее, чем просто пустая трата времени (хотя этого должно быть достаточно)   -  person alexD    schedule 24.09.2014
comment
К счастью, вы не используете Groovy, где x.getStuff() и x.stuff неразличимы (и где поле stuff может даже не существовать во время вызова...)   -  person David Tonhofer    schedule 21.02.2016
comment
@alexD: Откуда взята цитата в вопросе?   -  person Lii    schedule 20.04.2017


Ответы (3)


Об этом рассказывается в книге Роберта Мартина «Чистый код».

В главе 6 (Объекты и структуры данных) он говорит о фундаментальных различиях между объектами и структурами данных. Объекты выигрывают от инкапсуляции, а структуры данных — нет.

Есть раздел о Законе Деметры:

Существует хорошо известная эвристика под названием Закон Деметры, согласно которой модуль не должен знать о внутренности объектов, которыми он манипулирует. Как мы видели в предыдущем разделе, объекты скрывают свои данные и раскрывают операции. Это означает, что объект не должен раскрывать свою внутреннюю структуру через средства доступа, потому что это означает раскрытие, а не сокрытие своей внутренней структуры.

Точнее, Закон Деметры говорит, что метод f класса C должен вызывать только методы этих:

  • C
  • Объект, созданный f
  • Объект, переданный в качестве аргумента функции f
  • Объект, хранящийся в переменной экземпляра C

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

Дядя Боб приводит пример нарушения LoD:

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

Является ли это нарушением Demeter, зависит от того, являются ли ctxt, Options и ScratchDir объектами или структурами данных. Если они объекты, то их внутреннюю структуру следует скрывать, а не выставлять напоказ, и поэтому знание их внутренностей является явным нарушением Закона Деметры. С другой стороны, если ctxt, Options и ScratchDir являются просто структурами данных без какого-либо поведения, то они, естественно, раскрывают свою внутреннюю структуру, и поэтому Деметра не применяется.

Использование функций доступа запутывает проблему. Если бы код был написан следующим образом, то мы, вероятно, не спрашивали бы о нарушениях Деметры.

final String outputDir = ctxt.options.scratchDir.absolutePath;

Так что это, вероятно, то, откуда пришли ваши коллеги. Я думаю, что аргумент «мы должны сделать это, потому что LoD» в лучшем случае неточен. Главный вопрос не столько в LoD, сколько в том, состоит ли API из объектов или структур данных. Это кажется ненужным и чреватым ошибками изменением, если есть более неотложные дела.

person Nathan Hughes    schedule 24.09.2014
comment
Имеет смысл... на его столе лежит копия "Чистого кода". Кажется, он ее недавно читал :) - person alexD; 24.09.2014
comment
Я не согласен. Я думаю, это все еще проблема. Подумайте об этом так: если вам нужно протестировать класс с помощью outputDir, действительно ли вы хотите создать экземпляр всего дерева объектов — ctxt, options иcratchDir? Отличный рассказ об этом — youtube.com/watch?v=RlfLCWKxHJ0. - person alexandroid; 19.06.2015
comment
Различие Роберта Мартина между объектами и структурами данных звучит неуместно. Объекты — это структуры данных, структуры данных — это объекты. Рефакторинг кода, а также взгляд на код с другой точки зрения превратят одно в другое. Возможно, если под структурами данных понимаются типы контейнеров, такие как Map, List, Set и т. д. - person David Tonhofer; 21.02.2016
comment
@David: я думаю, что Мартин говорит о том, что иногда инкапсуляция полезна, а иногда нет, а когда она бесполезна, он старается не называть это объектом. В то время, когда Мартин писал эту книгу, он погружался в парадигму FP, и, похоже, он стал лучше понимать, что ООП в некоторых местах более актуален, чем в других, чего я не замечал в более ранних работах. - person Nathan Hughes; 21.02.2016

Мне не кажется, что это изменение имеет какое-либо отношение к Закону Деметры. Закон, по сути, заключается в том, чтобы закодировать структуру вашего графа объектов в ваш код, вызывая методы через всю цепочку других объектов. Например, предположим, что в приложении для автострахования у клиента есть полис, а у полиса есть транспортные средства, у транспортных средств есть назначенные им водители, а у водителей есть даты рождения и, следовательно, возраст. Вы можете представить себе следующий код:

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), связанную с ним. Я никогда раньше не слышал, чтобы Закон Деметры упоминался в таком контексте.

person David Conrad    schedule 24.09.2014
comment
У меня есть вопрос относительно вашего (отличного) примера политики. Разве вы не получите много кода в своем объекте политики. Поскольку каждый, кто хочет что-то узнать о страховых договорах, должен сначала спросить об объекте полиса? - person markus; 20.02.2015
comment
@markus Это правда, вы можете получить много кода в классе политики. Вы также можете столкнуться с множеством маленьких методов пересылки, если метод hasUnderageDrivers в Policy просто передает ответственность методу с таким же именем в некотором объекте коллекции Vehicles. - person David Conrad; 02.03.2015
comment
@markus Википедия упомяните это: На уровне метода LoD приводит к узкие интерфейсы, предоставляющие доступ только к такому объему информации, который необходим для выполнения его работы, поскольку каждый метод должен знать о небольшом наборе методов тесно связанных объектов. OTOH, на уровне класса LoD приводит к широким (т.е. расширенным) интерфейсам, потому что LoD требует введения множества вспомогательных методов вместо того, чтобы копаться непосредственно в структурах объекта. Одним из решений проблемы расширенных интерфейсов классов является аспектно-ориентированный подход... - person David Tonhofer; 21.02.2016

Закон Деметры о работе с объектами, а не со структурой данных, в вашем случае DTO как я понял.

Закон Деметры объясняет, что вы можете вызывать методы объектов, которые:

  1. Передается как аргумент
  2. Очищается локально внутри метода
  3. Переменные экземпляра (поля объекта)
  4. Глобальный

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

person Dmytro Melnychuk    schedule 25.06.2017