Все статьи | Код

Совершенный код: избавляйтесь от строк

Совершенный код: избавляйтесь от строк главное изображение

Веб-программирование насквозь состоит из манипулирования строковыми данными. Данные в базе, данные в JSON (который тоже строка), данные в коде (SQL-запросы, списки). Часть этих строк на код не влияет, это просто данные, которые гоняются из базы пользователю и обратно. Другие данные задействованы в логике приложения и серьезно влияют на устойчивость к ошибкам и скорость их обнаружения.

Ниже я расскажу, как правильно организовать логику, завязанную на строки, и повысить качество кода. Начнем с типичного примера. Представьте, что где-то в коде приложения есть такая проверка:

if (order.status === 'delivering') { // Если заказ доставляется
  // какая-то логика
}

Подобных проверок очень много в любом веб-проекте. Мы постоянно анализируем статусы, роли, доступные возможности, соответствия значениям из каких-нибудь списков и так далее. Большинство этих данных представлено в виде строк, поэтому и проверки выглядят как сравнение со строками.

if (user.role === 'admin') {
  // делаем что-то важное
}

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

Автоматизированные инструменты не гарантируют корректность такого сравнения и не помогают, если оно построено неверно. Представьте себе две ситуации:

  • программист мог допустить ошибку в слове 'delivering';
  • программа изменилась, и статуса 'deliviring' больше нет.

Что произойдет с проверкой в любой из этих ситуаций? Правильный ответ: ничего. Мы не узнаем об ошибочной проверке, пока не увидим ее косвенное влияние на поведение программы. Например, пока что-то не вывелось или не посчиталось неверно. Иногда это может произойти быстро, а иногда приходится ждать долго. В такой ситуации вы можете узнать об ошибке от пользователей, которые столкнулись с проблемами. Частично эту проблему могут решить тесты, но они помогают далеко не всегда. Если от этого условия зависит вывод блока на экране, то тесты почти наверняка его не обнаружат. Кроме того, такой код порождает дублирование. При любых изменениях придется искать все места и править их.

Продолжайте учиться На Хекслете есть блок «Треки», где собраны курсы как для опытных программистов, так и для начинающих, которые хотят углубить свои знания про разработку.

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

// Внутри обработчика http-запроса
// Выполняется редирект на страницу пользователя
response.redirectTo(`/users/${user.id}`);

Возникает ровно такая же ситуация. Адрес строится как строка. Что, если он поменяется? Программист снова не узнает о проблеме в момент ее появления. Более того, на практике подобные ошибки могут не проявлять себя месяцами, пока какой-то пользователь не пожалуется, что получает 404.

И последний пример:

const sql = 'select * from articles where status="published"';

И если ошибок в SQL база данных не прощает, то перепутать значение легко, и никто в такой ситуации не поругается. Даже если значение правильное, нет никакой гарантии, что оно не изменится в будущем. А значит код, который работает сегодня, завтра может работать неверно. И снова непонятно, в какой момент проблема станет известной.

Замена строк

Описанные подходы немного разнятся для статических и динамических языков.

Основной способ решения данной проблемы – замена строк на вызовы функций. Их можно писать самим, а можно пользоваться готовыми инструментами. И это лучший вариант. Например, в хороших фреймворках есть механизм, позволяющий строить ссылки на базе существующего роутинга. При таком подходе любые несоответствия начнут порождать исключения. Кроме того, подобный механизм позволяет менять готовую ссылку без необходимости переписывать весь код везде, где она используется.

// route строит адрес, базируясь на имени маршрута и переданных параметрах: /users/1.
// Этот код не просто формирует строку внутри, но и проверяет наличие маршрута и его корректность.
// Это стандартный механизм любого взрослого фреймворка. Имя функции может отличаться,
// но принцип остается тем же.
response.redirectTo(route('user', user));

// Если поменяется роутинг так, что начнет формироваться другой урл,
// Код трогать не придется

Даже если приходится работать с адресами вне роутинга (например, если пользователь вводит адрес сам), всегда можно выкрутиться. Обработку таких адресов лучше выполнять через специальные функции, которые сами проверяют их корректность. Во многих языках подобная функциональность доступна прямо в стандартной библиотеке:

import { URL } from 'url';

// Если адрес неверный, то получим ошибку
const url = new URL('https://yandex.ru');
url.schema; // https

То же самое касается работы с путями в файловой системе. Вместо ручной сборки есть готовые функции, которые могут защитить от опечаток:

import path from 'path';
const pathToIndex = path.join(__dirname, '..', 'index.js');

// Неправильный подход
// const pathToIndex = `${__dirname}/../index.js`);

В случае проверок на соответствие поможет создание перечисления (enum). В одних языках это встроенный механизм, в других библиотека, а иногда и просто самописный код. Идея этого подхода — замена строк на обращение к значениям из специального описания, где есть все возможные значения. Это описание должно использоваться всеми частями программы, которые на него завязаны. В таком случае изменение этой структуры приведет к ошибкам там, где есть неверные обращения с точки зрения нового кода. Ошибка будет выглядеть примерно так: «Такого значения не существует».

// Теоретический пример
const userRolesEnum = enum('guest', 'user', 'admin', 'manager');

// В более сложных ситуациях это будут не просто строки, а новая связь в базе данных
if (user.role === userRolesEnum('admin')) {
  // делаем что-то важное
}

 // или так: user.hasRole('admin')
// а внутри уже правильная проверка

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

// Да, внутри будет простое сравнение со строкой, но по крайней мере ровно в одном месте,
// что не только снижает дублирование, но и уменьшает вероятность ошибиться
// один раз заработало для одного случая, заработает и для всех остальных
if (user.isAdmin()) {
  // делаем что-то важное
}

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

Дополнительные материалы:

Аватар пользователя Kirill Mokevnin
Kirill Mokevnin 16 февраля 2020
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
1 июня 10 месяцев
Иконка программы Python-разработчик
Профессия
Разработка веб-приложений на Django
1 июня 10 месяцев
Иконка программы PHP-разработчик
Профессия
Разработка веб-приложений на Laravel
1 июня 10 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
1 июня 10 месяцев
Иконка программы Fullstack-разработчик
Профессия
Новый
Разработка фронтенд и бэкенд компонентов веб-приложений
1 июня 16 месяцев
Иконка программы Верстальщик
Профессия
Вёрстка с использованием последних стандартов CSS
в любое время 5 месяцев
Иконка программы Java-разработчик
Профессия
Разработка приложений на языке Java
1 июня 10 месяцев
Иконка программы Разработчик на Ruby on Rails
Профессия
Создает веб-приложения со скоростью света
1 июня 5 месяцев