Я люблю рефакторинг. Действительно люблю. В процессе TDD (разработки через тестирование) по мере прохождения тестов я могу сосредоточиться исключительно на коде, дорабатывая и доводя его до совершенства. Эта часть процесса мне особенно нравится, учитывая свойственные мне удовольствие от создания новаций и стремление к продуктивности.
Помните Зеркало Еиналеж из первой книги о Гарри Поттере? Оно показывает человеку самое сокровенное желание сердца, и были люди, которые подменили свою реальную жизнь бесконечными часами ожидания, проведенными перед этим зеркалом.
Очень точно сказал профессор Дамблдор:
Люди чахли перед зеркалом, зачарованные тем, что видели в нем, или сходили с ума, не понимая, то ли перед ними реальность, то ли потенциальная возможность.
И это, друзья, именно то, что рефакторинг делает со мной. Сам по себе он превосходен, но может стать моим личным безумием, которое я выбираю добровольно, поскольку воспринимаю его как продуктивную деятельность.
Я часто трачу время на улучшение и без того хорошего кода, хотя мог бы уже приступить к решению следующей задачи.
Обычно моя работа над проектом включает следующие этапы:
- Выбор требуемой функциональности.
- Написание для нее тестов.
- Реализация функциональности с целью прохождения тестов, чтобы убедиться в том, что первоначальный вариант решения пусть и не идеален, но приемлем.
- Рефакторинг кода и его оптимизация, цель которых проверить, что тесты продолжают проходить. Довожу код до совершенства.
- Отправка. Иногда это означает интеграцию с главной ветвью проекта и развертывание кода, а в каких-то случаях подразумевает необходимость сообщить клиенту, что функциональность готова для демонстрации.
В случае несвоевременной отправки кода можно предположить, что я застрял на 3-ем этапе. И затруднение, действительно, могло быть связано с поиском рабочего решения. Но чаще всего это происходит потому, что я слишком много времени уделяю 4-му этапу, настолько много, что текущая функциональность по-прежнему не готова для демо, а работа над последующей даже не начиналась.
Как же разглядеть ту черту, за которой рефакторинг из продуктивной деятельности превращается в одержимость?
Прежде всего нужно убедиться, что мы отчетливо понимаем его цель, а затем задать себе несколько вопросов, помогающих принять решение — продолжать ли рефакторинг или двигаться дальше.
Цели рефакторинга
Кстати, если вы еще не влюбились в рефакторинг (а вам это предстоит), советую прочитать книгу “Рефакторинг. Улучшение проекта существующего кода” Мартина Фаулера и Кента Бека. Это отличное пособие для начинающих с примерами, к которому я тоже периодически возвращаюсь при необходимости что-то уточнить или освежить в памяти.
Раз уж мы вспоминаем цели рефакторинга кода, посмотрим, что по этому поводу пишет в своей книге М. Фаулер. Процитируем дословно фрагмент из второй главы.
- Рефакторинг улучшает проектирование ПО.
- Рефакторинг упрощает понимание ПО.
- Рефакторинг помогает находить ошибки.
- Рефакторинг ускоряет написание программ.
Превосходно. Цели нам известны, теперь остается научиться устанавливать границы рефакторинга, чтобы найти баланс между улучшением существующего кода и переходом к написанию нового. Для этого сначала рассмотрим, какие виды рефакторинга нам следует применять, а затем критерии, указывающие на необходимость его завершения.
Каким видам рефакторинга отдать предпочтение?
Это глобальный вопрос, и ответ зависит от стиля вашего программирования. Все программисты, обладая разными типами логики, предрасположены к созданию и разного кода с запашком (code smells). С учетом этого ответы на следующие вопросы для каждого из нас будут отличаться. Но тем не менее данные рекомендации являются хорошей пищей для размышлений.
Какие виды рефакторинга всегда необходимы при написании нового кода?
Мы с женой обожаем вместе готовить. С течением лет мы поняли одну важную вещь — надо мыть посуду в процессе приготовления. Это занимает буквально пару минут, но зато какое невероятное облегчение нас ждет в финале, когда всё уже готово: нас не повергают в ужас горы грязной посуды, пока мы раскладываем еду по тарелкам, и мысли о предстоящем мытье сковородок не омрачают наш прием пищи.
Мойте посуду в процессе приготовления. Существуют виды рефакторинга, использование которых должно войти у вас в привычку при написании кода.
По мере расширения и углубления знаний о его видах, имеющихся в вашем распоряжении, их применение постепенно войдет у вас в неосознанную привычку. К этому надо стремиться. А пока рассмотрим 2 вида рефакторинга, почти всегда необходимых в процессе создания кода. Начните с них и со временем переходите к пополнению собственной коллекции наиболее для вас полезных.
Если функция, которую я пишу, становится все длиннее и длиннее (более 10 строк), то пора остановиться. Возможно, будет лучше написать несколько более компактных функций, которые она вызывает.
Если у меня возникает необходимость прокомментировать свой код, поясняя суть происходящего, то, скорее всего, следует извлечь данный блок кода в отдельную функцию и назвать ее таким образом, чтобы отпала потребность в комментарии.
Если я когда-либо ловлю себя на копировании блока кода из другого файла в тот, с которым работаю, то это очевидный повод преодолеть свою лень, поскольку этот фрагмент стремится стать переиспользуемым. Следует скопировать его в отдельный файл, чтобы он хранился только в одном месте. Таким образом, оба файла, которым нужна функция, смогут его вызвать.
Если мне часто встречается группа одинаковых переменных, обычно передаваемых вместе в качестве параметров функции, то целесообразно свести их в объект. Например, у нас есть несколько функций, которые всегда принимают 3 одинаковых параметра.
function sendEmail (firstName, lastName, email) { ... }
function logNotification (firstName, lastName, email) { ... }
function verifyIdentity (firstName, lastName, email) { ... }
Почему бы этим параметрам просто не стать одним объектом с тремя фрагментами данных?
function sendEmail (userObject) { ... }
function sendEmail (userObject) { ... }
function sendEmail (userObject) { ... }
Применение вышеуказанных видов рефакторинга во время написания кода не требует особых усилий, стоит лишь приучить себя к дисциплине и не лениться.
Введенный в привычку рефакторинг в процессе создания нового кода — это гарантия того, что вы не задержитесь на этапе заключительной доработки перед его обзором и последующей отправкой. Здесь нужна дисциплина, на выработку которой тоже могут уйти многие месяцы или даже годы.
Какие виды рефакторинга должны быть приоритетными на этапе обзора кода перед выполнением коммита?
Вплоть до этого этапа все мое внимание было сосредоточено на коде. Если я уже на стадии выполнения коммита, то либо код скоро попадет во всеобщее пользование, либо подвергнется проверке (или возможны оба варианта). И именно сейчас следует сконцентрироваться на его читаемости.
На этом этапе уже не стоит задаваться вопросом: “Это самый оптимальный способ решить данную задачу?” Лучше спросить себя: “Будет ли понятно мое намерение в этом блоке кода тем, кому предстоит его прочитать?”
При условии, что я уже готов к коммиту и думаю о рефакторинге, нет смыла продолжать поиски “лучшего способа решения задачи” — поздно, поезд ушел.
- Если имеющаяся переменная или функция являются не совсем очевидными, исходя из их имени, назначения или того, что они из себя представляют, то такое имя следует изменить. Возможно, потребуется внести изменения в объявление функции, переименовать переменную или поле.
- Если объявление переменной произошло в 5-ой строке, но она не используется до 20-ой, то следует переместить инструкции и объявить ее в 19-ой строке. При ссылке на переменную необходимо сразу же ее использовать. В противном случае читающим код будет непонятно, почему она указана гораздо раньше. Неминуемо возникнут вопросы. Не пропустили ли они что-нибудь в процессе чтения? А может ссылка на переменную есть, но они не поняли где именно?
Какие виды рефакторинга следует проводить при подготовке к написанию новой функциональности?
Мне очень нравится этот вид рефакторинга, поскольку он разжигает мой аппетит к созданию кода новой функциональности с нуля, но я разделяю точку зрения, что этот вид — всего лишь аперитив (так что не увлекайтесь!).
В моем случае подготовительный рефакторинг включает следующие виды:
- Больше случаев извлечения функции.Возможно, где-то еще сохранился дублированный код, и интуиция мне подскажет, что я его повторно использую при создании новой функциональности. И когда это время придет, то так приятно будет осознать, что рефакторинг этого кода уже выполнен.
- На этом этапе М. Фаулер рекомендует (и я с ним согласен) использовать параметризацию функции. Если я замечаю множественные определения функций, выполняющих практически одно и то же и отличающихся некоторыми деталями, то можно соединить их в одну функцию и просто добавить параметр для устранения этой разницы. Например, вместо четырех функций
add
,subtract
,multiply
иdivide
, каждая из которых принимает целые числа и выполняет действие, возможен вариант использования одной функцииcalculate
, принимающей два целых числа и параметр действия.
И хотя существуют разные виды рефакторинга, я снова и снова возвращаюсь к одним и тем же из них, так как для моего стиля программирования характерно создание дублированного кода.
Применительно к моему случаю, если бы я просто сосредоточился на устранении дубликатов при написании нового кода, при его обзоре в процессе подготовки к коммиту и при создании новой функциональности, то качество и читаемость моего кода существенно бы возросли.
Когда следует прекратить выполнение рефакторинга?
Теперь пришло время поговорить о границах. Вам уже известно о моей любви к рефакторингу, и у меня всегда найдется для него применение. Но нельзя же заниматься им постоянно: новые функциональности ждут своего воплощения, а код требует отправки.
Вот почему в процессе рефакторинга я постоянно задаюсь следующими вопросами:
Это подходящий момент для прекращения рефакторинга?
Речь идет не об объемах выполненного рефакторинга. Я лишь уточняю, приведен ли код в состояние, требуемое для прохождения всех тестов, и готов ли он для загрузки в главную ветку. Если ответ утвердительный, то на этом этапе и пора остановиться.
Если же я начинаю выполнение очередного цикла рефакторинга, то вхожу в следующий отрезок времени, в котором остановиться уже будет нельзя. Он может длится от 5 минут до 2 часов. И пока я нахожусь в середине этого процесса, мой код пребывает в нерабочем состоянии. Чтобы достичь подходящего момента, необходимо завершить рефакторинг или сделать откат.
Не стоит говорить себе: “Это не подходящий момент. Есть возможность провести еще рефакторинг”. Но такая возможность будет у вас всегда.
Если все ваши тесты проходят, и код готов для отправки в главную ветвь на рассмотрение, то это и есть самый подходящий момент для остановки.
Могу ли я позволить себе продолжить рефакторинг?
Предположим я достиг того самого подходящего момента, в связи с чем возникает следующий вопрос: “Следует ли проводить еще рефакторинг?” Но не стоит задавать его таким образом, так как мое сердце (переполненное любовью к рефакторингу) всегда ответит: “Конечно! Хочу еще! Это так прекрасно!”
Лучше спросить так: “Могу ли я позволить себе продолжить рефакторинг?”
В идеале мы прекращаем рефакторинг, когда можем, а не когда должны.
В чем разница между формулировками “прекратить, когда можешь” и “прекратить, когда должен”?
Формулировка “прекратить, когда можешь” означает способность констатировать: “Что ж, подходящий момент достигнут, и код выглядит достаточно чистым. Кроме того, мне пора приступить к решению следующей задачи”. Приятное ощущение, не правда ли? Это подобно тому, как завершая по-настоящему вкусную трапезу и сохраняя чувство самоконтроля, сказать: “Это было превосходно. Я сыт. Нет необходимости доводить себя до состояния пресыщения”. И после этого положить вилку.
“Прекратить, когда должен” может подразумевать следующее: я получаю гневный e-mail от коллеги, из которого следует, что демонстрация перед клиентом состоится через 10 минут, так почему же последняя ветвь функционала до сих пор не интегрирована. Тем временем мне нужно устранить повторы этой функции и с учетом этих изменений все почистить, при этом я понимаю, что хорошо бы выделить этот блок кода в отдельную функцию… но под давлением обстоятельств я выполняю тесты и загружаю код в главную ветвь для утверждения.
Ситуация “прекратить, когда должен” вынужденная, суматошная и ужасно неприятная. Когда же весь процесс под вашим контролем, чтобы “прекратить, когда можешь”, вы вполне удовлетворены тем, как завершается очередной рабочий этап.
Подведем итоги наших рассуждений в связи с вопросом: “Могу ли я позволить себе продолжить рефакторинг?”
- Если бы я собирался загружать код на утверждение в следующий подходящий момент, и при этом был бы уже полностью готов для его обзора или демонстрации функциональности, то у меня была бы возможность провести дополнительный рефакторинг. В противном случае, этот код пока что “достаточно хорош”, и эта подготовка является приоритетной.
- Если у меня нет другой задачи или функциональности для разработки, но возникает отсрочка, поскольку я не знаю с чего начать, то я могу позволить себе дополнительный рефакторинг.
Я на самом деле делаю свой код… менее читаемым?
Результатом правильного рефакторинга становится более читаемый и обслуживаемый код. Но иногда, когда я думаю, что выполняю рефакторинг кода, я фактически занимаюсь его оптимизацией.
Возможно, это происходит потому, что я слишком затягиваю данный процесс или неосознанно откладываю работу над следующей задачей. Но я начинаю отказываться от всего, что знал о правильном рефакторинге, и пытаюсь сделать код компактным и элегантным за счет его читаемости. Я жертвую пониманием ради элегантности.
Код по-прежнему выглядит читаемым, но только для меня. Это проклятие знания. В моих глазах он выглядит таковым, поскольку я знаю (не могу не знать), каким было намерение кода, прежде чем я все испортил. Он все еще работает, но уже не столь понятен для стороннего свежего взгляда.
Чем раньше я смогу заметить за собой подобную тенденцию, тем лучше. Приходит время, когда преобразование уже не идет на пользу. Как только я осознаю, что делаю свой код менее читаемым, мне необходимо вернуться назад и временно приостановить работу.
Рефакторинг, мы сможем все уладить. Нужно только установить границы
Нам нужны границы. Сначала определим, какие виды рефакторинга необходимо выполнить. На разных стадиях реализации функциональности вы можете применять наиболее подходящие из них. А какие именно будет зависеть от преобладающих тенденций в вашем стиле программирования.
После этого зададим себе несколько вопросов, чтобы понять, когда же следует прекратить рефакторинг. Почувствуйте подходящий для этого момент. Спросите себя, возможно ли провести дополнительный рефакторинг. Убедитесь, что ваши преобразования не усложняют читаемость кода.
Не задавайте следующие вопросы:
- Код совершенен?
- Он завершён?
Он никогда не будет совершенным, так же как и завершённым. Вместо этого стоит спросить: “Он стал лучше?”
Установите для себя границы в отношении рефакторинга. Во всем, что вы любите или ненавидите в жизни, нужны границы. А если вы питаете страсть к новаторству и рефакторингу, то вам без них просто не обойтись. Иначе вы навечно в нем увязнете, так никогда и не отправив свой код.
Читайте также:
- Комментарий в коде написать - всё равно, что проиграть
- Учимся программированию как Эйнштейн
- Какой язык программирования используют самые счастливые разработчики?
Перевод статьи Alvin Lee: Dear Refactoring: I Think We Need Some Time Apart