Пояснення про те, як “Скажи, не питай” вважається хорошим ОО


49

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

Такі як приклад №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'якщо на ньому немає вулиці? Що робити, якщо вулиця названа однаково?


Чи може хтось просвітити мене про переваги та обґрунтування "Скажи, не питай"? Я не шукаю кращого, а натомість намагаюся зрозуміти точку зору автора.


Прикладами коду можуть бути 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 не хвилюйся. Тепер, коли ви знаєте ключовий момент, який ставить цей приклад (та будь-який інший з прагпрогу з цього приводу) у передбачуваний контекст ("немає такого поняття, як найкраще рішення ..."), добре не читати книгу
gnat

1
Я все ще не впевнений, що Tell, Don't Ask повинен прописати для вас без контексту, але це справді хороша порада OOP.
Ерік Реппен

16

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

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

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

Також зауважте, що у другому прикладі безглуздо використовується антидіаграма Null Object Anti.


19
"Нульовий об'єкт" - це не те, що я б назвав анти-шаблоном, тому мені цікаво, що це за ваша причина?
Конрад Рудольф

4
Досить впевнений, що ніхто не має методів, визначених як "Не робить нічого". Це робить безліч їх називати безглуздими. Це означає, що будь-який об’єкт, що реалізує Null Object, щонайменше порушує LSP і описує себе як реалізаційні операції, яких він, власне, не робить. Користувач очікує повернення значення. Від цього залежить коректність їх програми. Ви просто приносите більше проблем, роблячи вигляд, що це цінність, коли її немає. Ви коли-небудь намагалися налагодити мовчки невдалі методи? Це неможливо, і ніхто ніколи не повинен через це страждати.
DeadMG

4
Я б заперечував, що це повністю залежить від проблемної області.
Конрад Рудольф

5
@DeadMG Я погоджуюся, що вищенаведений приклад - це неправильне використання шаблону Null Object, але в його використанні є заслуга. Я кілька разів використовував реалізацію якогось інтерфейсу чи іншого, щоб уникнути перевірки нуля чи справжнього "null" пронизування системи.
Макс

6
Не впевнений, що я бачу вашу думку з "клієнт може check_for_underheatingбез змін SystemMonitor". Чим клієнт відрізняється від SystemMonitorтого часу? Ви зараз не розповсюджуєте свою логіку моніторингу у кількох класах? Я також не бачу проблеми з класом монітора, який надає інформацію про сенсор для інших класів, зберігаючи при цьому функції тривоги. Контролер посилення повинен контролювати прискорений сигнал без необхідності турбуватися про підвищення тривоги, якщо температура стає занадто високою.
TMN

9

Справжня проблема вашого прикладу перегріву полягає в тому, що правила щодо того, що кваліфікується як перегрів, не змінюються легко для різних систем. Припустимо, система A така, як у вас є (temp> 100 перегрівається), але система B більш делікатна (temp> 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 - Я думаю, що ми тут говоримо для перехресних цілей - я дійсно дивився на створення різних систем, а не різних моніторів. Річ у тім, що система повинна знати, коли вона знаходиться в стані, що викликає тривогу, на відміну від деяких функцій зовнішнього контролера. Система має мати свої критерії, SystemB повинна мати свої власні. Контролер повинен просто мати можливість запитувати (через регулярні проміжки часу), чи нормально система чи ні.
Меттью Флінн

6

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

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

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

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

Тож можуть бути випадки, коли "Не так добре" - це шлях, але в цілому "Краще", ну, краще.


3

Дані / Об'єкт Антисиметрія

Як зазначали інші, Tell-Dont-Ask спеціально для випадків, коли ви змінюєте стан об'єкта після того, як ви запитали (див., Наприклад, текст Pragprog, розміщений в інших місцях на цій сторінці). Це не завжди так, наприклад, об'єкт "користувач" не змінюється після того, як його запитали для його user.address. Тому можна обговорити, якщо це відповідний випадок, щоб застосувати Tell-Dont-Ask.

Tell-Dont-Ask переймається відповідальністю, не витягуючи логіку з класу, який повинен бути виправдано всередині нього. Але не вся логіка, яка має справу з об'єктами, обов'язково є логікою цих об'єктів. Це натякає на більш глибокий рівень, навіть поза Tell-Dont-Ask, і я хочу додати коротке зауваження з цього приводу.

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

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

Боб Мартін у своїй книзі "Чистий код" називає це "антисиметрією даних / об'єктів" (p.95ff), інші спільноти можуть називати це " проблемою вираження ".


3

Ця парадигма іноді називається «Скажи, не питай» , тобто скажіть об’єкту, що робити, не запитайте про його стан; а іноді як "Попроси, не кажи" , тобто попросіть об'єкт зробити щось для вас, не кажіть, яким повинен бути його стан. У будь-якому випадку найкраща практика однакова - те, як об’єкт повинен виконувати дію, викликає занепокоєння об'єкта, а не об'єкта, що викликає. Інтерфейси повинні уникати розкриття їх стану (наприклад, через доступу або загальнодоступні властивості), а натомість викривати методи "робити", реалізація яких непрозора. Інші висвітлювали це посиланнями на прагматичного програміста.

Це правило пов’язане з правилом про уникнення коду "подвійної крапки" або "подвійної стрілки", який часто називають "Тільки спілкуватися з безпосередніми друзями", що стверджує, що foo->getBar()->doSomething()це погано, натомість використовуйте, foo->doSomething();що це обмотка обговорення функціональності бару, і реалізовано просто return bar->doSomething();- якщо fooце відповідає за управління bar, то нехай це робити!


1

На додаток до інших хороших відповідей про "скажи, не питай", деякі коментарі до ваших конкретних прикладів, які можуть допомогти:

Порада в C ++ полягає в тому, що вам слід віддавати перевагу вільним функціям, а не членам, оскільки вони збільшують інкапсуляцію. Обидва вони семантично однакові, тож чому б віддавати перевагу вибору, який має доступ до більшої кількості держав?

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

Чому Користувач несе обов'язок форматувати непов'язаний рядок помилок? Що робити, якщо я хочу зробити щось, окрім надрукувати "Без назви вулиці у файлі", якщо на ній немає вулиці? Що робити, якщо вулиця названа однаково?

Чому відповідальність за "ім'ям вулиці" несе як "отримати назву вулиці", так і "надати повідомлення про помилку"? Принаймні у «хорошій» версії, кожен твір несе одну відповідальність. Все-таки це не чудовий приклад.


2
Це не правда. Ви припускаєте, що перевірка на перегрів - це єдине розумне, що стосується температури. Що робити, якщо клас призначений бути одним з ряду моніторів температури, і система повинна вживати різних дій залежно від багатьох результатів, наприклад? Коли така поведінка може бути обмежена заздалегідь визначеною поведінкою окремого екземпляра, тоді обов'язково. Ще, це явно не може застосувати.
DeadMG

Зрозуміло, або якщо терморегулятор і сигналізатори існували в різних класах (як це можливо).
Теластин

1
@DeadMG: Загальна порада - робити речі приватними / захищеними, поки вам не потрібен доступ. Хоча цей конкретний приклад - це я, це не суперечить стандартній практиці.
Гуванте

Приклад у статті про те, що практика "meh" свого роду заперечує це. Якщо ця практика є стандартною через велику користь, то навіщо турбуватися знайти відповідний приклад?
Штійн де Вітт

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)

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


0

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

Наприклад, №2, я вважаю, що це занадто спрощено. Якби ми насправді збиралися реалізувати це, SystemMonitor в кінцевому підсумку мав би код для низькорівневого апаратного доступу та логіку для абстракції високого рівня, вбудований у той самий клас. На жаль, якщо ми намагаємось розділити це на два класи, ми б порушили «Скажи, не питай».

Приклад №4 більш-менш однаковий - це вбудовування логіки інтерфейсу користувача в рівень даних. Тепер, якщо ми збираємося виправити те, що хоче бачити користувач у разі відсутності адреси, нам доведеться виправити об'єкт у рівні даних, а що робити, якщо два проекти використовують цей самий об’єкт, але для нульової адреси потрібно використовувати інший текст?

Я погоджуюсь, що якщо ми зможемо реалізувати "Скажи, не питай" у всьому, це було б дуже корисно - я сам був би радий, якщо зможу просто сказати, а не попросити (і зробити це сам) у реальному житті! Однак, як і в реальному житті, доцільність рішення дуже обмежена класами високого рівня.

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