В своей книге «API Design for C ++» Мартин Редди подробно описывает закон Деметры. В частности, он заявляет, что:
Вы никогда не должны вызывать функцию для объекта, который вы получили посредством другого вызова функции.
Он поддерживает свое утверждение с помощью вызовов функции цепочки, таких как
Func()
{
[...]
m_A.GetObjectB().DoSomething();
[...]
}
Вместо этого он рекомендует передавать B в качестве аргумента функции, такой как:
Func(const ObjectB &B)
{
[...]
B.DoSomething();
[...]
}
Мой вопрос: почему последний пример будет производить более слабосвязанные классы, чем первый?
Часто используемая аналогия (в том числе на странице Википедии, я замечаю) заключается в том, что вы просите собаку выгуливать — вы спрашиваете собаку, вы не просите доступа к ее ногам, а затем просите ее ноги ходить.
Просить собаку выгуливать лучше, потому что однажды вы захотите собаку с чем-то другим, кроме ног.
В вашем конкретном примере m_A
реализация может перестать зависеть от случая B
,
РЕДАКТИРОВАТЬ: так как некоторые люди хотят дальнейшего изложения, позвольте мне попробовать это:
Если объект X
содержит утверждение m_A.GetObjectB().DoSomething()
затем X
должен знать:
m_A
имеет экземпляр объекта B
выставлено через GetObject()
; а такжеB
имеет метод DoSomething()
,Так X
нужно знать интерфейсы A
а также B
, а также A
всегда должен быть в состоянии продать B
,
И наоборот, если X
просто нужно было сделать m_A.DoSomething()
тогда все, что нужно знать, это:
m_A
имеет метод DoSomething()
,Таким образом, закон помогает отделить, потому что X
теперь полностью отделен от B
— он не должен иметь никаких знаний об этом классе — и имеет меньше знаний о A
— это знает, что A
может достичь DoSomething()
но ему больше не нужно знать, делает ли он это сам или просит другого сделать это.
На практике закон часто не используется, потому что он обычно означает написание сотен функций-оболочек, таких как A::DoSomething() { m_B.DoSomething(); }
и формальная семантика вашей программы часто явно A
будет иметь B
так что вы не так много раскрываете детали реализации, предоставляя GetObjectB()
поскольку вы просто выполняете контракт этого объекта с системой в целом.
Первый пункт также может быть использован, чтобы утверждать, что закон увеличивает сцепление. Предположим, вы изначально имели m_A.GetObjectB().GetObjectC().GetObjectD().DoSomething()
и ты рухнул бы до m_A.DoSomething()
, Это означает, что, потому что C
знает что D
инвентарь DoSomething()
, C
должен реализовать это. Тогда потому что B
теперь знает, что C
инвентарь DoSomething()
, B
должен реализовать это. И так далее. В конце концов у вас есть A
необходимо реализовать DoSomething()
так как D
делает. Так A
в конечном итоге приходится действовать определенным образом, потому что D
действует определенным образом, тогда как раньше он мог не знать D
бы то ни было.
С первой точки зрения похожая ситуация — методы Java, традиционно объявляющие исключения, которые они могут выдавать. Это означает, что они также должны перечислить исключения, которые может вызвать все, что они вызывают, если они не поймают это. Таким образом, каждый раз, когда листовой метод добавляет другое исключение, вам нужно пройтись по дереву вызовов, добавив это исключение в целую кучу списков. Таким образом, хорошая идея развязки в конечном итоге создает бесконечные документы.
По второму вопросу, я думаю, мы отклоняемся от темы «есть» против «есть». «Имеет» — это очень естественный способ выразить некоторые объектные отношения и догматически скрывать это за фасадом «У меня есть ключи от шкафчика, поэтому, если вы хотите, чтобы ваш шкафчик открылся, просто подойдите и спросите меня, и я его разблокирую» разговоры просто затеняют задачу под рукой.
Разница заметно больше, когда вы смотрите на юнит-тесты.
Предполагать DoSomething()
имеет побочный эффект, который вы не хотите, чтобы в вашем тестовом коде происходило, потому что имитировать было бы дорого или раздражающе, например, доступ к базе данных или сетевое взаимодействие.
В первом случае для того, чтобы заменить DoSomething()
в вашем тесте вы должны подделать оба ObjectA
а также ObjectB
и ввести поддельный экземпляр ObjectA
в класс, содержащий Func()
,
Во втором случае вы просто позвоните Func()
с подделкой ObjectB
экземпляр, который значительно упрощает тест.
Он более гибок к изменениям. Представь это m_A
это экземпляр объекта A
, разработанный программистом Бобом. Если он решит внести изменения в свой код, чтобы A
больше не имеет метода для возврата объекта типа B
Затем Алиса, разработчик Func
, придется изменить ее код тоже. Обратите внимание, что у вас нет этой проблемы с последним фрагментом кода.
В разработке программного обеспечения этот тип связи приводит к тому, что называется неортогональная дизайн, вид дизайна, где вы меняете локальную часть кода где-то, и вам нужно менять части и в других местах.
Чтобы прямо ответить на ваш вопрос:
Версия 2 производит более слабосвязанные классы, потому что Func
в первом случае зависит как интерфейс класса m_A
и класс возвращаемого типа GetObjectB
(предположительно ObjectB
), а во втором случае это зависит только от интерфейса класса ObjectB
,
То есть, в первом случае, есть связь между m_A
Класс и Func
во втором случае нет. Если интерфейс этого класса должен измениться, чтобы не иметь GetObjectB()
но, например, иметь GetFirstObjectB()
а также GetSecondObjectB()
, в первом случае вам придется переписать Func
вызвать соответствующую функцию замены (и, возможно, даже добавить некоторую логику, которую нужно вызвать, возможно, на основе дополнительного аргумента функции), в то время как во второй версии вы можете оставить функцию как есть и позволить пользователям Func
заботиться о том, как получить этот объект типа ObjectB
,
Что ж, я думаю, что должно быть очевидно, почему объединение функций в единое целое плохо, так как это приводит к тому, что код становится труднее поддерживать. В верхнем примере Func()
это уродливая функция, потому что кажется, что она будет просто вызываться как
Func();
По сути, ничего не говорю о функции. Второй предложенный метод вызывает функцию с B
передается к нему, что не только делает его более читабельным, но означает, что вы можете написать Func()
для других классов без переименования (поскольку, если он не принимает параметров, вы не можете переписать его для другого класса). Это говорит вам, что Func()
будет делать подобные вещи с объектом, даже если класс отличается.
Чтобы ответить на последнюю часть вашего вопроса, слабая связь достигается, потому что первый пример подразумевает, что вы должны получить B
через A
который объединяет классы, второй пример является более общим и подразумевает, что B может прийти откуда угодно.