Чи булеві як аргументи методу неприйнятні? [зачинено]


123

Мій колега заявляє, що булеви як аргументи методу неприйнятні . Їх слід замінити перерахуванням. Спочатку я не бачив ніякої користі, але він дав мені приклад.

Що простіше зрозуміти?

file.writeData( data, true );

Або

enum WriteMode {
  Append,
  Overwrite
};

file.writeData( data, Append );

Тепер я це отримав! ;-)
Це, безумовно, приклад, коли перерахування як другий параметр робить код значно читабельнішим.

Отже, яка ваша думка на цю тему?


7
Це було дуже цікаве прочитання, я частіше буду реалізовувати цей метод.
Сара Чіпс

Хрм, я робив це раніше, але ніколи не усвідомлював, наскільки це хороший дизайн. значить, перерахунок йде у файл?
Шон

Енуми, безумовно, мають більше сенсу від смислового pov. З іншого приводу, було б цікаво подивитися, що придумали деякі програмісти для обробки нечіткої логіки.
Джеймс П.

2
просто запитайте хлопця з лимоном у Adventure Time, якщо це неприпустимо
ajax333221

Відповіді:


131

Булеві варіанти представляють "так / ні" вибір. Якщо ви хочете представити "так / ні", тоді використовуйте булеву форму, вона повинна бути самоосмисленою.

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


3
Крім того, в назві методу повинно бути зрозуміло, що робить аргумент так чи ні, тобто недійсна настройка повороту turnLightOn (bool) true або так буде увімкнено світло.
симон

10
Хоча в цьому випадку я, можливо, краще мати turnLightOn () та turnLightOff (), залежно від ситуації.
скафман

14
"turnLightOn (false)" означає "не включати світло"? Плутанина.
Джей Базузі

17
Як щодо setLightOn(bool).
Фінбарр

10
Пізній коментар, але @ Джей Базузі: Якщо ваш метод називається turnLightOn, і ви передаєте помилковий, ви можете також не викликати метод взагалі, передаючи помилкові слова, не включайте світло. Якщо світло вже увімкнено, це не означає вимкнути його, це означає, що не вмикайте його ... Якщо у вас є метод 'turnLight', перерахунок із значеннями «Увімкнено» та «Вимкнено» має сенс, включте включення ( Увімкнути), увімкніть освітлення (вимкнено). Я погоджуюся зі skaffman tho, я вважаю за краще два різні явні методи, turnLightOn () і turnLightOff (). (BTW: Цей матеріал пояснюється в книзі дядька Бобса "Чистий код")
Філ


32

Використовуйте той, який найкраще моделює вашу проблему. У прикладі, який ви наводите, перелік - кращий вибір. Однак були б інші часи, коли булевий краще. Що для вас більше сенсу:

lock.setIsLocked(True);

або

enum LockState { Locked, Unlocked };
lock.setLockState(Locked);

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


2
у вашому прикладі я вважаю за краще два способи. lock.lock () lock.release () та lock.IsSet, але все залежить від того, що має найбільше значення для споживчого коду.
Роберт Поулсон

3
Це справедливий коментар, але я думаю, що він також ілюструє більш широкий момент, що існує багато способів моделювання даної проблеми. Ви повинні використовувати кращу модель для своїх обставин, а також використовувати найкращі інструменти, які надає середовище програмування, щоб відповідати вашій моделі.
Джеремі Бурк

Я повністю згоден :), я просто коментував конкретний псевдокод, який пропонував ще один прийом. Я згоден з вашою відповіддю.
Роберт Полсон,

14

Для мене ні використання булевого, ні перерахування не є хорошим підходом. Роберт К. Мартін дуже чітко фіксує це у своєму підказці «Чистий код» №12: Усунення булевих аргументів :

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

Якщо метод робить більше ніж одне, вам слід написати два різні методи, наприклад у вашому випадку: file.append(data)і file.overwrite(data).

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


7
Чи це не означає, що функція, яка приймає рядок ASCII довжиною N, робить 128 ^ N речами?
detly

@delty Це серйозний коментар? Якщо так, чи часто ви кодуєте if на всіх можливих значеннях рядка? Чи можливе порівняння зі випадком булевого аргументу?
Паскаль Thivent

Я вважаю, що вони прийнятні, коли ви встановлюєте булеве значення всередині об'єкта. Ідеальним прикладом може бути setVisible(boolean visible) { mVisible = visible; }. Що б ви запропонували альтернативу?
Бред

2
@Brad show () {mVisible = true} hid () {mVisible = false}
Освальдо Акауан

@Oswaldo, хоча все ще правильно, я не думаю, що має сенс мати два різних способи присвоїти булеві значення різним значенням. У вас немає setIntToOne (), setIntToTwo () setIntToThree (), правда? Дещо неоднозначніше, коли ви можете мати лише два можливі значення, але для чистоти використовуйте булевий у такому випадку.
Бред

13

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


13

Пам’ятаєте питання, яке поставив Адлай Стівенсон перед послом Зоріном в ООН під час кубинської ракетної кризи ?

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

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

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

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


13

Є дві причини, через які я зіткнувся з тим, що це погано:

  1. Тому що деякі люди будуть писати такі методи, як:

    ProcessBatch(true, false, false, true, false, false, true);
    

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

  2. Оскільки управління потоком програми простою гілкою "так / ні" може означати, що у вас є дві абсолютно різні функції, які об'єднані в одну незручно. Наприклад:

    public void Write(bool toOptical);
    

    Дійсно, це повинні бути два способи

    public void WriteOptical();
    public void WriteMagnetic();
    

    тому що код у них може бути зовсім іншим; їм, можливо, доведеться робити різного роду обробку помилок та перевірку помилок або, можливо, навіть доведеться по-різному форматувати вихідні дані. Ви не можете цього сказати, використовуючи Write()або навіть Write(Enum.Optical)(хоча, звичайно, ви можете скористатися одним із цих методів, просто зателефонуйте до внутрішніх методів WriteOptical / Mag, якщо хочете).

Я думаю, це просто залежить. Я б не робив надто великої угоди з цього приводу, крім №1.


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

Однак ця відповідь може отримати користь від переформатування! ;-)
Ярик

7

Енуми кращі, але я б не називав булеві парами "неприйнятними". Іноді просто простіше запустити одну маленьку булеву і рухатися далі (думати про приватні методи тощо)


просто зробіть метод дуже описовим, щоб було зрозуміло, що означає правда чи так.
симон

6

Булеви можуть бути в порядку в мовах, які мають названі параметри, наприклад, Python та Objective-C, оскільки ім'я може пояснити, що робить цей параметр:

file.writeData(data, overwrite=true)

або:

[file writeData:data overwrite:YES]

1
IMHO, writeData () є поганим прикладом використання булевого параметра, незалежно від того, підтримуються чи не підтримуються названі параметри. Як би ви не назвали параметр, значення значення False не очевидно!
Ярик

4

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

Спершу дозвольте мені взяти ваш приклад: відповідальність (і здатність) програмістів писати хороший код насправді не піддається небезпеці параметром булева. У вашому прикладі програміст міг написати так само багатослівний код, написавши:

dim append as boolean = true
file.writeData( data, append );

або я віддаю перевагу більш загальне

dim shouldAppend as boolean = true
file.writeData( data, shouldAppend );

По-друге: Приклад Enum, який ви навели, лише "кращий", тому що ви проходите CONST. Найімовірніше, що в більшості застосувань принаймні деякі, якщо не більшість часових параметрів, які передаються функціям, є VARIABLES. в такому випадку мій другий приклад (надання змінних з хорошими іменами) набагато кращий, і Енум дав би вам невеликі переваги.


1
Хоча я погоджуюся з тим, що булеві параметри прийнятні у багатьох випадках, у разі прикладу цього writeData (), булевий параметр, як shouldAppend, є дуже недоречним. Причина проста: не відразу зрозуміло, у чому полягає значення Неправдивого.
Ярик

4

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

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

Властивості (особливо з ініціалізаторами об'єктів C # 3) або аргументи ключових слів (a la ruby ​​або python) - це набагато кращий спосіб перейти туди, де ви б інакше використовували булевий аргумент.

Приклад C #:

var worker = new BackgroundWorker { WorkerReportsProgress = true };

Рубін приклад

validates_presence_of :name, :allow_nil => true

Приклад Python

connect_to_database( persistent=true )

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


4

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

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

Подивіться на рамку .Net. Булеви використовуються як параметри досить багатьох методів. API .Net не є ідеальним, але я не думаю, що використання булевих параметрів як параметрів є великою проблемою. Підказка завжди дає вам назву параметра, і ви також можете створити такі вказівки - заповніть свої XML коментарі до параметрів методу, вони з’являться у підказці.

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

Наприклад, якщо ваш клас має такі властивості

public bool IsFoo
public bool IsBar

І помилка в тому, щоб вони були одночасно істинними, те, що ви насправді отримали, це три дійсні стани, краще виражені як щось на зразок:

enum FooBarType { IsFoo, IsBar, IsNeither };

4

Деякі правила, яких ваш колега може краще дотримуватись:

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

3

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

Іншою перевагою Enum є те, що його легше читати.


2

Якщо метод задає таке питання, як:

KeepWritingData (DataAvailable());

де

bool DataAvailable()
{
    return true; //data is ALWAYS available!
}

void KeepWritingData (bool keepGoing)
{
   if (keepGoing)
   {
       ...
   }
}

Аргументи булевого методу, здається, мають абсолютно ідеальний сенс.


Колись вам потрібно буде додати "продовжуйте писати, якщо у вас є вільний простір", і тоді ви все одно перейдете від bool до перерахунку.
Ілля Риженков

І тоді у вас будуть зміни, або застарілі перевантаження, або може бути щось на кшталт KeppWritingDataEx :)
Ілля Риженков,

1
@Ilya чи, можливо, не станеш! Створення можливої ​​ситуації, коли жодної на даний момент не існує, не заперечує рішення.
Джессі К. Слікер

1
Право Джессі. Планувати такі зміни, як нерозумно. Робіть те, що має сенс. У цьому випадку булевий і інтуїтивно зрозумілий. c2.com/xp/DoTheSimplestThingThatCouldPossblyWork.html
Дерек Парк,

@Derek, в цьому випадку булевий навіть не потрібен, тому що DataAvailable завжди повертається вірно :)
Ілля Риженков,

2

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

CommentService.SetApprovalStatus(commentId, false);

Однак у більшості випадків, наприклад, згаданий вами приклад, краще використовувати перерахування. У самій .NET Framework є багато прикладів, коли цієї конвенції не дотримуються, але це тому, що вони запровадили цю інструкцію щодо дизайну досить пізно в циклі.


2

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


1
А як щодо попереднього планування як третього можливого варіанту? ;-))
Ярик

2

Енуми, безумовно, можуть зробити код більш читабельним. Є ще кілька речей, на які слід стежити (як мінімум у .net)

Оскільки базовим сховищем enum є int, значення за замовчуванням буде нульовим, тому ви повинні переконатися, що 0 є розумним за замовчуванням. (Наприклад, у структурах для всіх полів встановлено нуль, тому немає способу вказати за замовчуванням, відмінного від 0. Якщо у вас немає значення 0, ви навіть не можете перевірити перерахунок без кастингу до int, що було б поганий стиль.)

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

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

Я можу законно написати

WriteMode illegalButWorks = (WriteMode)1000000;
file.Write( data, illegalButWorks );

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

if (!Enum.IsDefined(typeof(WriteMode), userValue))
    throw new ArgumentException("userValue");

Єдине застереження Enum.IsDefined, що воно використовує рефлексію і повільніше. Він також зазнає проблеми з версією. Якщо вам потрібно часто перевіряти значення перерахунку, вам краще буде виконати наступне:

public static bool CheckWriteModeEnumValue(WriteMode writeMode)
{
  switch( writeMode )
  {
    case WriteMode.Append:
    case WriteMode.OverWrite:
      break;
    default:
      Debug.Assert(false, "The WriteMode '" + writeMode + "' is not valid.");
      return false;
  }
  return true;
}

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

Ще більше розваг можна зробити із [Flags]перерахунками, і код перевірки для цього дещо інший.

Я також зазначу, що для портативності вам слід використовувати дзвінок ToString()на enum та використовувати Enum.Parse()під час їх читання. І те ToString()й Enum.Parse()інше[Flags] інше enum, тому немає причин не використовувати їх. Майте на увазі, це ще одна помилка, тому що зараз ви навіть не можете змінити назву переліків, не маючи порушення коду.

Отже, іноді вам потрібно зважити все вищесказане, коли ви запитуєте себе: чи можу я піти просто з булом?


1

ІМХО, схоже, що переконання було б очевидним вибором для будь-якої ситуації, коли можливі більше двох варіантів. Але точно існують такі ситуації, коли булеве - все, що вам потрібно. У цьому випадку я б сказав, що використання enum, де bool працюватиме, буде прикладом використання 7 слів, коли 4 зробить.


0

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

Однак, ви також можете використовувати False та True (булеві 0 та 1), а потім, якщо вам знадобиться більше значень пізніше, розгорніть функцію, щоб підтримувати визначені користувачем значення (скажімо, 2 та 3) та ваші старі значення 0/1 буде гарненько порт, тому ваш код не повинен зламатися.


0

Іноді просто спростити моделювання різної поведінки із перевантаженнями. Продовжувати ваш приклад було б:

file.appendData( data );  
file.overwriteData( data );

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

Енуми можуть у деяких випадках зробити код більш читабельним, хоча перевірити точне значення перерахунку в деяких мовах (наприклад, C #) може бути складно.

Часто булевий параметр додається до списку параметрів як нове перевантаження. Один із прикладів .NET:

Enum.Parse(str);  
Enum.Parse(str, true); // ignore case

Остання перевантаження стала доступною в пізнішій версії .NET-бази, ніж перша.

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


EDIT

У новіших версіях C # можна використовувати аргументи з назвою, які, IMO, можуть зробити викличний код яснішим так само, як і перерахунки. Використовуючи той же приклад, що і вище:

Enum.Parse(str, ignoreCase: true);

0

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

напр

public void writeData(Stream data, boolean is_overwrite)

Любіть енумів, але булевий теж корисний.


0

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

Вбудований коментар проходить довгий шлях до вирішення несподіваної boolпроблеми. Оригінальний приклад особливо грізний: уявіть, що намагаєтесь назвати змінну в декларації функції! Це було б щось на кшталт

void writeData( DataObject data, bool use_append_mode );

Але, для прикладу, скажімо, це декларація. Потім, для інакше нез'ясованого булевого аргументу, я помістив ім'я змінної у вбудований коментар. Порівняйте

file.writeData( data, true );

з

file.writeData( data, true /* use_append_mode */);

-1

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


-1

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

var v = CallMethod(pData = data, pFileMode = WriteMode, pIsDirty = true);

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

C # 3.0 дозволяє називати аргументи в конструкторах. Я не знаю, чому вони також не можуть зробити це методами.


Цікава ідея. Але чи зможете ви змінити параметри? Опустіть параметри? Як компілятор дізнається, до якого перевантаження ви зобов’язуєтесь, якби це було необов’язково? Також, чи потрібно було б назвати всі параметри у списку?
Дрю Ноакс

-1

Булеві значення true/ falseлише. Тож незрозуміло, що це таке. Enumможе мати значуще ім'я, наприклад OVERWRITE, APPENDі т.д. Так перерахування краще.

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