Эта проблема

Предположим, что у нас есть три компонента: A, B и C.

Другой компонент, Main, зависит от A и хочет вызвать метод foo, принадлежащий компоненту C:

a.getB().getC().foo();

Предыдущий фрагмент эквивалентен:

B b = a.getB();
C c = b.getC();
c.foo();

Проблема с этим кодом заключается в том, что для вызова foo Main необходимо пройти через компоненты A и B, чтобы достичь C и, наконец, вызовите этот метод. Main необходимо знать все 3 компонента, а также их внутреннюю структуру, чтобы иметь возможность вызывать этот вызов.

Хотя наш пример слишком упрощен, мы могли бы сказать, что Main должен знать, какой метод вызывать, чтобы получить B из A, C из B и вызовите foo. Более того, промежуточные компоненты (B и C) должны предоставлять некоторый API для поддержки этих связанных вызовов. Понятно, что такие API меняют их природу. Компонент A не должен действовать как поставщик компонента B, но вместо этого должен иметь API, который оправдывает его существование. Проблема в том, что мы описали здесь нарушение Закона Деметры.

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

  • каждое подразделение должно иметь только ограниченные знания о других подразделениях: только подразделения, «тесно» связанные с текущим подразделением.
  • каждый юнит должен разговаривать только со своими друзьями; не разговаривай с незнакомцами.
  • разговаривайте только со своими ближайшими друзьями.

В ООП мы узнаем, что объекты взаимодействуют друг с другом, передавая сообщения, поэтому вызов метода на самом деле является сообщением, отправленным от вызывающего к вызываемому. Например, a.getB () просто означает, что Main отправляет сообщение A «с запросом» для B . Во втором фрагменте очевидно, что Main отвечает за отправку всех сообщений, оставляя промежуточные компоненты без какой-либо реальной логики, кроме предоставления их внутренних компонентов.

Теперь давайте подумаем, что нужно сделать Main. Ему нужно вызвать foo, но он знает только компонент A. Не нужно «искать» все остальные компоненты. Не нужно даже знать, что они существуют. Все, что ему нужно сделать, - это «сообщить» компоненту A, чтобы он нашел и вызвал метод foo. A, вероятно, делегирует эту задачу B, а B сделает что-то подобное с C. В этом случае компонент A «сообщает» B, что делать.

Преимущества

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

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

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

Закон Деметры в микросервисах

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

Рассмотрим в нашем примере, был ли каждый вызов getComponent () удаленным вызовом. Насколько хрупким будет Main, когда он выполняет 3 удаленных вызова к различным компонентам / службам? Конечно, даже если мы применим закон Деметры, по компонентам нужно будет передать 3 сообщения, чтобы получить результат foo (), но теперь Main сообщает только с 1 компонентом, который необходим. Как мы увидим в следующих параграфах, это изменение протокола связи дает очень большой выигрыш с точки зрения доступности и производительности.

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

Вызов a.bar () концептуально аналогичен отправке события с этим вызовом диспетчеру, где компонент A подписался на эти события. Этот дополнительный уровень косвенного обращения еще больше разделяет компоненты, поскольку вызывающему абоненту не нужно знать, как вызывать A, но тот факт, что обмен данными является асинхронным, означает, что в некоторых случаях вызывающему абоненту не нужно ждать для A, чтобы завершить обработку. В нашем примере, если бы foo () на самом деле был методом, отправляющим уведомление или электронное письмо, вызывающему абоненту не пришлось бы ждать завершения этой задачи.

Даже если мы используем асинхронную связь в нашем примере, при нарушении закона Деметры, все еще есть несколько проблем с дизайном из-за того, что события, которые отправляет Main, зависят друг от друга. Main запрашивает у компонента A значение B, чтобы иметь возможность запросить компонент B для C и т. д. Эта болтливость необходима для построения финального события. Main не может отправить событие вызова foo в C до того, как B вернет C (на самом деле B вернет местоположение C). Кроме того, если один из промежуточных компонентов / служб недоступен, Main не сможет создать последнее событие для отправки на C, поэтому Main не сможет завершить свой процесс и, в зависимости от бизнес-правил, может не выполнить свою задачу. В этом случае недоступность косвенных компонентов распространяется на Main.

Эта конструкция также имеет некоторые проблемы с производительностью. Main мог бы немедленно написать событие вызова foo и продолжить делать что-нибудь полезное вместо обнаружения других компонентов.

Все эти проблемы являются следствием того факта, что Main вместо «сообщать», что ему нужно, пытается найти способ выполнить большую часть своей задачи с помощью «Просить» другие компоненты собрать информацию. Можно сказать, что Main действует так, будто не доверяет другим компонентам!

Очевидно, что если мы спроектируем нашу систему таким образом, чтобы не нарушить закон Деметры, эти проблемы исчезнут почти бесплатно. Main отправит только 1 событие и продолжит выполнение своих задач, даже если другие компоненты недоступны. Поскольку Main отправляет только 1 событие, нет никаких дополнительных шагов для создания этого события, как это было раньше, поэтому протокол связи был оптимизирован. Очевидно, что доступность Main не зависит от доступности других компонентов (кроме диспетчера событий). Main выполнил свою задачу, просто «сообщив», что нужно сделать. Другие компоненты в конечном итоге получат событие от диспетчера, когда они снова станут доступны.

Мы должны проектировать наши компоненты так, чтобы они были как можно слабее связанными, и Закон Деметры - хороший способ двигаться в этом направлении.

Дальнейшее чтение:

Https://martinfowler.com/bliki/TellDontAsk.html
https://pragprog.com/articles/tell-dont-ask