Статьи

Закон Деметры

Уменьшите сцепление и улучшите герметизацию…

Общее
В этом посте я хочу перейти к  Закону Деметры  ( LoD ).
Я считаю эту тему чрезвычайно важной для того, чтобы код был чистым, хорошо продуманным и обслуживаемым.

По моему опыту, видеть его сломанным — огромный запах плохого дизайна.
Следование закону или рефакторинг на его основе приводит к значительному улучшению, удобочитаемому и более понятному коду.

Так что же такое закон Деметры?
Я начну с упоминания 4 основных правил:

Закон Деметры говорит, что метод M объекта O может обращаться к / вызывать методы:

  1. О себе
  2. Входные аргументы М
  3. Любой объект, созданный в М
  4. Параметры / зависимости O

Это довольно простые правила.

Давайте выразим это другими словами:
Each unit (method) should have limited knowledge about other units.

Метафоры
Наиболее распространенным является:  не разговаривайте с незнакомцами

Как насчет этого:
предположим, я что-то покупаю в 7-11.
Когда мне нужно будет заплатить, передам ли я свой кошелек служащему, чтобы она открыла его и вытащила деньги?
Или я дам ей деньги напрямую?

Как насчет этой метафоры:
когда вы выводите собаку на прогулку, вы говорите  ей  ходить или ее ногам?

Почему мы хотим следовать этому правилу?

  • Мы можем изменить класс, не имея волнового эффекта изменения многих других.
  • Мы можем изменить вызываемые методы без изменения чего-либо еще.
  • Использование LoD значительно упрощает построение наших тестов. Нам не нужно писать так много « когда » для насмешек, которые возвращаются, возвращаются и возвращаются.
  • Это улучшает инкапсуляцию и абстракцию (я покажу в примере ниже).
    Но в основном мы скрываем «как все работает».
  • Это делает наш код менее связанным. Метод вызывающей стороны связан только в одном объекте, а не во всех внутренних зависимостях.
  • Обычно он лучше моделирует реальный мир.
    Возьмите в качестве примера кошелек и оплату.

Считать точки?
Хотя обычно многие  точки  подразумевают нарушение LoD, иногда нет смысла «объединять точки». Предполагает
ли:
getEmployee().getChildren().getBirthdays()
что мы делаем что-то вроде
getEmployeeChildrenBirthdays() 😕
Я не совсем уверен.

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

Как всегда, мы должны иметь здравый смысл при кодировании, очистке и / или рефакторинге.

Пример
Предположим, у нас есть класс:  Item
Элемент может содержать несколько атрибутов.
Каждый атрибут имеет имя и значения (это атрибут с несколькими значениями)

Простейшие реализации будут использовать Map.

public class Item {
  private final Map<String, Set<String>> attributes;
  public Item(Map<String, Set<String>> attributes) {
    this.attributes = attributes;
  }
  public Map<String, Set<String>> getAttributes() {
    return attributes;
  }
}
Давайте иметь класс 
ItemsSaver,
 который использует Item и атрибуты:

(пожалуйста, игнорируйте неструктурированные методы. Это пример для LoD, а не SRP  :) )

public class ItemSaver {
  private String valueToSave;
  public ItemSaver(String valueToSave) {
    this.valueToSave = valueToSave;
  }
  public void doSomething(String attributeName, Item item) {
    Set<String> attributeValues = item.getAttributes().get(attributeName);
    for (String value : attributeValues) {
      if (value.equals(valueToSave)) {
        doSomethingElse();
      }
    }
  }
  private void doSomethingElse() {
  }
}

Предположим, я знаю, что это одно значение (из контекста приложения).
И я хочу взять это. Тогда код будет выглядеть так:

Set<String> attributeValues = item.getAttributes().get(attributeName);
String singleValue = attributeValues.iterator().next();
// String singleValue = item.getAttributes().get(attributeName).iterator().next();
Я думаю, что ясно, что у нас проблемы.

Где бы мы ни использовали атрибуты Предмета, мы знаем, как он работает. Мы знаем внутреннюю реализацию этого.
Это также делает наш тест намного сложнее поддерживать.

Давайте рассмотрим пример теста с использованием mock (Mockito):
вы можете представить себе, сколько нужно усилий, чтобы изменить и сохранить его.

Item item = mock(Item.class);
Map<String, Set<String>> attributes = mock(Map.class);
Set<String> values = mock(Set.class);
Iterator<String> iterator = mock(Iterator.class);
when(iterator.next()).thenReturn("the single value");
when(values.iterator()).thenReturn(iterator);
when(attributes.containsKey("the-key")).thenReturn(true);
when(attributes.get("the-key")).thenReturn(values);
when(item.getAttributes()).thenReturn(attributes);

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

Подведем итоги:

  • Мы раскрыли внутреннюю реализацию того, как Item хранит атрибуты
  • Чтобы использовать атрибуты, нам нужно было спросить элемент, а затем запросить внутренние объекты (значения).
  • Если мы когда-нибудь захотим изменить реализацию атрибутов, нам нужно будет внести изменения в классы, которые используют Item и атрибуты. Наверное, много классов.
  • Построение теста утомительно, громоздко, подвержено ошибкам и требует много обслуживания.

Улучшение
Первым улучшением было бы попросить Item делегировать атрибуты.

public class Item {
  private final Map<String, Set<String>> attributes;
  public Item(Map<String, Set<String>> attributes) {
    this.attributes = attributes;
  }
  public boolean attributeExists(String attributeName) {
    return attributes.containsKey(attributeName);
  }
  public Set<String> values(String attributeName) {
    return attributes.get(attributeName);
  }
  public String getSingleValue(String attributeName) {
    return values(attributeName).iterator().next();
  }
}

И тест становится намного проще.

Item item = mock(Item.class);
when(item.getSingleValue("the-key")).thenReturn("the single value");
Мы (почти) полностью скрываем реализацию атрибутов от других классов.

Классы клиента не знают о реализации, ожидают два случая:

  1. Элемент все еще знает, как строятся атрибуты.
  2. Класс, который создает Item (какой бы он ни был), также знает реализацию атрибутов.

Две вышеупомянутые точки означают, что если мы изменим реализацию Атрибутов (что-то еще, кроме карты), потребуется изменить как минимум два других класса. Это отличный пример для High Coupling .

Улучшение следующего шага
Приведенного выше решения иногда (обычно?) Будет достаточно.
Как прагматичные программисты, мы должны знать, когда остановиться.
Однако давайте посмотрим, как мы можем даже улучшить первое решение.

Создайте атрибуты класса  :

public class Attributes {
  private final Map<String, Set<String>> attributes;
  public Attributes() {
    this.attributes = new HashMap<>();
  }
  public boolean attributeExists(String attributeName) {
    return attributes.containsKey(attributeName);
  }
  public Set<String> values(String attributeName) {
    return attributes.get(attributeName);
  }
  public String getSingleValue(String attributeName) {
    return values(attributeName).iterator().next();
  }
  public Attributes addAttribute(String attributeName, Collection<String> values) {
    this.attributes.put(attributeName, new HashSet<>(values));
    return this;
  }
}

И пункт, который его использует:

public class Item {
  private final Attributes attributes;
  public Item(Attributes attributes) {
    this.attributes = attributes;
  }
  public boolean attributeExists(String attributeName) {
    return attributes.attributeExists(attributeName);
  }
  public Set<String> values(String attributeName) {
    return attributes.values(attributeName);
  }
  public String getSingleValue(String attributeName) {
    return attributes.getSingleValue(attributeName);
  }
}
(Вы заметили? Реализация атрибутов внутри элемента была изменена, но тесту это не понадобилось. Это благодаря небольшому изменению делегирования.)

Во втором решении мы улучшили инкапсуляцию атрибутов.
Теперь даже Item не знает, как это работает.
Мы можем изменить реализацию Атрибутов, не касаясь других классов.
Мы можем сделать различные реализации Атрибутов:
— Реализация, которая содержит Набор значений (как в примере).
— Реализация, которая содержит список значений.
— Совершенно другая структура данных, о которой мы можем думать.

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

Что мы получили?

  • Код намного удобнее в обслуживании.
  • Тесты проще и удобнее в обслуживании.
  • Это гораздо более гибкий. Мы можем изменить реализацию Атрибутов (карта, набор, список, что бы мы ни выбрали).
  • Изменения в атрибуте не влияют ни на какую другую часть кода. Даже те, кто его использует.
  • Модуляризация и повторное использование кода. Мы можем использовать   класс Attributes в других местах кода.