Как выбрать между Tell not Ask и разделением командного запроса?

25

Принцип « Говори, а не спрашивай» гласит:

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

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

Простой пример «Скажи, не проси» является

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

и сказать версию ...

Widget w = ...;
w.removeFromParent();

Но что, если мне нужно узнать результат от метода removeFromParent? Моя первая реакция состояла в том, чтобы просто изменить removeFromParent, чтобы он возвращал логическое значение, обозначающее, был ли удален родитель или нет.

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

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

Эти два действительно противоречат друг другу, и как я могу выбрать между этими двумя? Я согласен с прагматичным программистом или Бертраном Мейером?

Дакота Север
источник
1
что бы вы сделали с логическим значением?
Дэвид
1
Похоже, вы слишком глубоко
погружаетесь
Re boolean ... это пример, который было легко отбросить, но похожий на операцию записи ниже, цель в том, чтобы он был статусом операции.
Дакота Север
2
я хочу сказать, что вы должны сосредоточиться не на «возврате чего-то», а на том, что вы хотите с этим сделать. Как я сказал в другом комментарии, если вы сосредоточены на сбое, то используйте исключение, если вы хотите что-то сделать после завершения операции, затем используйте обратный вызов или события, если вы хотите записать, что произошло, это ответственность виджета ... Это почему я спрашиваю о твоих потребностях. Если вы не можете найти конкретный пример, где вам нужно что-то вернуть, возможно, это означает, что вам не придется выбирать между ними.
Дэвид
1
Разделение командного запроса - это принцип, а не шаблон. Шаблоны - это то, что вы можете использовать для решения проблемы. Принципы - это то, что вы соблюдаете, чтобы не создавать проблем.
candied_orange

Ответы:

25

На самом деле ваш пример проблемы уже иллюстрирует отсутствие декомпозиции.

Давайте просто немного его изменим:

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

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

Аналогично, виджету не обязательно знать своего родителя. Вы скажете: «Хорошо, но виджету нужен родительский элемент для правильной компоновки и т. Д.» и под капотом виджет знает своего родителя и запрашивает у него метрики для расчета своих собственных метрик на основе их и т. д. Согласно скажите, не спрашивайте, что это неправильно. Родитель должен указать всем своим дочерним элементам, передавая всю необходимую информацию в качестве аргументов. Таким образом, можно легко иметь один и тот же виджет у двух родителей (независимо от того, имеет ли это смысл).

Чтобы вернуться к примеру с книгой - введите библиотекаря:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

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

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

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

back2dos
источник
Я думаю, что это очень верный момент, но он не отвечает на вопрос вообще. В вашей версии вопрос заключается в том, должен ли метод remove (Book b) в классе Shelf иметь возвращаемое значение?
шарфридж
1
@scarfridge: В конце скажи, не спрашивай , это следствие правильного разделения интересов. Если вы думаете об этом, я, таким образом, отвечаю на вопрос. По крайней мере, я бы так подумал;)
back2dos
1
@scarfridge Вместо возвращаемого значения можно передать функцию / делегат, который вызывается при ошибке. Если это удастся, вы сделали, верно?
Благослови Яху
2
@BlessYahu Это кажется мне слишком сложным. Лично я чувствую, что разделение команд и запросов является более идеальным в реальном (многопоточном) мире. ИМХО, это нормально для метода с побочными эффектами возвращать значение, если имя метода четко указывает, что это изменит состояние объекта. Рассмотрим метод pop () стека. Но метод с именем запроса не должен иметь побочных эффектов.
шарфридж
13

Если вам нужно знать результат, тогда вы знаете; это ваше требование.

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

Например, вам может потребоваться узнать результат операции записи в файл (true или false). Это пример метода, который возвращает значение, но всегда вызывает побочные эффекты; нет никакого способа обойти это.

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

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

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

Роберт Харви
источник
1
Кто-то может возразить, что в вашем примере, если операция «write» завершится неудачей, должно быть сгенерировано исключение, поэтому нет необходимости в методе «get status».
Дэвид
@David: Затем вернемся к примеру OP, который вернул бы true, если изменение действительно произошло, и false, если оно не произошло. Я сомневаюсь, что вы хотите бросить исключение там.
Роберт Харви
Даже пример ОП мне не подходит. Либо вы хотите, чтобы вас заметили, если удаление было невозможно, и в этом случае вы бы использовали исключение, либо вы хотите, чтобы вас заметили, когда виджет был фактически удален, и в этом случае вы могли бы использовать механизм события или обратного вызова. Я просто не вижу реального примера, когда вы не хотели бы соблюдать «Шаблон разделения командных запросов»
Дэвид
@ Давид: я отредактировал свой ответ, чтобы уточнить.
Роберт Харви
Не стоит слишком подчеркивать это, но вся идея разделения команд и запросов состоит в том, что тот, кто выполняет команду для записи файла , не заботится о результате - по крайней мере, не в определенном смысле (пользователь может позже раскрыть список всех последних файловых операций, но это не имеет отношения к исходной команде). Если вам нужно предпринять действия после завершения команды, независимо от того, заботитесь ли вы о результате или нет, то правильный способ сделать это - использовать модель асинхронного программирования с использованием событий, которые (могут) содержать сведения об исходной команде и / или ее результат.
Ааронаут
2

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

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

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

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

Филипп Дупанович
источник
2

То, что вы описываете, является известным «исключением» из принципа разделения команд и запросов.

В этой статье Мартин Фаулер объясняет, как он будет угрожать такому исключению.

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

В вашем примере я бы рассмотрел то же исключение.

elviejo79
источник
0

Разделение команд-запросов удивительно легко понять.

Если я скажу вам, в моей системе есть команда, которая также возвращает значение из запроса, и вы говорите: «Ха! Вы нарушаете!» вы прыгаете пистолет

Нет, это не то, что запрещено.

Запрещается, когда эта команда является ЕДИНСТВЕННЫМ способом сделать этот запрос. Нет. Мне не нужно менять штат, чтобы задавать вопросы. Это не значит, что я должен закрывать глаза каждый раз, когда меняю состояние.

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

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

Свободные интерфейсы и iDSL постоянно "нарушают" эту чрезмерную реакцию. Если вы чрезмерно реагируете, вы игнорируете много силы.

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

Если посмотреть так, то стэк-поп - не странное исключение. Это единственная ответственность. Pop только нарушает разделение командного запроса, если вы забыли указать peek.

candied_orange
источник