Закон Деметры в API Design для С++ - программирование
Подтвердить что ты не робот

Закон Деметры в API Design для С++

В своей книге "API Design for С++" Мартин Редди подробно описывает Закон Деметры. В частности, он утверждает, что:

вы никогда не должны вызывать функцию на объекте, полученном с помощью другого вызова функции.

Он поддерживает его утверждение с помощью функции цепочки, например

Func()
{
    [...]
    m_A.GetObjectB().DoSomething();
    [...]
}

Вместо этого он рекомендует передать B в качестве аргумента функции вроде:

Func(const ObjectB &B)
{
    [...]
    B.DoSomething();
    [...]
}

Мой вопрос:, почему последний пример приведет к созданию более слабо связанных классов, чем первый?

4b9b3361

Ответ 1

Аналогия, часто используемая (в том числе на странице Википедии, я замечаю), заключается в том, чтобы просить собаку ходить - вы спросите собаку, вы не просите о доступе к ее ногам, а затем попросите ее ноги ходить.

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

В вашем конкретном примере реализация 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-методы, традиционно объявляющие исключения, которые они могут бросить. Это означает, что они также должны перечислять исключения, которые все, что они называют, могут бросать, если они не поймают его. Поэтому каждый раз, когда метод leaf добавляет другое исключение, вам нужно подойти к дереву вызовов, добавив это исключение в целую группу списков. Таким образом, хорошая идея развязки заканчивается созданием бесконечных документов.

Во втором пункте я думаю, что мы блуждаем в "есть" против "имеет" дебаты. "Есть" - это очень естественный способ выразить некоторые объектные отношения и догматически скрывать это за фасадом "У меня есть ключи от шкафчика, поэтому, если вы хотите, чтобы ваш шкафчик открылся, просто приходите и спрашивайте меня, и я его разблокирую" разговоры просто скрывают поставленную задачу.

Ответ 2

Разница выделяется немного больше, когда вы смотрите на модульные тесты.

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

В первом случае для замены DoSomething() в вашем тесте вам нужно подделать как ObjectA, так и ObjectB и ввести поддельный экземпляр ObjectA в класс, содержащий Func().

Во втором случае вы вызываете Func() с фальшивым экземпляром ObjectB, что значительно упрощает тест.

Ответ 3

Это более гибко для изменений. Представьте, что m_A является экземпляром объекта A, разработанного программистом Бобом. Если он решит внести изменения в свой код, так что A больше не имеет метода для возврата объекта типа B, то Алисе, разработчику Func, тоже пришлось бы изменить свой код. Обратите внимание, что у вас нет этой проблемы с последним фрагментом кода.

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

Ответ 4

Чтобы прямо ответить на ваш вопрос:

Версия 2 создает более свободно связанные классы, потому что Func в первом случае зависит как от интерфейса класса m_A, так и от класса возвращаемого типа GetObjectB (предположительно ObjectB), тогда как в во втором случае это зависит только от интерфейса класса ObjectB.

То есть в первом случае существует связь между классом m_A и Func, во втором случае нет. Если интерфейс этого класса должен когда-либо изменяться, чтобы не иметь GetObjectB(), но, например, иметь GetFirstObjectB() и GetSecondObjectB(), в первом случае вам придется переписать Func, чтобы вызвать соответствующую функцию замены (и, возможно, даже добавить некоторую логику, которую можно вызвать, возможно, на основе дополнительного аргумента функции) в то время как во второй версии вы можете оставить функцию такой, как есть, и позволить пользователям Func заботиться о том, как получить этот объект типа ObjectB.

Ответ 5

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

Func();

По существу не сообщая вам ничего о функции. Второй предложенный метод вызывает функцию с переданным ей B, что не только делает ее более читаемой, но и означает, что вы можете написать Func() для других классов без ее переименования (поскольку, если она не принимает никаких параметров, вы не можете переписать его для другого класса). Это говорит о том, что Func() будет делать подобные вещи с объектом, даже если класс отличается.

Чтобы ответить на последнюю часть вашего вопроса, достигается свободное соединение, потому что из первого примера следует, что вам нужно получить B через A, который объединяет классы, второй пример является более общим и подразумевает, что B может прийти откуда угодно.