Почему я предпочитаю объекты, а не оператор switch
Недавно (или не совсем недавно, в зависимости от того, когда вы читаете эту статью), у меня была дискуссия с некоторыми товарищами по команде о том, как справляться с условиями, требующими многократной оценки, обычно в таких случаях люди любят использовать оператор switch или огромный if с несколько else if. В этой статье я собираюсь описать третий способ (подход, который я предпочитаю), мы будем использовать объекты для быстрого поиска соответствию условию.
Оператор switch
Оператор switch позволяет нам вычислять выражение и делать что-то особенное в зависимости от значения переданного выражения, обычно, когда вы учитесь писать код и алгоритмы, вы узнаете, что вы можете использовать его специально для нескольких вычислений, вы начинаете его использовать, и все выглядит хорошо, и вы быстро понимаете, что это дает вам большую свободу, да !, но будьте осторожны, большая свобода приходит с большой ответственностью.
Давайте быстро посмотрим, как выглядит типичный оператор switch:
switch (expression) { case x: { /* Your code here */ break; } case y: { /* Your code here */ break; } default: { /* Your code here */ } }
Отлично, теперь есть пара вещей, на которые вы, возможно, не обратили внимание:
Ключевое слово break необязательно.
Ключевое слово break позволяет нам остановить выполнение блоков, когда условие уже выполнено. Если не добавлять ключевое слово break в оператор switch, не будет ни какой ошибки. Но если ключевое слово break пропущено случайно, это может означать, что выполнится код, о котором вы даже не подозреваете, что также добавит несоответствия нашим реализациям, мутациям, утечкам памяти и уровням сложности при отладке проблемы. Давайте посмотрим на представление этой проблемы:
switch ('first') { case 'first': { console.log('first case'); } case 'second': { console.log('second case'); } case 'third': { console.log('third case'); break; } default: { console.log('infinite'); } }
Если выполнить этот фрагмент кода в своей консоли, вы увидите, что типа такого:
firt case second case third case
Оператор switch выполнит блок во втором и третьем условие, даже если первое условие уже было правильным, затем он находит ключевое слово break в третьем условие и остановит выполнение, без предупреждений или ошибок в консоли.
Фигурные скобки в каждом условие НЕ являются обязательными.
Фигурные скобки представляют блоки кода в javascript, так как начиная с ECMAscript 2015 мы можем объявлять переменные с блочной областью с использованием операторов, таких как const или let, что отлично (но не так хорошо для switch), так как фигурные скобки не обязательны, мы можем получить ошибки, из-за дублирования переменных, давайте посмотрим, что произойдет, когда мы выполним код ниже:
switch ('second') { case 'first': let position = 'first'; console.log(position); break; case 'second': let position = 'second'; console.log(position); break; default: console.log('infinite'); }
мы получим:
Uncaught SyntaxError: Identifier 'position' has already been declared
Возникает ошибка, потому что переменная position уже была объявлена в первом условие, и поскольку там нет фигурных скобок, она поднимается, а затем, когда во втором условие пытаемся объявить ее же, то получается что она уже существует и BOOM.
А теперь представьте, что может произойти при использовании операторов switch без ключевого слова break и фигурными скобками:
switch ('first') { case 'first': let position = 'first'; console.log(position); case 'second': console.log(`second has access to ${position}`); position = 'second'; console.log(position); default: console.log('infinite'); }
В консоли будет следующее:
first second has access to first second infinite
Только представьте, количество ошибок и мутаций, которые могут быть внесены из-за этого… возможности безграничны … Во всяком случае мы пришли сюда, чтобы поговорить о другом подходе, мы пришли сюда, чтобы поговорить об объектах.
Объекты для более безопасного поиска условий
Поиск объектов выполняется быстро и быстрее по мере увеличения их размера, а также позволяет нам представлять данные в виде пар ключ-значение, что является превосходным условным выполнением.
Работа со строками
давайте начнем с чего-то простого, такого как примеры со switch, давайте предположим, что нам нужно сохранить и вернуть строку условно, используя объект, которые мы заранее подготовим:
const getPosition = position => { const positions = { first: 'first', second: 'second', third: 'third', default: 'infinite' }; return positions[position] || positions.default; }; const position = getPosition('first'); // Returns 'first' const otherValue = getPosition('fourth'); // Returns 'infinite'
Если вы хотите еще больше сократить количество кода, мы могли бы использовать функции со стрелками:
const getPosition = position => ({ first: 'first', second: 'second', third: 'third' }[position] || 'infinite'); const positionValue = getPosition('first'); // Returns 'first' const otherValue = getPosition('fourth'); // Returns 'infinite'
Это делает то же самое, что и предыдущая реализация, но мы использовали более компактное решение с меньшим количеством строк кода.
Давайте теперь будем немного более реалистичны, не все условия, которые мы напишем, будут возвращать простые строки, многие из них будут возвращать логические значения, выполнять функции и многое другое.
Работа с boolean
Мне нравится создавать свои функции таким образом, чтобы они возвращали согласованные типы значений, но, поскольку javascript — это язык с динамической типизацией, могут быть случаи, когда функция может возвращать динамические типы, поэтому я приму это во внимание в этом примере и сделаю функцию, которая возвращает boolean, undefined или string, если ключ не найден.
const isNotOpenSource = language => ({ vscode: false, sublimetext: true, neovim: false, fakeEditor: undefined }[language] || 'unknown'); const sublimeState = isNotOpenSource('sublimetext'); // Returns true
Выглядит отлично, правда ?, но подождите, похоже, у нас есть проблема … что произойдет, если мы вместо этого вызовем функцию с аргументом ‘vscode‘ или fakeEditor ?, ммм, давайте посмотрим:
- Он будет искать ключ в объекте.
- Он увидит, что значение ключа vscode равно false.
- Он попытается вернуть false, но, поскольку false || ‘unknown’ неизвестно, в итоге мы получим неправильное значение.
У нас будет такая же проблема для ключа fakeEditor.
О нет, хорошо, не паникуйте, давайте разберемся с этим:
const isNotOpenSource = editor => { const editors = { vscode: false, sublimetext: true, neovim: false, fakeEditor: undefined, default: 'unknown' }; return editor in editors ? editors[editor] : editors.default; }; const codeState = isNotOpenSource('vscode'); // Returns false const fakeEditorState = isNotOpenSource('fakeEditor'); // Returns undefined const sublimeState = isNotOpenSource('sublimetext'); // Returns true const webstormState = isNotOpenSource('webstorm'); // Returns 'unknown'
И это решает проблему, но … я хочу, чтобы вы спросили себя одну вещь: действительно ли это проблема здесь? Я думаю, что мы должны больше беспокоиться о том, зачем нам нужна функция, которая в первую очередь возвращает boolean, undefined или string, это серьезное несоответствие, в любом случае, это просто возможное решение для очень частного случая.
Работа с функциями
Давайте продолжим с функциями, часто мы находимся в положении, когда нам нужно выполнить функцию в зависимости от аргументов, давайте предположим, что нам нужно проанализировать некоторые входные значения в зависимости от типа ввода, но если указанный тип анализатора не будет зарегистрирован, то мы просто вернем значение:
const getParsedInputValue = type => { const emailParser = email => `email, ${email}`; const passwordParser = password => `password, ${password}`; const birthdateParser = date => `date , ${date}`; const parsers = { email: emailParser, password: passwordParser, birthdate: birthdateParser, default: value => value }; return parsers[type] || parsers.default; }; // We select the parser with the type and then passed the dynamic value to parse const parsedEmail = getParsedInputValue('email')('myemail@gmail.com'); // Returns email, myemail@gmail.com const parsedName = getParsedInputValue('name')('Enmanuel'); // Returns 'Enmanuel'
Если бы у нас была похожая функция, которая на этот раз возвращает другие функции, но без параметров, мы могли бы улучшить код для непосредственного возврата при вызове первой функции, что-то такого:
const getValue = type => { const email = () => 'myemail@gmail.com'; const password = () => '12345'; const parsers = { email, password, default: () => 'default' }; return (parsers[type] || parsers.default)(); // we immediately invoke the function here }; const emailValue = getValue('email'); // Returns myemail@gmail.com const passwordValue = getValue('name'); // Returns default
Общие кодовые блоки
Операторы switch позволяют нам определять общие блоки кода для нескольких условий.
switch (editor) { case 'atom': case 'sublime': case 'vscode': return 'It is a code editor'; break; case 'webstorm': case 'pycharm': return 'It is an IDE'; break; default: return 'unknown'; }
Как мы сделаем то же самое, используя объекты ?, мы могли бы сделать это следующим образом:
const getEditorType = type => { const itsCodeEditor = () => 'It is a code editor'; const itsIDE = () => 'It is an IDE'; const editors = { atom: itsCodeEditor, sublime: itsCodeEditor, vscode: itsCodeEditor, webstorm: itsIDE, pycharm: itsIDE, default: () => 'unknown' }; return (editors[type] || editors.default)(); }; const vscodeType = getEditorType('vscode'); // Returns 'It is a code editor'
И теперь у нас есть подход, который:
- Более структурирован.
- Лучше масштабируется .
- Проще в обслуживании.
- Легче тестировать.
- Безопаснее, имеет меньше побочных эффектов и рисков.
Вещи, которые стоит принять во внимание
Как и ожидалось, все подходы имеют свои недостатки, и этот не является исключением из правила.
- Поскольку мы используем объекты, мы будем занимать некоторое временное пространство в памяти для их хранения, это пространство будет освобождено благодаря сборщику мусора, когда область, в которой был определен объект, больше недоступна.
- Подход объектов может быть менее быстрым, чем операторы switch, когда не так много случаев для оценки, это может произойти, потому что мы создаем структуру данных, а затем получаем доступ к ключу, тогда как в switch мы просто проверяем значения и сразу его возвращаем.
Заключение
Эта статья не предназначена для того, чтобы изменить ваш стиль кодирования или заставить вас перестать использовать операторы switch, она просто пытается повысить осведомленность, чтобы ее можно было правильно использовать, а также открыла ваш разум для поиска новых альтернатив. Я просто поделился подходом, который мне нравится использовать, но есть и другие, например, вы можете взглянуть на вариант ES6 под названием сопоставление с образцом (pattern matching), если вам он не понравится, вы можете продолжить исследование.
ОК, разработчики будущего, вот и все, надеюсь, вам понравилась статья, если так то возможно вам понравиться и эта статья о фабричном паттерне (article about factory pattern). Кроме того, не забудьте поделиться этим и подписаться, вы можете найти меня в твиттере или связаться со мной по электронной почте duranenmanuel@gmail.com, до встречи в следующем.
Оригинальная статья: Enmanuel Durán — Why I prefer objects over switch statements
Оверинжиниринг на пустом месте. Используйте лопату, чтобы копать и молоток, чтобы забивать. Не наоборот! Оператор switch сразу говорит, что сейчас будет перебор вариантов и если он делает что-то не то, то круг ошибок ограничен. С объектами надо ещё понять, что это и зачем это и только потом найти ошибку. Если уж так хочется, то хотя бы коллекцию в отдельную константу вынести с ВОТ_ТАКИМ_НЕЙМИНГОМ или Map заюзать.
Да и вообще пишите семантический код! Об этом в один голос твердят все разработчики браузерных движков.
👍
🤝
Свич-кейс ужасен в плане читаемости и вохможностей. Редко когда он действительно нужен, в большитсве случаев он сильно избыточен. Нам крупно повезло что сегодня мы можем писать более элегантные решения с помощью хэш таблиц (хотя последний пример автора не лучший пример того что можно сделать, понимаю почему у новичков глаза вытекают).