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

Основная часть руководств по работе с унаследованным кодом относится к небольшим участкам программы и к решениям, которые можно реализовать за часы или дни, а не недели и месяцы. При этом давно ожидается появление книг по рефакторингу в масштабе всей архитектуры программной системы, описывающих оптимальные методы и рассматривающих успешный и провальный опыт. Отчасти этот пробел начинает заполняться — появились книги «Рефакторинг» [1] и «Эффективная работа с унаследованным кодом» [2], а эволюция архитектуры и процесс принятия решения подробно описаны в работе [3], излагающей опыт проекта Ford, в рамках которого проводились исследования интерфейсов управления автомобилями и процесс выбора между их сохранением и исключением.

Сдерживайтесь

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

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

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

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

Затем принимается решение о том, по карману ли вам будет доработка. Проект может иметь ограничения по срокам сдачи или бюджету, и тогда единственным выбором будет игнорирование. Но даже если бюджет достаточен, возможно, стоит провести доработку позднее. В ходе такой доработки архитектура может поначалу ухудшиться. Масштабному рефакторингу присущ длительный переходный период, в течение которого одновременно будут активно проверяться несколько вариантов архитектуры. Можете ли вы это себе позволить? Хаос переходной стадии может сбить разработчиков с пути, особенно при отсутствии четкого изложения общего плана с описанием промежуточных этапов.

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

И наконец: каков же итог? Рефакторинг обеспечит интеграцию кода, а если его переписать целиком, то уже нельзя будет гарантировать простоту интеграции. Чем масштабнее проект и чем больше времени займет переписывание, тем сложнее будет выполнить интеграцию. До какой-то степени уровень сложности можно предсказать, изучив требования и покрытие существующего кода тестированием. Если требования неизвестны, а покрытие недостаточное, то придется выделить время на уточнение — в противном случае интеграцию придется осуществлять методом проб и ошибок, асимптотически приближаясь к функционалу исходной системы.

Реализация заново

Рефакторизовать имеющийся код в надежде на успешную интеграцию менее рискованно, чем все переписывать с нуля. В каких случаях все же стоит рассматривать последний вариант? Создание заново — большой и рискованный проект, но он иногда обходится дешевле, поскольку внесение множества мелких изменений может отнять больше времени. Условия, при которых стоит рассмотреть возможность создания заново, следующие:

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

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

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

Разработчики ПО — не единственные, кто испытывает сложности с анализом возможных последствий масштабных изменений. То же касается, например, архитекторов ПО.

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

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

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

Культурные перемены

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

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

Руководя группой, вы разбиваете сад, где будут расти хорошие идеи. Игнорирование проблемного кода ставит ваш сад под угрозу — вы вносите в культуру отравляющую практику. Код ведь уже безобразный, почему бы не «разбить еще одно окно» — добавить еще один вложенный условный переход? Абстракции уже не соблюдаются, какой вред будет от лишнего «хака»? Когда тестовое покрытие скудное, зачем тратить время на исправления?

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

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

Теоретическое обоснование

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

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

Когда человек решает задачи не программными средствами, его идеи остаются в голове. Но с применением ПО другая ситуация: сам код служит выражением задач, которые пытается решить программист. С механической точки зрения программа прекрасно бы работала с переменными, которые назвали X и Y, но разработчик выражает идеи в коде, называя переменные totalSales («общий объем продаж») и last-KnownAddress («последний известный адрес»). Это помогает передавать информацию другим и освобождает от необходимости разбираться, что конкретно выражает переменная X.

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

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

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

***

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

Решения об архитектуре ПО базируются на трех столпах: компромиссы, компромиссы и компромиссы. Стремление устранять проблемы в коде может перевесить соображения рентабельности бизнеса и необходимость быстрее создавать новые функции. Нельзя просто стремиться к максимальному качеству кода — нужно заботиться о том, чтобы принять оптимальное решение.

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

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

Литература

1. M. Fowler. Refactoring. Reading. — MA: Addison-Wesley, 2018.

2. M. Feathers. Working Effectively With Legacy Code. — Englewood Cliffs, NJ: Prentice Hall, 2004.

3. A. Tsakiris, Managing software interfaces of on-board automotive controllers // IEEE Software.— 2011 (Jan. — Feb.). — Vol. 28, N. 1. — P. 73–76. doi: 10.1109/MS.2011.11.

Джордж Фэрбенкс (gf@georgefairbanks.com) — разработчик ПО, компания Google.

George Fairbanks, Ignore, Refactor or Rewrite? IEEE Software. March/April 2019, IEEE Computer Society. All rights reserved. Reprinted with permission.