Бытует мнение, что программные системы, будучи объектом не совсем материальным, не поддаются старению. И если говорить о старении физическом, то действительно, шансы на то, что буковка “o” в имени класса вдруг от старости ссохнется и превратится в букву “c” – действительно малы. Но вместо старения физического, программные системы стареют морально. Со временем накапливается груз ошибок за счет неточностей в исходных требованиях, непонимания требований самим заказчиком, архитектурных ошибок или неудачных компромиссных решений; да и ошибки поменьше, типа слабопонятного кода, его высокой связности, отсутствия юнит-тестов и комментариев делают свое черное дело. Все это приводит к накоплению технического долга (о котором шла речь в прошлый раз), из-за которого при добавлении новой возможности в систему приходиться платить «проценты» в виде более высокой стоимости реализации и более низкого качества получаемого результата.
Существует несколько способов выплаты технического долга, и наиболее простым из них является рефакторинг системы или некоторых ее частей, выделение новых абстракций, написание юнит-тестов, комментариев или полное переписывание некоторых кусков системы. Однако, как и в любом другом деле, которым занимается славная программерская душа, душа эта очень часто перегибает палку и вместо прагматичного подхода к улучшению некоторых частей системы для получения разумной выгоды в будущем, она бросается с шашкой наголо переписывать все подряд: что нужно, что не обязательно и что вообще переписывать не стоит.
Все это приводит к еще одной метафоре, которая как раз описывает подобное неустранимое желание к переписыванию старого кода – к синдрому рефакторинга.
Симптом 1. Чужой код – г#$но
Как только в команде появляется более одного разработчика, сразу же начинаются проблемы: у разных людей могут быть разные мнения на счет форматирования кода, используемых идиом, языка программирования и подхода к решению прикладных задач. Кроме того, насколько бы ни были понятными требования, как бы ни был выразителен язык программирования, как бы ни был хорош код и как бы он не был отлично задокументирован (хотя все эти сочетания в одном месте никогда не встречаются), многие мысли и намерения разработчика все еще остаются только в его голове. А, не понимая его намерений, мы очень часто высокомерно считаем, что текущее решения является неверным и то же самое можно сделать по-другому и значительно лучше.
И понимая, что весь мир вокруг сошел с ума, многие представители нашей «рассы» бросаются на барикады и начинают переписывать или рефакторить чужой код, просто потому что он чужой. Проявляется классический синдром «это придумано не здесь» (Not Invented Here) и весь любой код, написанный не своими руками, автоматом является непонятным (ибо много букав) и, соответственно, хреновым.
Такое неуважительное отношение к чужому коду, во-первых не красит разработчика, а во-вторых, в большинстве случаев не уменьшает тот технический долг, который должен уменьшаться благодаря этому рефакторингу. Система в целом не становится более сопровождаемой и понятной, просто она теперь ближе и понятнее одному конкретному человеку. Конечно, на некоторое время сопровождаемость системы улучшается, пока новыми фичами занимается автор этих изменений, но все выгоды закончится ровно тогда, когда за этот кусок кода берется другой разработчик. И процесс начинается сначала.
Симптом 2. Стремление к идеальному коду
Стремление к идеальному коду является наиболее благим намерением при изменении существующего кода, хотя все мы знаем, к чему подобные намерения ведут. Слепое следование непродуманным стандартам кодирования не многим лучше, чем отсутствие стандартов кодирования вовсе. Красота кода – не самоцель; код может быть красиво оформленным, все функции могут быть небольшого размера с достаточным количеством комментариев и даже покрыты юнит-тестами. Но это не значит, что этот код будет справляться со своей основной задачей, поскольку, банально, никто не удосужился узнать эту задачу у пользователя.
Для получения идеального результата требуется бесконечное количество усилий. Код должен быть достаточно красив, достаточно понятен, с достаточным количеством комментариев и юнит-тестов. От юнит-теста, который покрывает совершенно тривиальные случаи, или который настолько непонятен, что его сопровождение практически невозможно, скорее больше вреда, чем пользы. Юнит-тесты являются бесценным источником информации о спецификации системы, каждый из них должен рассказывать историю об одном из способов использования этого класса или части системы. Их не должно быть много, и их не должно быть мало; их должно быть ровно столько, чтобы затраты на их написание и сопровождение были оправданы (*).
Симптомы 3. «Я дерусь, потому что я дерусь»
С течением времени, с высоты своего собственного нового профессионального опыта, благодаря более четкому пониманию требований пользователя и, пониманию своей собственной системы, даже свой код начинает выглядеть ужасным. Сколько раз вы ловили себя на мысли: «Вот, блин, кто же эту ерунду написал?» и потом с удивлением узнавали, что этот кто-то – это вы.
Это нормальная ситуация, ведь небольшие должки накапливаются у всех. Более того, обратная ситуация, когда через время вы не можете найти проблемы в собственном коде, кажется более подозрительной. Поэтому улучшение своего собственного кода – процесс точно такой же полезный, как и улучшение кода чужого. Но бывают случаи, когда свой собственный код подвергается значительным переделкам пяток раз за год, но при этом новые фичи добавляются все так же медленно, и если посмотреть на этот код со стороны, то ничего в нем толком и не меняется.
По сути, это некоторая разновидность предыдущего синдрома, но когда движущей силой является не идеальный код, а одному ему понятное идеальное решение. Это нормально, если человек это делает для своего собственного проекта, когда при этом нарабатываются новые решения уже хорошо известных задач, но не совсем разумно, когда это делается для коммерческого продукта. Ведь получается, что код переписывается просто так; просто потому что сегодня я узнал о новой фиче в моем любимом языке программирования или о новой библиотеке, которая значительно лучше решает эту же задачу. Но через время выходят все новые фичи (или вы о них узнаете) и процесс опять повторяется с начала, но при этом никакие долги не выплачиваются и ничего ценного в систему не добавляется.
Симптом 4… 1001
Думаю, что каждый достаточно опытный разработчик сталкивался со всеми этими симптомами и, наверное, не с одним десятком других. Я не ставил себе целью описать все возможные причины, которые могут побудить к ненужному переписыванию кода. Суть этой небольшой заметки в том, что код, как и архитектура не являются самоцелью; все это лишь средства для решения определенных задач бизнес-пользователей или любых других задач, которые должны решаться приложением или библиотекой. Код и архитектура должны быть хорошими и гибкими ровно настолько, насколько это нужно для успешного решения этих задач в разумные сроки сейчас и для разумных трудозатрат на решение новых задач в будущем.
Практически в любом деле прагматизм и отсутствие крайностей является лучшим выбором и рефакторинг существующего кода не исключение. Не стоит забывать о законе Парето (принципе 80/20): двадцати процентов усилий на улучшение вашего кода и поддержания его в адекватном состоянии, зачастую достаточно, чтобы улучшить его на 80%. А если это не так, то может быть поциент скорее мертв, чем жив и инвестировать дополнительные средства на покрытие столь большого технического долга просто нет смысла и стоит начать все с чистого листа?
—
(*) Исключительно на всякий случай скажу, что я считаю юнит-тесты ценнейшим инструментом в арсенале разработчика. Это бесценный источник информации о том, как система должна работать и потрясающая лакмусовая бумажка качества дизайна: если код невозможно покрыть юнит-тестами, значит с ним что-то не так. Это не значит, что я покрываю весь свой код тестами, но я обязательно «примеряю» их к своим классам и меняю дизайн, если написать их будет невозможно или очень сложно. Но при этом я отношусь к тестам с достаточным прагматизмом и стараюсь, чтобы их количество было наиболее оптимальным с точки зрения трудозатрат к получаемой от них выгоде.