Як вибрати між функцією Tell Don't Ask and Command Query?


25

Принцип Tell Don't Ask говорить:

ви повинні намагатися розповідати об’єктам, що ви хочете, щоб вони робили; не задавайте їм запитань про їх стан, прийміть рішення, а потім розкажіть, що їм робити.

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

Простий приклад «Скажи, не проси» є

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

і версія Tell ...

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книгу з полиці, на якій вона є насправді).

Зі вступом бібліотекаря ми моделюємо цей взаємозв'язок окремо, тому досягаючи роз'єднаності.


Я вважаю, що це дуже вагомий момент, але зовсім не відповідає на питання. У вашій версії питання полягає в тому, чи повинен метод видалення (Book b) класу Shelf мати зворотне значення?
scarfridge

1
@scarfridge: У нижньому рядку, скажіть, не запитуйте, чи є наслідком належного розмежування проблем. Якщо ви думаєте про це, я таким чином відповідаю на питання. Принаймні, я б так подумав;)
back2dos

1
@scarfridge Замість зворотного значення можна передати функцію / делегата, який викликається при відмові. Якщо вам це вдасться, ви готові, правда?
Благословіть Яху

2
@BlessYahu Це здається мені надмірно розробленим. Я особисто вважаю, що розділення команд-запитів - це більше ідеал у реальному (багатопотоковому) світі. IMHO добре, що метод із побічними ефектами повертає значення до тих пір, поки ім'я методу чітко вказує, що він змінить стан об'єкта. Розглянемо метод pop () стека. Але метод із назвою запиту не повинен мати побічних ефектів.
шарфридж

13

Якщо вам потрібно знати результат, тоді ви робите; це ваша вимога.

Словосполучення "методи повинні повернути значення лише в тому випадку, якщо вони референтно прозорі і, отже, не мають побічних ефектів", є хорошим керівним принципом (особливо якщо ви пишете програму у функціональному стилі, для одночасності чи інших причин), але це не абсолют.

Наприклад, вам може знадобитися знати результат операції запису файлу (істинний або хибний). Це приклад методу, який повертає значення, але завжди викликає побічні ефекти; не обійтись.

Щоб виконати розділення команд / запитів, вам доведеться виконати операцію з файлом одним методом, а потім перевірити його результат іншим методом, небажаною технікою, оскільки він від'єднує результат від методу, який спричинив результат. Що робити, якщо щось трапиться зі станом вашого об'єкта між викликом файлу та перевіркою стану?

Суть полягає в наступному: якщо ви використовуєте результат виклику методу для прийняття рішень в інших місцях програми, ви не порушуєте функцію Tell Don't Ask. Якщо, з іншого боку, ви приймаєте рішення для об'єкта на основі виклику методу до цього об’єкта, тоді вам слід перемістити ці рішення в сам об’єкт, щоб зберегти інкапсуляцію.

Це зовсім не суперечить поділу запитів команд; насправді це додатково застосовує його, тому що більше не потрібно піддавати зовнішній метод для цілей статусу.


1
Можна стверджувати, що у вашому прикладі, якщо операція "запис" не вдасться, слід виключити виняток, тому немає необхідності в методі "отримати статус".
Девід

@David: Тоді поверніться до прикладу ОП, який би повернув істину, якщо зміна насправді відбулася, помилкову, якщо вона не відбулася. Сумніваюся, ви хочете туди винести виняток.
Роберт Харві

Навіть приклад ОП не вважає мене правильним. Або ви хочете, щоб вас помітили, якщо видалення не було можливим, і в цьому випадку ви будете використовувати виняток, або ви хочете, щоб вас помітили, коли віджет насправді видалено, і в цьому випадку ви можете використовувати подію чи механізм зворотного виклику. Я просто не бачу реального прикладу, коли ви б не хотіли поважати "Шаблон поділу запитів команд"
Девід

@David: я відредагував свою відповідь, щоб уточнити.
Роберт Харві

Щоб не ставити надто точну крапку на цьому, але вся ідея, яка стоїть за розділенням команд-запитів, полягає в тому, що той, хто видає команду для написання файлу , не хвилює результат - принаймні, не в конкретному сенсі (користувач може пізніше скласти список усіх останніх операцій з файлами, але це не має кореляції з початковою командою). Якщо вам потрібно вжити заходів після завершення команди, незалежно від того, чи піклуєтесь ви про результат, чи правильний спосіб зробити це з асинхронною моделлю програмування, використовуючи події, які (можуть) містити деталі про вихідну команду та / або її результат.
Aaronaught

2

Я думаю, що ви повинні піти зі своїм початковим інстинктом. Іноді конструкція повинна бути навмисно небезпечною, вводити складність негайно під час спілкування з об'єктом, щоб змусити відразу з цим правильно поводитися, замість того, щоб намагатися обробити його на самому об’єкті і приховати всі горілі деталі та ускладнити його чистоту змінити основні припущення.

У вас повинно виникнути відчуття занурення, якщо об’єкт пропонує вам реалізовані еквіваленти openта closeповедінку, і ви відразу починаєте розуміти, з чим маєте справу, коли бачите бульне значення повернення для того, що ви думали, що це буде проста атомна задача.

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

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


2

Те, що ви описуєте, є відомим "винятком" із принципу розділення команд-запитів.

У цій статті Мартін Фаулер пояснює, як він загрожує таким винятком.

Мейєр любить використовувати розділення команд-запитів абсолютно, але є винятки. Вискакування стека - хороший приклад запиту, який змінює стан. Меєр правильно говорить, що ви можете уникнути наявності цього методу, але це корисна ідіома. Тому я вважаю за краще дотримуватися цього принципу, коли можу, але готовий порушити його, щоб отримати свою попсу.

У вашому прикладі я вважав би той самий виняток.


0

Розлуку Command-Query надзвичайно легко зрозуміти неправильно.

Якщо я скажу вам у моїй системі, є команда, яка також повертає значення із запиту, і ви говорите "Ха! Ти порушуєш!" ти стрибаєш пістолет.

Ні, це не те, що заборонено.

Заборонено, коли ця команда є ТІЛЬКИМ способом зробити цей запит. Ні. Я не повинен міняти стан, щоб задавати питання. Це не означає, що я повинен закривати очі щоразу, коли змінюю стан.

Не має значення, якщо я це роблю, так як я все одно відкрию їх ще раз. Єдиною вигодою від цієї зайвої реакції було те, що дизайнери не лінуються і не забудуть включити запит, що не змінюється.

Справжнє значення настільки прокляте тонкі, що ледачим людям просто сказати, що сетери повинні повернутись ні до чого. Це полегшує переконання, що ви не порушите реальне правило, але це надмірна реакція, яка може стати справжнім болем.

Вільні інтерфейси та iDSL постійно "порушують" цю реакцію. Якщо надмірно реагувати, багато сил ігнорується.

Ви можете стверджувати, що команда повинна робити лише одне, а запит - лише одне. І в багатьох випадках це хороший момент. Хто скаже, що існує лише один запит, який повинен слідувати команді? Добре, але це не розділення команд-запитів. Це принцип єдиної відповідальності.

Поглянувши на це так, і стек-поп - не якийсь химерний виняток. Це одна відповідальність. Поп порушує лише розділення запитів команд, якщо ви забудете заглянути.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.