Пожалуйста, не пишите запутанные условия!

Вам случалось потратить больше нескольких секунд, вглядываясь в одну строку кода? Часто ли вам попадались условия, которые было сложно разобрать? В этой статье мы поговорим о том, как в нашем коде проявляются запутанные условия. Попутно мы также рассмотрим различные методы рефакторинга для улучшения читабельности условных потоков управления.

Двойные отрицания

Двойное отрицание возникает, когда мы отрицаем переменную, имя которой уже имеет неутвердительный стиль. Рассмотрим следующий пример.

// Неутвердительный стиль
const isNotReady = doSomething();
const isForbidden = doSomething();
const cannotJoinRoom = doSomething();
const hasNoPermission = doSomething();

// Отрицание сразу приводит к странным двойным отрицаниям
console.log(!isNotReady);
console.log(!isForbidden);
console.log(!cannotJoinRoom);
console.log(!hasNoPermission);

Это противоположно утвердительному стилю, который легче разобрать, поскольку, чтобы понять логику имени переменной, не нужно продираться сквозь путы отрицания. То есть смысл, заложенный в имени утвердительной переменной, считывается как есть — без лишних усилий! Рассмотрим следующие модификации предыдущего примера.

// Утвердительный стиль
const isReady = doSomething();
const isAllowed = doSomething();
const canJoinRoom = doSomething();
const hasPermission = doSomething();

// А здесь отрицание имеет смысл!
console.log(!isReady);
console.log(!isAllowed);
console.log(!canJoinRoom);
console.log(!hasPermission);

К сожалению, неутвердительные имена переменных порой неизбежны из-за стандартов и обратной совместимости. Возьмем, к примеру, свойство HTML DOM APIs — HTMLInputElement#disabled.

Наличие атрибута disabled в теге <input> говорит браузеру (визуально и буквально) отключить элементы управления. А если этого атрибута нет, <input> проявляет свое поведение по умолчанию — принимает пользовательский ввод. Это прискорбный побочный эффект эргономичности HTML.

<!-- Normal Checkbox (Default) -->
<input type="checkbox" />

<!-- Disabled Checkbox -->
<input type="checkbox" disabled />

Тем не менее, по возможности следует отдавать предпочтение утвердительным именам. Они легче читаются и разбираются просто потому, что нет необходимости постоянно мысленно отрицать имя переменной. Это правило относится как к именам переменных, так и к именам функций.

От редакции Techrocks: о выборе имен читайте в статье «Нейминг: как давать осмысленные имена переменным».

Неутвердительные управляющие потоки

Следующая форма двойного отрицания немного более тонкая.

// Предположим, что мы придерживаемся утвердительного стиля
const isAllowed = checkSomething();
if (!isAllowed) {
    // Это как-то смешно...
    doError();
} else {
    // Замечаете, что блок `else` это практически двойное отрицание?
    doSuccess();
}

Как видите, неутвердительный стиль может проникать и в условный поток управления. Вспомним, что блок else практически является отрицанием соответствующего условия if. Поэтому мы должны использовать здесь утвердительный стиль. На самом деле исправление довольно простое.

// Просто инвертируйте логику!
const isAllowed = checkSomething();
if (isAllowed) {
    doSuccess();
} else {
    doError();
}

То же правило касается проверок на равенство и неравенство.

// ❌ Так не делайте!
if (value !== 0) {
    doError();
} else {
    doSuccess();
}

// ✅ Лучше так.
if (value === 0) {
    doSuccess();
} else {
    doError();
}

Некоторые могут даже позволить условному блоку быть пустым, чтобы просто отрицать условие в утвердительном стиле. Хотя я не призываю всех идти таким путем, я понимаю, почему для некоторых людей это может быть более читабельным. Возьмем, к примеру, оператор instanceof, для которого не так просто составить отрицание (тут нужны скобки).

if (obj instanceof Animal) {
    // Намеренно оставлено пустым.
} else {
    // Собственно работа производится здесь (в отрицании).
    doSomething();
}

Исключение для ранних возвратов

Небольшое замечание: существуют исключения для условных потоков управления с ранним возвратом. В таких случаях отрицание может оказаться необходимым.

if (!isAllowed) {
    // Тут ранний возврат.
    doError();
    return;
}

// В противном случае переходим на ветку успешного пути.
doSuccess();

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

// Отдавайте предпочтение утвердительным ранним возвратам.
if (isAllowed) {
    doSuccess();
    return;
}

// Если бы мы не инвертировали логику, это было бы
// вложено в условный блок `!isAllowed`.
if (!hasPermission) {
    doPermissionError();
    return;
}

// Если всё провалено, делай что-то еще.
doSomethingElse();
return;

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

// Да здравствует утвердительный стиль!
if (isAllowed) {
    doSuccess();
} else if (hasPermission) {
    doSomethingElse();
} else {
    doPermissionError();
}
return;

Конечно, существует множество других способов замены, инверсии и рефакторинга кода — достоинства каждого из них абсолютно субъективны. Таким образом, сохранение утвердительных имен становится своего рода искусством. В любом случае, читаемость кода будет улучшаться, если мы будем придерживаться общих принципов утвердительного стиля.

От редакции Techrocks: у нас есть и другие статьи на тему условий:

Сложные условия

С логическими операторами, такими как AND и OR, дело обстоит несколько сложнее. Например, как перевести приведенный ниже код в более утвердительный стиль?

// Это все хорошо, но должен быть лучший способ, верно?
// Тут просто слишком много отрицаний!
if (!isUser || !isGuest) {
    doSomething();
} else {
    doAnotherThing();
}

Для составных условий мы применяем законы де Моргана из булевой алгебры!

// Предположим, что это две **любые** булевы переменные.
let a: boolean;
let b: boolean;

// Следующие утверждения всегда справедливы
// для любых возможных пар значений `a` и `b`.
!(a && b) === !a || !b;
!(a || b) === !a && !b;

Благодаря законам де Моргана мы можем «распределить» отрицание внутри условия и затем «инвертировать» его оператор (заменить && на || и наоборот).

Хотя в приведенных ниже примерах используется только бинарное сравнение (т.е. два элемента), законы де Моргана можно распространить на любое количество условных переменных при условии соблюдения старшинства операторов. То есть, не забывайте, что сперва всегда оценивается оператор &&, а уж затем оператор ||.

// Пользуясь законами де Моргана, можно изменить отрицание:
if (!(isUser && isGuest)) {
    doSomething();
} else {
    doAnotherThing();
}
// Затем мы просто инвертируем логику, как мы делали в предыдущем разделе.
if (isUser && isGuest) {
    doAnotherThing();
} else {
    doSomething();
}

Не правда ли, так гораздо удобнее читать? Применяя законы де Моргана, мы можем очистить условные выражения, в которых слишком много отрицаний.

Заключение

Общая мысль должна быть очевидна. По возможности не следует заставлять читателя кода без необходимости напрягать мозг. В этой статье мы рассмотрели следующие приемы:

  • Придерживайтесь утвердительного стиля при выборе имен.
    • Избегайте отрицательных терминов/префиксов типа no, not, dis-, mal- и т.д.
    • Отдавайте предпочтение их положительным эквивалентам.
  • Инвертируйте условный поток управления (где это возможно), чтобы приспособить его к утвердительному стилю.
    • Не стесняйтесь поиграться с заменами, инвертированием и рефакторингом ветвей.
    • Ранние возвраты могут потребовать отрицаний.
  • Используйте приемы из булевой алгебры для инвертирования условных значений.
    • Законы де Моргана — особенно мощный инструмент для рефакторинга!

А теперь вперед, украшать мир более чистыми условными выражениями!

Перевод статьи «Please don’t write confusing conditionals».

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх