Як я можу здійснити дзвінок із булевим ясним? Булева пастка


76

Як зазначається у коментарях @ benjamin-gruenbaum, це називається булевою пасткою:

Скажіть, у мене така функція

UpdateRow(var item, bool externalCall);

і в моєму контролері це значення для externalCallзавжди буде ПРАВИЛЬНИМ. Який найкращий спосіб викликати цю функцію? Я зазвичай пишу

UpdateRow(item, true);

Але я запитую себе, чи повинен я оголосити булеве, аби лише вказати, на що означає «справжнє» значення? Ви можете це знати, переглянувши декларацію функції, але це, очевидно, швидше і зрозуміліше, якщо ви просто побачили щось подібне

bool externalCall = true;
UpdateRow(item, externalCall);

ПД: Не впевнений, чи справді це питання тут підходить, якщо воно не відповідає, де я можу отримати більше інформації про це?

PD2: Я не тегував жодної мови, тому що думав, що це дуже загальна проблема. У будь-якому випадку я працюю з c # і прийнята відповідь працює для c #


10
Ця закономірність також називається булевою пасткою
Бенджамін Грюнбаум

11
Якщо ви використовуєте мову з підтримкою алгебраїчних типів даних, я дуже рекомендую новий adt замість булевого. data CallType = ExternalCall | InternalCallнаприклад у haskell
Філіп Хаглунд

6
Я здогадуюсь, що "Енумс" наповнив би саме таку мету; отримання імен для булів та трохи безпеки типу.
Філіп Хаглунд

2
Я не погоджуюся з тим, що оголосити булеве, щоб вказати сенс, "явно зрозуміліше". З першим варіантом очевидно, що ти завжди будеш правдою. З другим, ви повинні перевірити (де визначена змінна? Чи змінюється її значення?). Звичайно, це не проблема, якщо два рядки разом ... але хтось може вирішити, що ідеальне місце для додавання коду - саме між двома рядками. Буває!
AJPerez

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

Відповіді:


154

Не завжди є ідеальним рішенням, але у вас є багато альтернатив:

  • Використовуйте названі аргументи , якщо вони доступні вашою мовою. Це працює дуже добре і не має особливих недоліків. У деяких мовах будь-який аргумент може бути переданий як іменований аргумент, наприклад updateRow(item, externalCall: true)( C # ) або update_row(item, external_call=True)(Python).

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

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

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

  • Використовуйте перерахунок . Проблема з булевими полягає в тому, що їх називають "справжніми" та "помилковими". Отже, замість цього введіть тип із кращими іменами (наприклад enum CallType { INTERNAL, EXTERNAL }). Як додаткова перевага, це підвищує безпеку типу вашої програми (якщо ваша мова реалізує перераховує як окремі типи). Недолік переліків полягає в тому, що вони додають тип вашому загальнодоступному API. Для суто внутрішніх функцій це не має значення, і перерахунки не мають суттєвих недоліків.

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

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


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

6
Одне, що може допомогти полегшити проблеми з короткими рядками, - це використовувати константи зі значеннями рядка. @MarioGarcia Не надто багато чого розробити. Крім сказаного тут, більшість деталей залежатиме від конкретної мови. Чи було щось особливе, що ви хотіли побачити тут?
jpmc26

8
У мовах, де ми говоримо про метод, і enum можна віднести до класу, я зазвичай використовую enum. Це повністю самодокументування (в єдиному рядку коду, який оголошує тип enum, тому він також легкий), повністю запобігає виклику методу з голим булевим, а вплив на загальнодоступний API незначний (оскільки ідентифікатори проходять до класу).
давидбак

4
Ще один недолік використання рядків у цій ролі полягає в тому, що ви не можете перевірити їхню дійсність під час компіляції, на відміну від перерахунків.
Руслан

32
enum - переможець. Тільки тому, що параметр може мати лише одне з двох можливих станів, це не означає, що він автоматично повинен бути булевим.
Піт

39

Правильне рішення - робити те, що ви пропонуєте, але упакуйте його в міні-фасад:

void updateRowExternally() {
  bool externalCall = true;
  UpdateRow(item, externalCall);
}

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


8
Чи справді ця інкапсуляція варта того, коли я лише один раз називаю цю функцію у своєму контролері?
Маріо Гарсія

14
Так. Так. Як я вже писав вище, причиною є те, що вартість (виклик функції) менша від вигоди (ви економите нетривіальну кількість часу для розробника, тому що ніколи не доводиться шукати, що trueробить.) Це було б вагомим, навіть якби коштувати стільки часу для процесора, скільки економить час розробника, оскільки час процесора значно дешевший.
Кіліан Фот

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

33
@KilianFoth За винятком помилкового припущення. Супроводжуючий дійсно потрібно знати , що робить другий параметр, і чому це так, і майже завжди це відноситься до деталей , що ви намагаєтеся зробити. Приховування функціональних можливостей у крихітних функціях, що скорочуються на тисячу, зменшить ремонтопридатність, а не збільшить її. Я бачив, як це відбувалося сам, сумно сказати. Надмірний розподіл на функції може насправді бути гіршим за величезну функцію Бога.
Грем

6
Я думаю, UpdateRow(item, true /*external call*/);було б чистіше, мовами, які дозволяють цей синтаксис коментарів. Здуття вашого коду додатковою функцією лише для того, щоб не писати коментар, здається, що це не просто для простого випадку. Можливо, якщо для цієї функції було б багато інших аргументів та / або якийсь захаращений + хитрий оточуючий код, він почав би звертатись ще більше. Але я думаю, що якщо ви налагоджуєте, ви збираєтеся перевірити функцію обгортки, щоб побачити, що вона робить, і потрібно пам’ятати, які функції - тонкі обгортки навколо API бібліотеки, а які насправді мають певну логіку.
Пітер Кордес

25

Скажіть, у мене є така функція, як UpdateRow (var item, bool externalCall);

Чому у вас така функція?

За яких обставин ви б назвали це аргументом externalCall, встановленим на різні значення?

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

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


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

18

Хоча я згоден, ідеально використовувати мовну функцію для забезпечення читабельності та безпеки цінності, ви також можете скористатися практичним підходом: коментарі під час дзвінка. Подібно до:

UpdateRow(item, true /* row is an external call */);

або:

UpdateRow(item, true); // true means call is external

або (правильно, як це запропонував Frax):

UpdateRow(item, /* externalCall */true);

1
Немає жодної причини подавати заявку. Це цілком коректна пропозиція.
Suncat2000

Вдячний за <3 @ Suncat2000!
єпископ

11
(Можливо, кращим) варіантом є просто введення простого імені аргументу туди, як UpdateRow(item, /* externalCall */ true ). Зауваження в повному реченні набагато складніше проаналізувати, це насправді здебільшого шум (особливо другий варіант, який також дуже вільно поєднується з аргументом).
Frax

1
Це, безумовно, найбільш відповідна відповідь. Саме для цього призначені коментарі.
Sentinel

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

11
  1. Ви можете «назвати» ваші булі. Нижче наводиться приклад мови OO (де вона може бути виражена класом, що надає UpdateRow()), проте сама концепція може бути застосована будь-якою мовою:

    class Table
    {
    public:
        static const bool WithExternalCall = true;
        static const bool WithoutExternalCall = false;
    

    та на сайті виклику:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Я вважаю, що пункт 1 є кращим, але він не змушує користувача використовувати нові змінні під час використання функції. Якщо безпека типу важлива для вас, ви можете створити enumтип і змусити UpdateRow()прийняти це замість bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Ви можете якось змінити ім'я функції, щоб воно краще відображало значення boolпараметра. Не дуже впевнений, але можливо:

    UpdateRowWithExternalCall(var item, bool externalCall)


8
Не подобається №3, як зараз, якщо ви називаєте функцію externalCall=false, її назва абсолютно не має сенсу. Решта вашої відповіді хороша.
Гонки легкості по орбіті

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

@LightnessRacesinOrbit Справді. Безпечніше йти UpdateRowWithUsuallyExternalButPossiblyInternalCall.
Ерік Думініл

@EricDuminil: Пляма на 😂
Гонки легкості в орбіті

10

Ще один варіант, якого я тут ще не читав: Використовуйте сучасний IDE.

Наприклад, IntelliJ IDEA друкує ім'я змінної змінних у методі, який ви викликаєте, якщо ви передаєте літерал, такий як trueабо nullабо username + “@company.com. Це робиться невеликим шрифтом, щоб він не займав занадто багато місця на екрані і виглядав дуже відмінним від фактичного коду.

Я все ще не кажу, що гарну ідею скрізь кидати буліни. Аргумент, який говорить про те, що ви читаєте код набагато частіше, ніж ви пишете, часто дуже сильний, але для цього конкретного випадку це сильно залежить від технології, яку ви (та ваші колеги!) Використовуєте для підтримки своєї роботи. З IDE це набагато менше проблем, ніж, наприклад, vim.

Модифікований приклад частини моєї тестової установки: введіть тут опис зображення


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

@DanielPryden впевнений, звідси мій коментар "та ваших колег". У компаніях зазвичай є стандартний ІДЕ. Що стосується інших інструментів, я б заперечив, що цілком можливо, що такі інструменти також підтримують це чи будуть у майбутньому, або що ці аргументи просто не застосовуються (як для команди з програмування для двох пар)
Sebastiaan van den Broek

2
@Sebastiaan: Я не думаю, що я згоден. Навіть будучи соло розробником, я регулярно читаю власний код у Git diff. Git ніколи не зможе робити той самий тип візуалізації, орієнтованого на контекст, що і IDE, а також не повинен. Я все за те, щоб використовувати хороший IDE, щоб допомогти вам написати код, але ви ніколи не повинні використовувати IDE як привід для написання коду, який би ви не написали без нього.
Даніель Приден

1
@DanielPryden Я не виступаю за написання надзвичайно неохайного коду. Це не чорно-біла ситуація. Можуть бути випадки, коли в минулому для конкретної ситуації ви були б на 40% за те, щоб написати функцію з булевим, а 60% - ні, і ви не зробили цього. Можливо, при хорошій підтримці IDE зараз залишок становить 55%, коли це написано так, а 45% - ні. І так, можливо, вам все ж доведеться іноді читати його поза IDE, щоб це не було ідеально. Але це все-таки дійсна компенсація проти альтернативи, як-от додавання іншого методу чи перерахування для уточнення коду в іншому випадку.
Себастіян ван ден Брук

6

2 дні і ніхто не згадав про поліморфізм?

target.UpdateRow(item);

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

Зробіть це, і це стане частиною будівельної проблеми. Це можна вирішити багатьма способами. Ось один:

Target xyzTargetFactory(TargetBuilder targetBuilder) {
    return targetBuilder
        .connectionString("some connection string")
        .table("table_name")
        .external()
        .build()
    ; 
}

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

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


2

Якщо ви можете змінити updateRowфункцію, можливо, ви можете змінити її на дві функції. Зважаючи на те, що функція приймає булевий параметр, я підозрюю, що це виглядає приблизно так:

function updateRow(var item, bool externalCall) {
  Database.update(item);

  if (externalCall) {
    Service.call();
  }
}

Це може бути трохи кодовим запахом. Функція може мати різно різну поведінку залежно від того, для якої externalCallзмінної встановлено, і в цьому випадку вона має дві різні обов'язки. Повторний розподіл його на дві функції, на які покладено лише одну відповідальність, може покращити читабельність:

function updateRowAndService(var item) {
  updateRow(item);
  Service.call();
}

function updateRow(var item) {
  Database.update(item);
}

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

Звичайно, це не завжди так. Це ситуативно і справа смаку. Рефакторинг функції, яка бере булевий параметр на дві функції, зазвичай варто розглянути, але це не завжди найкращий вибір.


0

Якщо UpdateRow знаходиться в контрольованій базі коду, я б розглядав шаблон стратегії:

public delegate void Request(string query);    
public void UpdateRow(Item item, Request request);

Де Запит представляє якусь DAO (зворотний виклик у тривіальному випадку).

Справжній випадок:

UpdateRow(item, query =>  queryDatabase(query) ); // perform remote call

Неправдивий випадок:

UpdateRow(item, query => readLocalCache(query) ); // skip remote call

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

UpdateRow(item, query => {
  var data = readLocalCache(query);
  if (data == null) {
    data = queryDatabase(query);
  }
  return data;
} );

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

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