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

С вами в очередной раз Костя Логиновских, ведущий разработчик и технический лидер внутреннего проекта в 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('Не забудь меня обработать, мистер разработчик')
}

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

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

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

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

Комментарии (4)


  1. Emelian
    30.01.2025 12:19

    : вы рефакторите, если вы меняете код, но не меняете функциональность приложения

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

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

    1. Вы создаете начальный прототип, с минимумом функционала, например, из проекта созданного мастером, допустим, Visual Studio. Обычно, у всех людей свои «представления о прекрасном», поэтому стандартный стиль программирования не всем может нравиться. Поэтому, здесь рефакторинг заключается в приведении базового кода в «божий вид».

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

    3. Постепенно увеличиваете количество итераций, в т.ч., используемых внешних модулей, в своем проекте. Кстати, это дает хорошую возможность делать «форк» для собственного проекта, когда нужно создать новое приложение, которое частично использует наработки из предыдущего проекта. Все же не придется делать лишнюю работу. В идеале, самодостаточные внешние модули можно использовать как в конструкторе, просто комбинируя их состав. Это направление я называю «модульным программированием».

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

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

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