Очистите свой JavaScript: Трансформация условных операторов
В ходе бесчисленных интервью и общения с коллегами-разработчиками я заметил одну общую черту: лучшие — это не те, кто слепо следует тенденциям или просто копирует то, что считается "лучшей практикой" по общему мнению. Скорее, это те, кто обладает глубоким пониманием своего дела. Эти люди могут убедительно объяснить, почему были приняты те или иные решения, понимая, что, как говорится в старой поговорке, есть много способов "снять шкуру с животного". Их решения принимаются не по прихоти, а на основе веских аргументов, всестороннего понимания потенциальных рисков и продуманных стратегий их снижения.
Это откровение вдохновило меня на создание серии статей Очистите свой JavaScript
. Этот сборник — не просто руководство по программированию, это глубокое погружение в искусство принятия субъективных решений в кодинге, которые поднимают человека от грамотного программиста до профессионала высокого уровня.
В этой серии статей мы не просто рассмотрим, как писать код. Мы погрузимся в тонкости написания чистого, удобного и надёжного JavaScript. Мы разберём различные подходы, оценим их достоинства и поймём их подводные камни. Каждый пост поможет вам принять взвешенное решение, подкреплённое вескими аргументами и чётким пониманием альтернативных решений и связанных с ними рисков.
Гипотетический случай со скидками на автомобили
Представьте себе напряжённый день среды, когда у меня включён плейлист с музыкой для программирования (который, кстати, очень разнообразен по жанрам). И тут я замечаю, что мой менеджер прислал мне код. Я знаю, что он работает над функцией расчёта скидок на автомобили, выбранные пользователем, которые будут отображаться в других частях системы.
Мне нравится читать чужой код, потому что я часто сравниваю его с тем, как бы я сам реализовал этот же код, и извлекаю уроки. Так я получаю возможность улучшить свой стиль кодирования, а также синхронизироваться со всем, что происходит вокруг, на случай, если мне понадобится помощь в других командах, где я не участвовал в первоначальной реализации.
И когда мой менеджер делает этот коммит, я замечаю, что это простая функция, рассчитывающая скидки в зависимости от бренда автомобиля. Она проходит все модульные тесты, так что функционально все в порядке. Но когда я смотрю на функцию, кое-что привлекает моё внимание. Давайте посмотрим, сможете ли вы понять, что это такое, в приведённом ниже фрагменте:
function calculateDiscount(carBrand) {
let discount;
switch(carBrand) {
case CAR_BRANDS.TOYOTA:
discount = 0.05; // 5% скидка
break;
case CAR_BRANDS.FORD:
discount = 0.07; // 7% скидка
break;
// Дополнительные случаи для других брэндов
default:
discount = 0;
}
return discount;
}
Что ж, надеюсь, вы увидите его, этот страшный оператор switch
. Я позвонил своему руководителю и сказал, что хочу пройтись с ним по части изменений его кода. Поскольку, у меня есть на примете одно улучшение, и я хочу обсудить его с ним.
Телефонный звонок
Я как раз просматривал ваш недавний коммит по функции скидок
, — начал я, как только мы оба оказались на линии. Это функционально и все такое, но я думаю, что можно было бы более элегантно обработать логику скидок
.
О? Я весь во внимании
, — ответил менеджер, испытывая любопытство.
Вы знаете, что мы всегда стремимся к чистому, сопровождаемому коду, верно? Так вот, я обратил внимание на оператор
.switch
для обработки скидок на автомобили. Сейчас оно работает хорошо, но я думаю о будущей масштабируемости и сопровождаемости
Мой менеджер на мгновение замолчал, а затем сказал: Продолжай
.
Я хотел обратить внимание на то, как легко это может привести к ошибкам по мере роста приложения
, — пояснил я.
Интересно. Можете привести пример?
— спросил менеджер.
Конечно, давайте рассмотрим сценарий, в котором мы случайно вводим новый
. Я быстро набросал фрагмент, чтобы проиллюстрировать свою мысль:case
без оператора break
function calculateCarDiscount(carBrand) {
let discount;
switch(carBrand) {
case CAR_BRANDS.TOYOTA:
discount = 0.05; // 5% скидка
break;
case CAR_BRANDS.FORD:
discount = 0.07; // 7% скидка
// Случайно забыл про break здесь
case CAR_BRANDS.BMW:
discount = 0.1; // 10% скидка
break;
// Дополнительные случаи для других брэндов
default:
discount = 0;
}
return discount;
}
В этом модифицированном варианте, если кто-то выберет Ford, он непреднамеренно получит скидку 10% вместо 7% из-за того, что она попадёт в кейс BMW
, — пояснил я.
А, я вижу проблему
, — признал менеджер. Это мелкий баг, который легко может остаться незамеченным
.
Именно так. И это ещё не все. Что если нам нужно применить сложную логику скидок для конкретного бренда? Оператор
. Продолжил я.switch
может стать раздутым и громоздким
Мы можем провести рефакторинг с использованием выражения
. Заявил я.if-else
вместо switch
, и это будет выглядеть примерно так
function calculateCarDiscount(carBrand) {
let discount;
if (carBrand === CAR_BRANDS.TOYOTA) {
discount = 0.05;
} else if (carBrand === CAR_BRANDS.FORD) {
discount = 0.07;
} else if (carBrand === CAR_BRANDS.BMW) {
discount = 0.1;
} // И так далее для других брендов
else {
discount = 0;
}
return discount;
}
Такой подход позволяет избежать проблемы "проваливания" выражений
, — продолжил я. switch
Но он создаёт свой собственный набор проблем. Во-первых, он не очень хорошо масштабируется. По мере добавления новых брендов автомобилей функция будет расти линейно, становясь все более громоздкой
.
Менеджер ответил: Я понимаю, о чем вы говорите. Длинная цепочка операторов
.if-else
может стать довольно громоздкой, и это не слишком улучшает сопровождаемость
Именно так
, — согласился я. А ещё есть аспект читабельности. Представьте, что вы прокручиваете десятки условий
.if-else
. Трудно быстро определить логику для конкретного бренда автомобиля. К тому же, как и выключатель, он нарушает Принцип Открытости/Закрытости
Итак, похоже, что
, — заключил менеджер. if-else
тоже не идеальное решениеЧто вы предлагаете использовать вместо этого?
Ну, я предлагаю использовать паттерн Стратегия
, — пробормотал я. Он аккуратно инкапсулирует логику скидок каждого бренда в отдельные сущности, делая код более управляемым и расширяемым. Добавление или изменение скидки становится вопросом обновления объекта или функции стратегии, без необходимости изменения основной логики нашей функции расчёта
.
Менеджер на мгновение задумался. Хорошо, тогда давайте выберем паттерн Strategy. Похоже, он решает наши проблемы гораздо лучше, чем
switch
или if-else
. Вы можете начать работу над рефакторингом кода?
Конечно
, — ответил я, желая внедрить более чистое и эффективное решение.
Рефакторинг
Хотя описанный выше сценарий может показаться несколько преувеличенным для наглядности, в реальной жизни вряд ли можно столкнуться с такой динамикой, когда руководитель оказывается менее информированным, чем джун. Как правило, опытные менеджеры хорошо знакомы с подобными практиками и принципами программирования. Однако в нашем гипотетическом диалоге я решил изобразить менеджера таким образом по определённой причине: чтобы подчеркнуть важность изучения и объяснения в процессе разработки программного обеспечения.
В действительности независимо от того, с кем вы обсуждаете проблему — со старшим менеджером, коллегой или даже джуном, — умение исследовать различные варианты программирования и аргументировать свой выбор является бесценным. Речь идёт не только о знании лучших практик, но и о понимании контекста и умении эффективно доносить свои решения.
Теперь давайте более подробно рассмотрим паттерн Стратегия. Мы рассмотрим, как этот паттерн может элегантно заменить традиционные switch case
или цепочки if-else
, улучшая читаемость, сопровождаемость и масштабируемость нашего кода.
Реализация паттерна "Стратегия"
Паттерн "Стратегия" предполагает определение семейства алгоритмов (в нашем случае — расчёт скидок для различных брендов автомобилей), инкапсуляцию каждого из них и обеспечение их взаимозаменяемости. Этот паттерн позволяет алгоритму меняться независимо от клиентов, которые его используют. Вот как мы можем применить его в нашем сценарии:
function getToyotaDiscount() {
return 0.05; // 5% скидка для Toyota
}
function getFordDiscount() {
return 0.07; // 7% скидка для Ford
}
function getBmwDiscount() {
return 0.1; // 10% скидка для BMW
}
const discountStrategies = {
[CAR_BRANDS.TOYOTA]: getToyotaDiscount,
[CAR_BRANDS.FORD]: getFordDiscount,
[CAR_BRANDS.BMW]: getBmwDiscount,
// При необходимости добавить дополнительные стратегии
};
Мне нравится называть это созданием стратегии, обратите внимание, что я не иду по пути наименьшего сопротивления. Я не пытаюсь использовать анонимные функции или простые числовые константы, а фактически создаю обработчики для каждого бренда автомобиля, что просто очевидно при просмотре этого кода.
Это ошибка, на которую, по моим наблюдениям, попадаются очень многие разработчики. Они считают, что меньше кода — это то же самое, что и хороший код. Но на самом деле хороший код может означать больше кода, и иногда это даже может показаться излишеством. Но в большинстве случаев разница заключается всего лишь в нескольких дополнительных строках кода, которые могут превратить ваш код из того, что отвечает только на текущие запросы, в то, что будет хорошо работать при любых ожидаемых изменениях, которые в мире программного обеспечения неизбежны.
Теперь, после определения наших стратегий, мы можем создать основную логику, которую я называю селектором стратегий. Это часть кода, использующая входные данные для определения того, какая стратегия должна быть вызвана.
function calculateCarDiscount(car) {
const discountStrategyHandler = discountStrategies[car.brand];
if(typeof discountStrategyHandler === 'function'){
return discountStrategyHandler(car);
}else {
// Реализация обработчика по умолчанию
return defaultBrandDiscountHandler(car);
}
}
Вот один из способов реализации нашей центральной логики. Обратите внимание, что теперь я принимаю в качестве входных данных не просто бренд, а весь car
. Также обратите внимание на то, что я передаю объект car
, несмотря на то, что предыдущим обработчикам он был не нужен. Я принимаю эти субъективные решения, потому что могу предвидеть использование, при котором расчёт скидки зависит не только от марки. Но, возможно, BMW использует разные стратегии для "внедорожников" и "седанов".
Поэтому я написал свою функцию таким образом, думая о возможных будущих случаях использования, а также заботясь о том, чтобы код не был хрупким. Не поймите меня неправильно, этот код не идеален. Например, из-за прототипной природы JavaScript я могу сломать этот код, если буду использовать автомобиль с брендом toString
. И исходя из того, как вы используете эту функцию, вы можете понять, как защитить свою центральную логику дальше.
Но в целом, глядя на код, который закоммитил менеджер, и на нашу текущую итерацию, мы понимаем, что в текущей итерации мы действительно можем легко прочитать алгоритм, и он будет понятен даже человеку, не очень хорошо знакомому с JavaScript.
Заключение
Суть нашей дискуссии сводится к принятию взвешенных решений. Неважно, будете ли вы использовать операторы switch
или предпочтёте альтернативные варианты, такие как паттерн "Стратегия", главное, чтобы ваш выбор был чётко и аргументированно обоснован. Важно уметь конструктивно излагать свои рассуждения, а не прибегать к защитным или пренебрежительным реакциям, когда ваш код подвергается критике.
Как разработчики, мы должны выйти за рамки бесполезных оправданий типа Даже Google так не делает
, а вместо этого стремиться к постоянному обучению и открытому содержательному диалогу о практике написания кода. Цель состоит в том, чтобы развиваться не только как отдельные разработчики, но и как участники более широкого сообщества разработчиков программного обеспечения, где обмен мнениями и обсуждение альтернативных вариантов обогащают понимание и мастерство каждого.
Я с нетерпением жду продолжения этого путешествия вместе с вами в следующей части "Очистите свой JavaScript". А пока удачного вам кодинга, и помните: сила вашего кода заключается как в его логике, так и в рассуждениях, которые лежат в его основе.