Со времен погружения в мир программирования я стремился писать код чище, проще, понятнее. Просто для того, чтобы самому разобраться в нем, если вдруг допущу какую-то ошибку. Шли годы, а предпочтения не менялись. Сложнее всего удержать в голове код, собранный по примеру матрешки: когда куски логики вложены один в другой на несколько уровней.
Умение «выпрямить» такой код — ценный навык. Сегодня хочу поделиться некоторыми из популярных проблем и подходов к их решению. С этого и начнется рубрика статей, в которых мы будем упрощать жизнь себе и нашим коллегам.
Сразу оговорюсь: список описанных ниже подходов не исчерпывающий и не претендует на полноту. Это лишь то, что чаще другого встречается и пригождается в моей фронтендерской практике.
Устраняем вложенные циклы
Циклы — особый ребенок. Далеко не всегда это критично и требует исправления. Поэтому прежде чем хвататься за клавиатуру и фиксить проблему, убедитесь что проблема действительно есть. Например, если в функцию не будет приходить коллекция длиной больше 100, я бы не стал беспокоиться.
Более того, вложенный цикл может быть наиболее читаемым решением стоящей перед кодом проблемы. Поэтому подумайте дважды. Если же решите править, рассмотрите одним из следующих методов.
Для примера, рассмотрим задачу поиска пересечения в двух массивах. Иначе говоря, функция должна найти общие элементы в этих коллекциях. Базовое решение может выглядеть как-то так
1. Примените встроенные методы массивов
Со встроенными методами решение становится вообще однострочным и очень даже читаемым. Это плюс. Из минусов, алгоритмическая сложность осталось той же.
function findCommonElements(arr1, arr2) {
return arr1.filter(item => arr2.includes(item));
}
2. Попробуйте хеш-таблицы
В JS наиболее близок к хеш-таблице встроенный объект Set. Им и воспользуемся. Кода чуть больше чем в первом случае, но мы уменьшили и алгоритмическую сложность за счет того что доступ к данным в хеш-таблице происходит за константное время.
function findCommonElements(arr1, arr2) {
const setOfArr2 = new Set(arr2);
return arr1.filter(item => setOfArr2.has(item));
}
3. Разбиение на отдельные функции
Здесь нам придется вспомнить встроенные методы обхода массивов и написать две небольшие функции. Для каждого элемента первого массива мы будем вызывать метод поиска его копии во втором, складывать найденные совпадения в массив и потом отдадим его в качестве результата
function findCommonElements(arr1, arr2) {
const result = [];
const findCopy = (element) => {
if(arr2.includes(element)) {
result.push(element);
}
}
arr1.map(findCopy);
return result;
}
Устраняем вложенные условия
Если вложенные циклы ухудшают производительность кода, то есть повышают его временнУю сложность, заставляют потеть в первую очередь процессор, то вложенные условия заставляют потеть человека который пытается в них разобраться. Они повышают так называемую когнитивную сложность.
При этом сложность может расти квадратично.
// Сумма
// Условий нет - из функции один выход
const sumTwoNumbers = (a, b) => a + b;
// Модуль числа
// Одно условие - из функции два выхода
const getAbsoluteValue = (a) => {
if(a >= 0) {
return a
} else {
return -a
}
};
// Вымышленная функция, которая берет большее число из двух и если оно нечетное, возвращает ближайшее к нему четное "сверху"
// Два условия (одно вложенное) - из функции четыре выхода
const nestedMagicFn = (a, b) => {
if(a > b) {
const isAEven = Boolean(a % 2);
if(isAEven) {
return a;
} else {
return a + 1;
}
} else {
const isBEven = Boolean(b % 2);
if(isBEven) {
return b;
} else {
return b + 1;
}
}
};
Основная задача этой совершенно бесполезной с практической точки зрения функции показать, что разобраться в таком коде непросто. Разные комбинации входных параметров на входе будут вести нас разными дорожками и это нормально. Проблема в том, что нам потребуется какой-то определенный когнитивный ресурс, чтобы найти ошибку, если она есть. А в коде выше она есть.
Именно эту функцию можно отрефакторить десятком разных способов. В данном случае пожалуй, воспользуемся самым простым — вынесем одинаковый код в отдельные функции. Перепишем, сохранив намеренно допущенный баг.
const getIsEven = (n) => Boolean(n % 2);
const noNestedMagicFn = (a, b) => {
const maxNumber = Math.max(a, b);
const isEven = getIsEven(maxNumber);
if(isEven) {
return maxNumber;
} else {
return maxNumber + 1;
}
};
Сейчас баг аллоцирован в одной строчке вместо двух (как в примере выше) и в процессе дебага мы можем быстро обнаружить, что мы некорректно вычисляем четность/нечетность. Исправлять, я думаю, понятно как.
Помимо этого приема, часто удобно применять еще и ранний выход. Для выдуманной выше функции прием неприменим. Поэтому выдумаем другую, которая выполняет какую-то связанную с заказом бизнес-логику и возвращает сообщение о статусе обработки
function processOrder(order) {
if (order) {
if (order.status === 'active') {
if (order.items && order.items.length > 0) {
if (order.paymentStatus === 'paid') {
// Здесь выполняем какую-то логику, связанную с обработкой заказа
return 'Заказ успешно обработан';
} else {
return 'Ошибка: заказ не оплачен';
}
} else {
return 'Ошибка: в заказе нет товаров';
}
} else {
return 'Ошибка: заказ неактивен';
}
} else {
return 'Ошибка: заказ не существует';
}
}
У нас есть несколько выходов из функции с ошибочным статусом. Подвинем их наверх и сделаем условия совсем-совсем плоскими. Если в функции выше мы надеялись на то что все нужные нам условия соблюдены, а потом по слоям обрабатывали исключительные случае, то ниже мы будем сначала обрабатывать исключения. Обрабатывать и выходить. Если ни один из них не отработает, то значит у нас есть все необходимое для выполнения бизнес-логики.
function processOrder(order) {
if (!order) {
return 'Ошибка: заказ не существует'; // Выходим
}
if (order.status !== 'active') {
return 'Ошибка: заказ неактивен'; // Выходим
}
if (!order.items || order.items.length === 0) {
return 'Ошибка: в заказе нет товаров'; // Выходим
}
if (order.paymentStatus !== 'paid') {
return 'Ошибка: заказ не оплачен'; // Выходим
}
// Здесь выполняем какую-то логику, связанную с обработкой заказа
console.log('Заказ обрабатывается');
return 'Заказ успешно обработан';
}
Устраняем вложенные тернарники
Если из всех правил, вам можно будет поднять серьезность обнаруженного линтером нарушения до уровня error лишь для одного правила из описанных сегодня, выбирайте no-nested-ternary. Помимо указанных выше проблем со вложенными условиями, подобные тернарники обладают просто отвратительным уровнем читаемости. Знаю разработчиков, которые и обычные-то тернарники предпочитают менять на обычные if/else, но мы не будем столь радикальны.
Придумаем притянутый за уши пример. Допустим у нас есть метод который высчитывает финальную стоимость товара
- Если товар акционный, умножает обычную стоимость на размер скидки
- В ином случае вычисляет обычную цену, которая может быть увеличена на определенный коэффициент, если пользователь зашел на сайт с айфона
const getFinalPrice = () => {
return isDiscount ? price * DISCOUNT_VALUE : isIphone ? price * IPHONE_PREMIUM : price
}
Простите. Мне самому было больно это писать. Можно сделать чуть лучше, если разбить построчно, но сильно это не поможет.
const getFinalPrice() => {
return
isDiscount ? price * DISCOUNT_VALUE :
isIphone ? price * IPHONE_PREMIUM : price
}
Хорошая новость в том, что если вы прочитали прошлый блок, вы уже знаете как это можно исправить. Просто вынесем логику в отдельные методы
const getDiscountedValue = () => price * DISCOUNT_VALUE;
const getNormalValue = () => isIphone ? price * IPHONE_PREMIUM : price;
const getFinalPrice = () => {
return isDiscount ? getDiscountedValue() : getNormalPrice();
}
Либо, если вы симпатизируете if/else-радикалам, то получится чуть более многословно, но, признаюсь, более читаемо
const getFinalPrice = () => {
if(isDiscount) return getDiscountedValue();
return getNormalPrice();
}
Добро пожаловать в плоский мир
Если вам удалось внедрить все или часть описанных выше советов, ваш код стал более плоским, линейным, а значит и более читаемым. Прямо сейчас коллегам будет проще разобраться с тем, что и как он делает, увидеть места где его можно улучшить и без раздражения указать эти места на код-ревью.
Будущий вы также окажетесь в выигрыше. Открыв этот кусок кода спустя какое-то время, чтобы поменять логику или исправить какую-то проблему, вы удивитесь, насколько же хорошо вы сработали когда-то давно. Вам будет проще разобраться с тем, что и как код делает, увидеть места где его можно улучшить.
Пишите просто, пишите чисто и всем нам будет жить радостнее.







