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

Откуда мне знать, что именно и главное как именно рефакторить?

Очевидно, что итоговый результат заранее неясен, иначе нам не нужен был собственно рефакторинг, мы бы просто выкидывали старый код и писали новый с нуля так, как спроектировали заранее. То есть, приступая к рефакторингу, мы не представляем “пункт назначения” во всех деталях.

Зато у нас есть каталог “запахов кода” и соответствующих им рефакторингов (см. “Рефакторинг: улучшение существующего кода” Мартина Фаулера). Можно идти по пути устранения всех проблем в коде и полагать, что в результате получится гарантированно хороший код. Однако и запахи, и рефакторинги неоднозначны, очень многие из них имеют “зеркальный” вариант. То есть, получается, что “хороший код” живет в каких-то границах (слишком много чего-то - плохо, слишком мало - тоже плохо), при этом границы довольно-таки субъективны.

К тому же совершенно непонятен порядок применения отдельных рефакторингов - итоговый результат от него явно зависит (например, если слишком рано применить “Замену условного оператора полиморфизмом”, то можно лишить себя возможности вывести более правильные абстракции). Иногда советуют исправлять в первую очередь наиболее очевидные проблемы в коде, но это как раз может принести больше вреда, чем пользы. Выходит, что руководствуясь только каталогами, гарантированно получить хороший код в общем случае нельзя.

А на что еще опираться? На метрики - нельзя, потому что отдельные шаги рефакторинга значительно ухудшают показатели. Сэнди Метц приводит пример ABC-метрики (показатель когнитивной сложности кода, чем больше - тем хуже), которая на протяжении всего процесса рефакторинга постоянно растет, и только на каком-то пятнадцатом или около того шаге резко падает. Так что метрики в процессе не очень помогают (хотя, возможно, есть какие-то более полезные метрики, но я о таких не знаю).

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

  1. Аккуратно разделить код на Действия (побочные эффекты, изменения состояния) и Вычисления (чистые функции). Каждый метод должен быть либо тем, либо другим, смешивать нельзя. Частный случай такого разделения - CQRS (Command-Query Request Separation).
  2. Вытеснить Действия на самый верхний (самый мелкий) возможный уровень вложенности в стеке вызовов. Причина в том, что если Действие оказывается где-то глубоко в стеке, то все функции выше по стеку тоже превращаются в Действия, то есть получают побочные эффекты. Нам же нужно максимально изолировать все Действия и ограничить область их влияния.
  3. Высока вероятность, что в ходе этих преобразований функции станут довольно мелкими (то есть будут вычислять по одному значению каждая). Если же останутся цепочки вычислений внутри одной функции, то их нужно разделить. Цель на этом этапе - сделать все функции ответственными только за одно вычисление. Таким образом можно будет избавиться от значительной части дублированного кода, т.к. отдельные мелкие вычисления можно будет переиспользовать в нескольких местах.
  4. На предыдущем этапе код должен естественным образом разложиться по уровням абстракции. На одном уровне будут все низкоуровневые вычисления, уровнем выше - композиции из низкоуровневых, еще выше - еще более сложные композиции, как-то так. Теперь с этим кодом можно работать исходя из принципов ООП - объединять функции, работающие с одними и теми же входными данными, в классы, и выявлять высокоуровневые абстракции, соответствующие бизнес-сущностям.

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