javascript

«Я делаю рефакторинг ежечасно» или как за пять минут улучшить приложение

  • пятница, 31 января 2025 г. в 00:00:07
https://habr.com/ru/companies/cloud_ru/articles/877762/

История этой статьи началась с того, что я вспомнил о довольно известном высказывании Мартина Фаулера, автора книг и статей по архитектуре ПО, которое нередко вызывает недопонимание (во всяком случае так было у меня) — «Я делаю рефакторинг ежечасно». Первая мысль, которая логично возникает после этого высказывания — уважаемый публицист просто лукавит. Вторая — что, наверное, кроме рефакторинга он в своей жизни ничем больше не занимается. Но так ли это?

С вами в очередной раз Костя Логиновских, ведущий разработчик и технический лидер внутреннего проекта в Cloud.ru. В этой статье предлагаю во всем разобраться и обсудить, как можно делать рефакторинг «за пять минут» и улучшить приложение буквально за утренним кофе.

Спойлер: на самом дел рефакторинг ооочень мал
Спойлер: на самом дел рефакторинг ооочень мал

Чтобы устранить разночтения, сначала определим, что считать рефакторингом. Есть очень простое определение: вы рефакторите, если вы меняете код, но не меняете функциональность приложения.

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

Некоторые из этих поинтов могут показаться спорными — например, что значит «улучшает читаемость»? Можно ли читаемость как-то измерить? Оказывается, можно!

Как измерить читаемость кода

Читаемость кода можно приравнять к «когнитивной сложности кода», которая включает нескольких аспектов:

  • использование редких конструкций когда разработчику очень сложно вспомнить, как именно работают примененные конструкции кода (также известные как «ниндзя-код»);

  • использование «оперативной памяти программиста» — когда разработчика заставляют держать в голове конструкции, которые необходимы для понимания кода (например, когда название переменной не позволяет точно определить, что именно в ней находится);

  • цикломатическая сложность — математическое измерение читаемости кода, на котором я остановлюсь немного подробнее.

Если с первого раза удалось понять, что делает этот код — напишите в комментарии =)
Если с первого раза удалось понять, что делает этот код — напишите в комментарии =)

Если грубо, то цикломатическую сложность можно представить как количество всех циклов и ветвлений в коде — чем больше в вашей функции if, for и им подобных, тем сложнее будет читать ваш код, даже если он идеальный во всем остальном. Сама цикломатическая сложность — не тема статьи, поэтому, если хотите разобраться в нем подробнее, есть отличная статья на эту тему.

Как улучшить проект за чашкой кофе

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

Вариант 1. Введение объекта параметра

function getSomething(
  pagination: PaginationConfig, 
  sorter: SorterConfig
  _filter: inknown, 
  extra: unknown,
){}

У этой функции много проблем, но сейчас мы сфокусируемся только на одной из них —большом количестве позиционных параметров. Если нужно будет поменять какой-то из параметров, то придется перекопать всю кодовую базу, найти все использования этой функции и везде всё изменить. Более того, кто пишет на TypeScript, тот знает, что означает эта маленькая черточка перед названием. Фильтр в данном примере уже не используется. То есть когда-то он стал опциональным, а потом стал вообще ненужным. Так мы логически приходим к тому, чтобы сделать вот такое изменение — поставить фигурные скобки вокруг параметров и вывести их в тип:

function getSomething({pagination, sorter, extra}: SorterConfig){}

Что мы таким образом получим? Гибкость изменений, уберем лишний код (тот самый фильтерс) и потенциально ускорим разработку. И, когда нам в следующий раз в этом коде нужно будет что-то поменять (да, я говорю когда, а не если 🙂), мы это сделаем гораздо быстрее и проще.

Вариант 2. Извлечение функции

В этом примере у нас появилась очень большая функция (и давайте не будем задаваться вопросом, кто это наделал):

Это пример из реального проекта — цикломатическая сложность 233!
Это пример из реального проекта — цикломатическая сложность 233!

У цикломатической сложности есть одно важное правило: если в коде есть цикломатическая сложность «а» и «б» — два блока, следующих друг за другом, то в сумме они будут давать приблизительно «а + б». Это не совсем точно, но примерно так. Соответственно, эта математика работает и в обратную сторону. Если вы из функции вырезаете какой-то блок с цикломатической сложностью, то вы уменьшаете всю функцию на эту сложность.

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

Вариант 3. Перемещение инструкций

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

const shouldDisplayNotification = 
  userPreferences.notifyByEmail === 'NOTIFY' ||
  notificationSettings.level === NOTIFICATION_LEVEL.HIGH;
const hasUserAccess = 
  user.role === roles.ADMIN ||
  user.permissions.includes(PERMISSIONS.VIEW_DASHBOARD);
const isPurchaseAllowed = 
  customer.credits >= item.price ||
  customer.membershipStatus === MEMBERSHIP.PREMIUM;
const canSubmitForm = 
  formData.isValid &&
  userSession.isActive === SESSION_STATUS.ACTIVE;

Мало того, что здесь очень много if (мы можем бегло прикинуть, что сложность у этого кода достаточно большая), так еще и код невозможно прочитать. Выносить все эти параметры в отдельную функцию нецелесообразно. Поэтому в качестве рефакторинга можно банально добавить пробелы между строками. Это очень просто и настолько эффективно, что вы удивитесь, насколько станет легче 🙂. 

const shouldDisplayNotification = 
  userPreferences.notifyByEmail === 'NOTIFY' ||
  notificationSettings.level === NOTIFICATION_LEVEL.HIGH;

const hasUserAccess = 
  user.role === roles.ADMIN ||
  user.permissions.includes(PERMISSIONS.VIEW_DASHBOARD);

const isPurchaseAllowed = 
  customer.credits >= item.price ||
  customer.membershipStatus === MEMBERSHIP.PREMIUM;

const canSubmitForm = 
  formData.isValid &&
  userSession.isActive === SESSION_STATUS.ACTIVE;

Код всё тот же, но, согласитесь, читать его значительно приятнее?

Вариант 4. Использование встроенного метода

Еще один простой способ, который подойдет для всех проектов старше трех лет — использовать системные функции вместо кастомных. Бывает, что спецификация языка создает для нас инструменты, которые раньше были недоступны, и мы можем встретить в коде нечто подобное:

function stableSort() {/*...*/}

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

Вариант 5. Введение утверждения

Единственный из всех предложенных мной способов, который ухудшает читаемость, но при этом защищает нас от багов, особенно в сочетание с правилом @typescript-eslint/no-non-null-assertion:

const product = productsDictionary[productId]!

Этот восклицательный знак после выражения говорит о том, что мы, по неизвестной причине, уверены, что этот productId точно есть в этом объекте. Но почему? Может быть мы только что его запросили, может быть он запросился в соседнем файле или был добавлен на предыдущей строке? В любом случае TypeScript нам не верит — он говорит, что такого ID здесь может и не быть. И он прав — такого ID действительно рано или поздно не будет. Соответственно, мы должны прокинуть ошибку — сообщаем TypeScript, что готовы признать, что этого productId может и не быть:

const product = productsDictionary[productId]; // product: Product | undefined

if (!product) {
  throw new Error('Не забудь меня обработать, мистер разработчик')
}

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

Как убедить бизнес, что рефакторинг нужен: заключение

Но у нас остался последний вопрос — как убедить бизнес в необходимости рефакторинга? Для этого вернемся к фразе, с которой я начал эту статью. Мартин Фаулер говорит, что делает рефакторинг ежечасно. Как? Всё просто — он делает простой рефакторинг, о чем, кстати, и пишет в своих книгах. И в этом вся соль — он ни о чем не сообщает бизнесу и ни с кем не договаривается, потому что вкладывает рефакторинг в свои задачи инкрементально. Он просто делает свой проект лучше. И давайте мы все будем делать свои проекты лучше каждый день, оставляя код после себя лучше, чем он был.

Задавайте вопросы в комментариях — буду рад на все ответить. Но если захотите меня там поругать, то помните, что за моей статьей стоит Мартин Фаулер 😊.