Объяснение того, как «говори, не спрашивай» считается хорошим ОО

49

Этот пост был опубликован на Hacker News с несколькими отзывами. Исходя из C ++, большинство этих примеров, кажется, идут вразрез с тем, чему меня учили.

Например, пример № 2:

Плохо:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

по сравнению с хорошим:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

Совет в C ++ заключается в том, что вы должны отдавать предпочтение свободным функциям, а не функциям-членам, поскольку они увеличивают инкапсуляцию Оба они семантически идентичны, так почему же предпочитаете выбор, который имеет доступ к большему количеству состояний?

Пример 4:

Плохо:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

по сравнению с хорошим:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

Почему это является обязанностью Userформатировать несвязанную строку ошибки? Что если я захочу сделать что-то кроме печати, 'No street name on file'если на ней нет улицы? Что если улица названа так же?


Может ли кто-нибудь рассказать мне о преимуществах и объяснениях «говори, не спрашивай»? Я не ищу что лучше, а пытаюсь понять точку зрения автора.

Pubby
источник
Примеры кода могут быть Ruby, а не Python, я не знаю.
Пабби
2
Мне всегда интересно, не является ли что-то вроде первого примера скорее нарушением SRP?
Стийн
1
Вы можете прочитать это: pragprog.com/articles/tell-dont-ask
Mik378
Рубин. Например, @ является сокращением, а Python неявно завершает свои блоки пробелами.
Эрик Реппен
3
«Совет в C ++ заключается в том, что вы должны отдавать предпочтение свободным функциям, а не функциям-членам, поскольку они увеличивают инкапсуляцию». Я не знаю, кто тебе это сказал, но это неправда. Свободные функции могут использоваться для увеличения инкапсуляции, но они не обязательно увеличивают инкапсуляцию.
Роб К

Ответы:

81

Запрос объекта о его состоянии, а затем вызов методов для этого объекта на основе решений, принятых за пределами объекта, означает, что объект теперь является утечкой абстракции; часть его поведения находится за пределами объекта, а внутреннее состояние подвергается (возможно, излишне) внешнему миру.

Вы должны стараться рассказать объектам, что вы хотите, чтобы они делали; не задавайте им вопросов об их состоянии, примите решение, а затем скажите им, что делать.

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

Конечно, вы можете сказать, это очевидно. Я бы никогда не написал такой код. Тем не менее, очень легко увлечься проверкой какого-либо ссылочного объекта и последующим вызовом различных методов на основе результатов. Но это, возможно, не лучший способ сделать это. Скажите объект, что вы хотите. Пусть это выяснит, как это сделать. Думай декларативно, а не процедурно!

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

http://pragprog.com/articles/tell-dont-ask

Роберт Харви
источник
4
Текст примера запрещает многие вещи, которые явно являются хорошей практикой.
DeadMG
13
@DeadMG делает то, что вы говорите, только тем, кто рабски следует этому, кто слепо игнорирует «прагматичный» в названии сайта и ключевой мысли авторов сайта , которая была четко изложена в их ключевой книге: « лучшего решения не существует ... "
комнат
2
Никогда не читайте книгу. И я бы не хотел. Я читаю только текст примера, что совершенно справедливо.
DeadMG
3
@DeadMG не беспокойся. Теперь, когда вы знаете ключевой момент, который помещает этот пример (и любой другой из pragprog в этом отношении) в заданный контекст («нет такого понятия, как лучшее решение ...»), можно не читать книгу
комнат
1
Я до сих пор не уверен, что «Скажи, не спрашивай» должен говорить для тебя без контекста, но это действительно хороший совет ООП.
Эрик Реппен
16

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

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

Например, если система предоставляет в temperatureкачестве запроса, то завтра клиент может check_for_underheatingбез необходимости вносить изменения SystemMonitor. Это не тот случай, когда SystemMonitorреализует check_for_overheatingсам. Таким образом, SystemMonitorкласс, чья работа состоит в том, чтобы поднять тревогу, когда температура слишком высока, действительно следует этому, но SystemMonitorкласс, работа которого состоит в том, чтобы позволить другому коду кодировать температуру, чтобы он мог управлять, скажем, TurboBoost или чем-то подобным , не следует.

Также обратите внимание, что во втором примере бессмысленно используется анти-шаблон Null Object.

DeadMG
источник
19
«Нулевой объект» - это не то, что я бы назвал анти-паттерном, поэтому мне интересно, какова ваша причина для этого?
Конрад Рудольф
4
Уверен, что ни у кого нет методов, которые определены как "ничего не делает". Это делает их бессмысленными. Это означает, что любой объект, реализующий Null Object, по крайней мере нарушает LSP и описывает себя как реализацию операций, чего на самом деле нет. Пользователь ожидает возврата. От этого зависит правильность их программы. Вы просто приносите больше проблем, притворяясь, что это ценность, а она - нет. Вы когда-нибудь пытались отлаживать молча неудачные методы? Это невозможно, и никто никогда не должен страдать из-за этого.
DeadMG
4
Я бы сказал, что это полностью зависит от проблемной области.
Конрад Рудольф
5
@DeadMG Я согласен , что приведенный выше пример является плохим использованием шаблона объекта Null, но это заслуга в ее использовании. Несколько раз я использовал реализацию «no-op» того или иного интерфейса, чтобы избежать проверки на нуль или наличия истинного «ноля», пронизывающего систему.
Макс
6
Не уверен, что я вижу вашу точку зрения «клиент может check_for_underheatingбез необходимости менять SystemMonitor». Чем клиент отличается от SystemMonitorэтого? Разве вы сейчас не распределяете свою логику мониторинга по нескольким классам? Я также не вижу проблемы с классом монитора, который предоставляет сенсорную информацию другим классам, сохраняя при этом функции сигнализации для себя. Контроллер наддува должен контролировать наддув без необходимости беспокоиться о срабатывании сигнализации, если температура становится слишком высокой.
TMN
9

Реальная проблема с вашим примером перегрева заключается в том, что правила для того, что квалифицируется как перегрев, нелегко варьировать для разных систем. Предположим, что система A такая же, как у вас (температура> 100 перегревается), но система B более чувствительная (температура> 93 перегревается). Измените ли вы свою функцию управления для проверки типа системы, а затем примените правильное значение?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

Или у вас каждый тип системы определяет свою теплопроизводительность?

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

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

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

Мэтью Флинн
источник
1
Почему бы не зарегистрировать каждую систему на мониторе. Во время регистрации они могут указать, когда происходит перегрев.
Мартин Йорк,
@LokiAstari - Вы можете, но тогда вы можете столкнуться с новой системой, которая также чувствительна к влажности или атмосферному давлению. Принцип состоит в том, чтобы абстрагироваться от того, что меняется - в данном случае это подверженность перегреву
Мэтью Флинн
1
Именно поэтому у вас должна быть контрольная модель. Вы сообщаете системе текущие условия, и она информирует вас, если она находится за пределами нормальных рабочих условий. Таким образом, вам никогда не нужно изменять SystemMoniter. Это инкапсуляция для вас.
Мартин Йорк,
@LokiAstari - я думаю, что мы здесь говорим о разных целях - я действительно думал о создании разных систем, а не разных мониторов. Дело в том, что система должна знать, когда она находится в состоянии, которое вызывает тревогу, а не какую-то функцию внешнего контроллера. У SystemA должны быть свои критерии, у SystemB должны быть свои. Контроллер должен просто иметь возможность спрашивать (через регулярные промежутки времени), в порядке ли система.
Мэтью Флинн
6

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

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

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

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

Так что могут быть случаи, когда «Не так хорошо» - это путь, но обычно «Лучше», ну, лучше.

jmoreno
источник
3

Антисимметрия данных / объектов

Как отмечали другие, Tell-Dont-Ask специально предназначен для случаев, когда вы изменяете состояние объекта после того, как спросили (см., Например, текст Pragprog, размещенный в другом месте на этой странице). Это не всегда так, например, объект 'user' не изменяется после того, как его спросили о его user.address. Это поэтому спорны , если это является подходящим случаем для применения Tell-Dont-прод.

Tell-Dont-Ask занимается вопросами ответственности, а не вытягиванием логики из класса, который должен быть обоснованно внутри него. Но не вся логика, которая имеет дело с объектами, обязательно является логикой этих объектов. Это намекает на более глубоком уровне, даже за пределами Tell-Dont-Ask, и я хочу добавить краткое замечание об этом.

Что касается архитектурного проектирования, вы можете захотеть иметь объекты, которые на самом деле являются просто контейнерами для свойств, может быть даже неизменяемыми, а затем запускать различные функции над коллекциями таких объектов, оценивая, фильтруя или преобразовывая их, а не отправляя им команды (что является больше домен Телль-Дон-Аск).

Решение, более подходящее для вашей проблемы, зависит от того, ожидаете ли вы иметь стабильные данные (декларативные объекты), но с изменением / добавлением на стороне функции. Или если вы ожидаете, что у вас будет стабильный и ограниченный набор таких функций, но вы ожидаете большего потока на уровне объектов, например, путем добавления новых типов. В первой ситуации вы бы предпочли свободные функции, во втором - методы объекта.

Боб Мартин в своей книге «Чистый код» называет это «Антисимметрией данных / объектов» (стр. 95 и далее), другие сообщества могут называть это « проблемой выражения ».

ThomasH
источник
3

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

Это правило связано с правилом об избежании кода «двойная точка» или «двойная стрелка», часто называемого «разговаривать только с непосредственными друзьями», что говорит о недопустимости foo->getBar()->doSomething(); вместо этого используйте использование, foo->doSomething();которое является вызовом обертки вокруг функциональности панели, и реализовано так просто return bar->doSomething();- если fooотвечает за управление bar, то пусть так и сделает!

Николас Шэнкс
источник
1

В дополнение к другим хорошим ответам о «говорите, не спрашивайте», некоторые комментарии к вашим конкретным примерам, которые могут помочь:

Совет в C ++ заключается в том, что вы должны отдавать предпочтение свободным функциям, а не функциям-членам, поскольку они увеличивают инкапсуляцию Оба они семантически идентичны, так почему же предпочитаете выбор, который имеет доступ к большему количеству состояний?

Этот выбор не имеет доступа к большему количеству государства. Они оба используют одинаковое количество состояний для выполнения своей работы, но «плохой» пример требует, чтобы состояние класса было публичным, чтобы выполнять свою работу. Кроме того, поведение этого класса в «плохом» примере распространяется на функцию free, что затрудняет его поиск и усложняет рефакторинг.

Почему пользователь несет ответственность за форматирование несвязанной строки ошибки? Что делать, если я хочу что-то сделать, кроме печати «Нет названия улицы в файле», если на ней нет улицы? Что если улица названа так же?

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

Telastyn
источник
2
Это не правда. Вы предполагаете, что проверка на перегрев - единственная вменяемая вещь, связанная с температурой. Что, если класс предназначен для использования в качестве одного из нескольких мониторов температуры, и система должна предпринимать разные действия, например, в зависимости от многих их результатов? Когда это поведение может быть ограничено заранее заданным поведением отдельного экземпляра, тогда обязательно. Иначе, это явно не может применяться.
DeadMG
Конечно, или если бы термостат и сигнализация существовали в разных классах (как они, вероятно, должны).
Теластин
1
@DeadMG: Общий совет - делать вещи частными / защищенными, пока вам не понадобится доступ к ним. Хотя этот конкретный пример - мех, это не оспаривает стандартную практику.
Гуванте
Пример в статье о том, что практика «ме» вроде как оспаривает это. Если эта практика является стандартной из-за ее больших преимуществ, то зачем искать подходящий пример?
Стейн де Витт
1

Эти ответы очень хороши, но вот еще один пример, чтобы подчеркнуть: обратите внимание, что обычно это способ избежать дублирования. Например, предположим, что у вас есть НЕСКОЛЬКО мест с кодом вроде:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

Это означает, что вам лучше иметь что-то вроде этого:

Product product = productMgr.get(productUuid, currentUser)

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

antonio.fornie
источник
0

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

Например # 2, я думаю, что это слишком упрощено. Если бы мы действительно собирались реализовать это, SystemMonitor в конечном итоге имел бы код для низкоуровневого доступа к оборудованию и логику для высокоуровневой абстракции, встроенную в тот же класс. К сожалению, если бы мы попытались разделить это на два класса, мы бы нарушили само слово «говори, не спрашивай».

Пример № 4 более или менее одинаков - он встраивает логику пользовательского интерфейса в уровень данных. Теперь, если мы собираемся исправить то, что пользователь хочет видеть в случае отсутствия адреса, мы должны исправить объект на уровне данных, и что если два проекта используют один и тот же объект, но нужно использовать разный текст для нулевого адреса?

Я согласен, что если бы мы могли реализовать «Скажи, не проси» обо всем, это было бы очень полезно - я сам был бы счастлив, если бы я мог просто сказать, а не спрашивать (и делать это сам) в реальной жизни! Однако, как и в реальной жизни, выполнимость решения очень ограничена классами высокого уровня.

ТИА
источник