Чи повинні програмісти використовувати логічні змінні для "документування" свого коду?


79

Я читаю Код Макконелла Complete Complete , і він обговорює використання булевих змінних для документування вашого коду. Наприклад, замість:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) || 
   (elementIndex == lastElementIndex)){
       ...
}

Він пропонує:

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

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


@Paul R - ну, дякую. Я розпочав із самодокументування і зрозумів, що для цього немає тегу, тому спробував змінити його на самодокументування, яке вже існувало. :)
froadie

Чи не краще просто коментувати те, що відбувається в тесті?
JS

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

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

2
Чому це питання все ще відкрите?
orangepips

Відповіді:


55

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

Деякі мови, які не мають поняття "присвоєння" як такі, як Haskell, навіть вводять спеціалізовані конструкції, які дозволять вам використовувати техніку "дайте ім'я підвиразу" ( whereпункт у Haskell) - здається, це говорить про деякі популярність відповідної техніки! -)


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

Я згоден. У багатьох випадках ти, мабуть, міг би втекти коротким коментарем, але я не розумію, як це насправді може зашкодити.
Макс Е.

16

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

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

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


Дякую за відповідь! Що стосується методів багаторазового використання, то у них є ще одна (не пов’язана) вагома причина, яку слід викласти з факторизму ... Тож, я гадаю, моє питання насправді полягає в тому, чи слід виключати одноразові булеві вирази, коли немає жодної причини, крім читабельності. (Що, звичайно, є досить великою причиною само по собі :)) Дякую, що вказали на це.
froadie

+1 для зменшення змін коду, необхідних при зміні логіки, і для глузування з програмістів з тисячорядковими функціями.
Jeffrey L Whitledge

9

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

У вашому прикладі я дивлюсь на код і запитую себе: "добре, чому людина, яка бачить значення, менше 0?" У другому ви чітко говорите мені, що деякі процеси закінчились, коли це відбувається. Не здогадуючись у другому, яким був ваш намір.

Найбільшим для мене є, коли я бачу такий метод, як: DoSomeMethod(true); Чому для нього автоматично встановлюється значення true? Це набагато читабельніше, як

bool deleteOnCompletion = true;

DoSomeMethod(deleteOnCompletion);

7
Мені не подобаються логічні параметри саме з цієї причини. У підсумку ви отримуєте дзвінки типу "createOrder (true, false, true, true, false)", і що це означає? Я вважаю за краще використовувати перерахування, тому ви кажете щось на зразок "createOrder (Source.MAIL_ORDER, BackOrder.NO, CustomOrder.CUSTOM, PayType.CREDIT)".
Джей,

Але якщо ви наслідуєте приклад Кевіна, це еквівалентно вашому. Яка різниця, чи може змінна приймати 2 значення або більше 2?
Марк Рузон

2
Завдяки Jay's ви можете мати перевагу, безумовно, бути зрозумілішим у певних випадках. Наприклад, у використанні типу PayType. Якби це було логічне значення параметра, імовірно, було б названо, isPayTypeCredit. Ви не знаєте, що таке альтернатива. За допомогою переліку ви можете чітко побачити, які варіанти є PayType: кредит, чек, готівка та виберіть правильний.
kemiller2002

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

Objective-C та Smalltalk дійсно вирішують цю проблему в Objective-C:[Object createOrderWithSource:YES backOrder:NO custom:YES type:kCreditCard];
Grant Paul

5

Наданий зразок:

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

Також можна переписати на використання методів, які покращують читабельність і зберігають логічну логіку (як зазначив Конрад):

if (IsFinished(elementIndex) || IsRepeatedEntry(elementIndex, lastElementIndex)){
   ...
}

...

private bool IsFinished(int elementIndex) {
    return ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
}

private bool IsRepeatedEntry(int elementIndex, int lastElementIndex) {
    return (elementIndex == lastElementIndex);
}

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


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

3

Єдиний спосіб, як я міг бачити, що це йде не так, - це якщо логічний фрагмент не має імені, яке має сенс, і ім’я все одно вибрано.

//No clue what the parts might mean.
if(price>0 && (customer.IsAlive || IsDay(Thursday)))

=>

first_condition = price>0
second_condition =customer.IsAlive || IsDay(Thursday)

//I'm still not enlightened.
if(first_condition && second_condition)

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

i++; //increment i by adding 1 to i's previous value

2
Ви неправильно згрупували умови у своєму прикладі, чи не так? "&&" пов'язується сильніше, ніж "||" у більшості мов, які їх використовують (сценарії оболонки є винятком).
Джонатан Леффлер

Паренс доданий. Отже, це був би черговий удар проти розбиття на іменовані змінні, він надає можливість змінити групування неочевидним чином.
MatthewMartin

2

Роблячи це

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

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

Ви навіть можете визначити весь блок як предикат:

bool ElementBlahBlah? (elementIndex, lastElementIndex);

і робити більше речей (пізніше) у цій функції.


І що важливіше, наступний розробник, який подивиться код, буде знати, що ви мали на увазі також! Це чудова практика, і я роблю це постійно.
Кріс Торнтон,

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

2

Якщо вираз є складним, тоді я або переміщую його до іншої функції, яка повертає booleg, isAnEveningInThePubAGoodIdea(dayOfWeek, sizeOfWorkLoad, amountOfSpareCash)або переглядаю код, щоб такий складний вираз не потрібен.


2

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

Так що:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) || 
   (elementIndex == lastElementIndex)){
   ...
}

Після перетворення стає:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) |
   (elementIndex == lastElementIndex)){
   ...
}

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


Правда - я просто помітив це зараз, коли повернувся до якогось мого коду, щоб переформатувати пару операторів if за допомогою цього методу. Був вираз if, який використовує оцінку короткого замикання за допомогою && (друга частина видає NPE, якщо перша частина хибна), і мій код зазнав невдачі, коли я переробив це (оскільки він завжди оцінював обидва і зберігав їх у логічній формі змінні). Гарна думка, дякую! Але мені було цікаво, коли я спробував це - чи є спосіб зберегти логіку у змінній і затримати її оцінку до фактичного оператора if?
froadie

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

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

@froadie Деякі мови, такі як Kotlin (а ​​незабаром і Dart), дозволяють "ледачі" змінні, які обчислюються лише при їх використанні. Як варіант, розміщення логіки у функції замість змінної матиме тут такий самий ефект. Просто, знаєте, на випадок, якщо ви все одно захочете знати це через 10 років.
hacker1024

2

Я думаю, що краще створювати функції / методи, а не тимчасові змінні. Таким чином збільшується читабельність ще й тому, що методи стають коротшими. У книзі Мартіна Фаулера «Рефакторинг» є хороші поради щодо покращення якості коду. Рефакторинг, пов’язаний із вашим конкретним прикладом, називається «Замінити темп на запит» та «Метод вилучення».


2
Ви хочете сказати, що, захаращуючи простір класів великою кількістю одноразових функцій, ви підвищуєте читабельність? Будь ласка, поясніть.
Зано

Це завжди компроміс. Читаність оригінальної функції покращиться. Якщо початкова функція коротка, можливо, вона не варта.
mkj

Також "захаращення простору класів", на мою думку, залежить від використовуваної мови та способу розділення коду.
mkj

2

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


1

якщо метод вимагає повідомлення про успіх: (приклади в c #) Мені подобається використовувати

bool success = false;

щоб почати. код помилковий, поки я не зміню його на:

success = true;

то в кінці:

return success;

0

Думаю, це залежить від того, який стиль ви / ваша команда віддаєте перевагу. Рефакторинг "Ввести змінну" може бути корисним, але іноді ні :)

І я мав би не погодитися з Кевіном у його попередньому дописі. Його приклад, я гадаю, придатний для використання, якщо введену змінну можна змінити, але вводити її лише для одного статичного логічного значення марно, оскільки ми маємо ім'я параметра в оголошенні методу, то навіщо дублювати його в коді?

наприклад:

void DoSomeMethod(boolean needDelete) { ... }

// useful
boolean deleteOnCompletion = true;
if ( someCondition ) {
    deleteOnCompletion = false;
}
DoSomeMethod(deleteOnCompletion);

// useless
boolean shouldNotDelete = false;
DoSomeMethod(shouldNotDelete);

0

З мого досвіду, я часто повертався до деяких старих сценаріїв і замислювався, "про що, блін, я тоді думав?". Наприклад:

Math.p = function Math_p(a) {
    var r = 1, b = [], m = Math;
    a = m.js.copy(arguments);
    while (a.length) {
        b = b.concat(a.shift());
    }
    while (b.length) {
        r *= b.shift();
    }
    return r;
};

що не так інтуїтивно, як:

/**
 * An extension to the Math object that accepts Arrays or Numbers
 * as an argument and returns the product of all numbers.
 * @param(Array) a A Number or an Array of numbers.
 * @return(Number) Returns the product of all numbers.
 */
Math.product = function Math_product(a) {
    var product = 1, numbers = [];
    a = argumentsToArray(arguments);
    while (a.length) {
        numbers = numbers.concat(a.shift());
    }
    while (numbers.length) {
        product *= numbers.shift();
    }
    return product;
};

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

0

Я рідко створюю окремі змінні. Що я роблю, коли тести ускладнюються, я вкладаю ІФ та додаю коментарі. Подібно до

boolean processElement=false;
if (elementIndex < 0) // Do we have a valid element?
{
  processElement=true;
}
else if (elementIndex==lastElementIndex) // Is it the one we want?
{
  processElement=true;
}
if (processElement)
...

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

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