Рельсы: Закон Путаницы Деметры

13

Я читаю книгу под названием Rails AntiPatterns, и они говорят об использовании делегирования, чтобы избежать нарушения закона Деметры. Вот их главный пример:

Они считают, что вызывать что-то подобное в контроллере плохо (и я согласен)

@street = @invoice.customer.address.street

Их предлагаемое решение заключается в следующем:

class Customer

    has_one :address
    belongs_to :invoice

    def street
        address.street
    end
end

class Invoice

    has_one :customer

    def customer_street
        customer.street
    end
end

@street = @invoice.customer_street

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

http://www.dan-manges.com/blog/37

В блоге ярким примером является

class Wallet
  attr_accessor :cash
end
class Customer
  has_one :wallet

  # attribute delegation
  def cash
    @wallet.cash
  end
end

class Paperboy
  def collect_money(customer, due_amount)
    if customer.cash < due_ammount
      raise InsufficientFundsError
    else
      customer.cash -= due_amount
      @collected_amount += due_amount
    end
  end
end

В сообщении блога говорится, что, хотя customer.cashвместо него есть только одна точка customer.wallet.cash, этот код все еще нарушает Закон Деметры.

Теперь в методе Paperboy collect_money у нас нет двух точек, у нас есть только одна в «customer.cash». Эта делегация решила нашу проблему? Не за что. Если мы посмотрим на поведение, разносчик газет все еще лезет прямо в кошелек клиента, чтобы получить наличные.

РЕДАКТИРОВАТЬ

Я полностью понимаю и согласен с тем, что это все еще является нарушением, и мне нужно создать метод в Walletвызываемом выводе, который обрабатывает платеж для меня, и что я должен вызвать этот метод внутри Customerкласса. Что я не понимаю, так это то, что в соответствии с этим процессом мой первый пример все еще нарушает Закон Деметры, потому что Invoiceон все еще идет прямо Customerк улице.

Может кто-нибудь помочь мне устранить путаницу. Я искал последние 2 дня, пытаясь раскрыть эту тему, но это все еще сбивает с толку.

user2158382
источник
2
Подобный вопрос здесь
Торстен Мюллер
Я не думаю, что второй пример (газетчик) из блога нарушает закон Деметры. Это может быть плохой дизайн (вы предполагаете, что клиент заплатит наличными), но это НЕ нарушение закона Деметры. Не все ошибки дизайна вызваны нарушением этого закона. Автор смущен ИМО.
Андрес Ф.

Ответы:

24

Ваш первый пример не нарушает закон Деметры. Да, с помощью кода в том виде, в каком он есть, он говорит, что @invoice.customer_streetдействительно получает то же значение, что и гипотетический @invoice.customer.address.street, но на каждом шаге обхода возвращаемое значение определяется запрашиваемым объектом - это не значит, что «разносчик газет достигает кошелек клиента ", это то, что" разносчик запрашивает у клиента наличные, а клиент получает деньги из своего кошелька ".

Когда вы говорите @invoice.customer.address.street, вы предполагаете знание клиента и адреса внутренних органов - это плохо. Когда вы говорите @invoice.customer_street, вы спрашиваете invoice: «Эй, я бы хотел улицу клиента, вы сами решаете, как вы ее получите ». Затем клиент говорит по своему адресу: «Эй, я бы хотел, чтобы на твоей улице ты сам решил, как ты ее получишь ».

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

Я согласен, что это может показаться тонким различием, но учтите это: в коде, совместимом с Demeter, сколько кода нужно изменить, когда внутреннее представление addressизменений меняется? Как насчет кода, не совместимого с Demeter?

AakashM
источник
Это именно то объяснение, которое я искал! Спасибо.
user2158382
Очень хорошее объяснение. У меня есть вопросы: 1) Если объект счета хочет вернуть клиентский объект клиенту счета, это не обязательно означает, что это тот же объект клиента, который он содержит внутри. Это может быть просто объект, созданный на лету, с целью возврата клиенту хорошего упакованного набора данных с несколькими значениями в нем. Используя представленную логику, вы говорите, что в счете-фактуре не может быть поля, представляющего более одной информации. Или я что-то упустил.
zumalifeguard
2

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

Делегирование - это эффективный метод, позволяющий избежать нарушений закона Деметры, но только для поведения, а не для атрибутов. - из второго примера, блог Дэна

Опять же, « только для поведения, а не для атрибутов »

Если вы спрашиваете об атрибутах, вы должны спросить . «Эй, парень, сколько у тебя денег в кармане? Покажи мне, я оценю, сможешь ли ты заплатить это». Это неправильно, ни один продавец не будет вести себя так. Вместо этого они скажут: «Пожалуйста, заплатите»

customer.pay(due_amount)

Заказчик будет обязан самостоятельно оценить, должен ли он платить и может ли он платить. И задача клерка закончена после того, как он сказал клиенту заплатить.

Итак, второй пример доказывает, что первый неверен?

По моему мнению. Нет , пока:

1. Вы делаете это с самоограничением.

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

Подумайте о странице, показывающей счет в приложении Rails. Там будет раздел сверху, чтобы показать детали клиента. Так, в шаблоне счета, вы будете кодировать как это?

#customer-info
  = @invoice.customer_name
  = @invoice.customer_address
  ....

Это неправильно и неэффективно. Лучший подход

#customer-info
  = render partial: 'invoice_header_customer', 
           locals: {customer: @invoice.customer}

Затем пусть клиент частично обработает все атрибуты, принадлежащие клиенту.

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

= @invoice.customer_name

2. Дальнейших действий в зависимости от вызова этого метода нет.

В приведенном выше случае со страницей списка в счете-фактуре запрашивался атрибут имени клиента, но его реальная цель - « покажи мне свое имя », так что это в основном поведение, а не атрибут . Нет никаких дальнейших оценок и действий, основанных на этом атрибуте, например, если вас зовут «Майк», я вас полюблю и предоставлю вам еще 30 дней кредита. Нет, счет просто скажи "покажи мне свое имя", не более. Так что это вполне приемлемо в соответствии с правилом «Не спрашивай» в примере 2.

Билли Чан
источник
0

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

class Wallet
  attr_accessor :cash
  def withdraw(amount)
     raise InsufficientFundsError if amount > cash
     cash -= amount
     amount
  end
end
class Customer
  has_one :wallet
  # behavior delegation
  def pay(amount)
    @wallet.withdraw(amount)
  end
end
class Paperboy
  def collect_money(customer, due_amount)
    @collected_amount += customer.pay(due_amount)
  end
end

Поэтому я думаю, что ваш второй отзыв дает более полезную рекомендацию.

Идея «единой точки» является частичным успехом, поскольку она скрывает некоторые глубокие детали, но все же увеличивает связь между отдельными компонентами.

DJNA
источник
Извините, может быть, я не был ясен, но я прекрасно понимаю второй пример и понимаю, что вам нужно создать абстракцию, которую вы опубликовали, но то, что я не понимаю, это мой первый пример. Согласно сообщению в блоге, мой первый пример неверен
user2158382
0

Похоже, Дэн извлек свой пример из этой статьи: «Бумажник», «Кошелек» и «Закон Деметры»

Закон Деметры. Метод объекта должен вызывать только методы следующих типов объектов:

  1. сам
  2. его параметры
  3. любые объекты, которые он создает / создает
  4. его прямые составляющие объекты

Когда и как применять закон Деметры

Итак, теперь вы хорошо разбираетесь в законе и его преимуществах, но мы еще не обсуждали, как определить места в существующем коде, где мы можем его применять (и не менее важно, где НЕ применять его ...)

  1. Цепные операторы get - Первое, наиболее очевидное место применения Закона Деметры - это места кода, которые имеют повторяющиеся get() утверждения,

    value = object.getX().getY().getTheValue();

    как если бы наш канонический человек для этого примера был остановлен полицейским, мы могли бы видеть:

    license = person.getWallet().getDriversLicense();

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

    Wallet tempWallet = person.getWallet(); license = tempWallet.getDriversLicense();

    это эквивалентно, но сложнее обнаружить.

  3. Импорт многих классов. В проекте Java, над которым я работаю, есть правило, согласно которому мы импортируем только те классы, которые фактически используем; вы никогда не видите что-то вроде

    import java.awt.*;

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

Я понимаю, что ваш пример написан на Ruby, но это должно применяться ко всем языкам ООП.

Мистер Поливирл
источник