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

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

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

Работоспособность и экономика


Обычно старый код на 100% работоспособен. Ну или, если быть честным, на 95%. Боль начинается, когда необходимо внести в систему изменения, особенно, когда изменения приводят к появлению плавающих багов, на борьбу с которыми, обычно, уходит большая часть времени.

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

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

Любые изменения затрагивают большое количество пользователей и могут негативно повлиять на стабильность продукта. Если продукт – флагманский и приносит компании кучу денег, то нарушение стабильности может обойтись компании в кругленькую сумму. В данном случае как нельзя более точно описывает ситуацию анекдот про сына сисадмина: «тогда, ради Бога, ничего не трогай, ничего не меняй!».

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

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

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

Гуру-носитель знаний


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

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

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

Не стоит и говорить о том, что попытки кого-то ещё работать с этим кодом приводят к огромным трудозатратам. Автор-гуру держит знания о коде «в оперативной памяти» и помнит (если повезёт) историю забивания того или иного костыля, тогда как новый программист вынужден продираться через хитросплетения костылей, обросших вековым мхом и разбираться в устройстве легаси с нуля.

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

Запутанная логика


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

Неактуальная документация и юнит-тесты


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

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

В результате рано или поздно, документация замирает на уровне «так было в прошлом году, а сейчас всё поменялось». Увы, парадокс документации в том, что там, где она действительно нужна – в самых сложных, запутанных и даже алогичных на первый взгляд местах, её написание и поддержка занимает неоправданное количество времени и сил и обычно откладывается.

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

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

Что дальше?


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

Через несколько дней здесь появится ссылка на вторую часть. Если она ещё не появилась – напомните мне, пожалуйста :).

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


  1. DanteXXI
    14.09.2015 13:40
    +3

    Сейчас работаю над махровым легаси, при том в достаточно специфичной сфере. В проекте не было тестов изначально, код пребывает не в лучшем виде, тестами удастся покрыть не более 10% кода (да и то, это новая функциональность). Проект надо поддерживать и развивать, ибо им активно пользуются и периодически хотят новые фичи, но к сожалению он становится настолько запутанным, что каждая новая задача или багфикс приводит к тому, что нужно судорожно протыкивать весь проект и молиться, что ничего не сломалось (привет, отсутствие тестов!)
    В связи с этим у меня два вопроса: «как жить если тестов не бывает?» и «как продать бизнесу рефакторинг?»


    1. Tomcat
      14.09.2015 14:16
      +2

      В точку прямо.
      Без тестов грустно. Иногда помогает — вместо честных юнит-тестов автоматизировать хотя бы функциональные. По принципу — все контролы на месте, ничего не пропало. Инструмент не посоветую, но можно поспрашивать тестировщиков они должны знать. Ну и покрыть юнит-тестами для начала хотя бы «стыки» между компонентами. По опыту, самая большая засада — именно при интеграции.

      По второму вопросу точно буду отдельно что-то писать. В двух словах — бизнес говорит на языке денег, поэтому разговаривать с ним надо на языке денег. Поддержка кода и внедрение стоят времени. Цель рефакторинга — сэкономить время на внедрение новых фич и избавиться от части багов (обычно попутно получается). Соответственно, я обычно некоторое время собираю статистику по таким задачам — сколько времени уходит на их решение, а потом прихожу и говорю, что рефакторинг займёт X часов и сэкономит Y часов на каждой фиче — вот список.

      Надо понимать, что затраты на рефакторинг — это инвестиции, и бизнес должен чётко видеть ROI для него.


  1. ShadowsMind
    14.09.2015 15:29
    +1

    Как то работал над кодом которому лет 7-10, написанному еще на Java 1.6. И впечатления, мягко говоря, печальные. Из того что самое печальное из воспоминаний — местами не юзались темплейт енджайны и данные юзеру отдавались слепленные стрингбилдером…
    Проблема в таких проектах даже не в том, что языковые конструкции устарели, или что фрэймворки/либы старых версий. А проблема в том, что раз они дошли до нас в таком состоянии, то разработка изначально была построена не правильно. Проект не обновлялся из версии к версии языка/либы/фрэймворка, не рефакторился и т.д. Отношение к коду в этих проектах было такое — что сделай быстро запладку, не важно как. И проект весь на запладках и разобрать что, где и как — это еще та проблема.
    В общем мораль такова — жадный заказчик платит дважды, а то и трижды, а то и весь жизненный цикл своего кривого проекта…


    1. iamironz
      14.09.2015 17:36
      +3

      еще на Java 1.6

      Так говорите, как будто 1.4


      1. ShadowsMind
        14.09.2015 19:26

        К счастью(а может быть и к сожалению, если расценивать как опыт) начал с 1.7, об сложностях программирования на Java до 1.5 знаю только по наслышке…
        P.S. пользуясь моментом хочу поблагодарить Мартина Одерски за то, что дал Java генериксы :-D


        1. dozent
          23.09.2015 18:03

          Запладки, генериксы. Откуда вы это берете?


          1. ShadowsMind
            24.09.2015 10:41

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


  1. seregus
    14.09.2015 20:34

    Редко комментирую, ну уж очень статья на злобу дня.

    А не надо продавать никакой рефакторинг никому.
    Более того рефакторинг ради рефакторинга — зло.

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

    А вся история с рефакторингом ни во что не выльется, после рефакторинга туда внесут такие-же изменения, о которых тот кто рефакторил, даже не задумывался и как результат не подготовил код…


    1. qw1
      15.09.2015 00:48
      +2

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

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


    1. Tomcat
      15.09.2015 10:26
      +1

      Рефакторинг ради эстетики, действительно несёт мало пользы, Однако, он необходим, если:
      — изменения в проекте становятся дорогими и долгими из-за низкого качества кода и архитектуры
      — в ближайшее время планируется развитие продукта с добавлением новых фичей (у нормального живого продукта это состояние перманентно)
      — как альтернатива предыдущему — команда проекта будет расширяться

      А вся история с рефакторингом ни во что не выльется, после рефакторинга туда внесут такие-же изменения, о которых тот кто рефакторил, даже не задумывался и как результат не подготовил код…

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

      Если с командой всё в порядке, то такие изменения как минимум обсуждаются заранее. В том числе и с тем, кто делал рефакторинг. Уверяю вас, чтобы сделать кривой костыль в условиях, когда код обсуждается и регулярно проводится ревью, надо очень постараться. Особенно после того, как код приведён в относительный порядок.

      А вот если даже небольшие правки ведут к костылям, то надо прежде всего настраивать коммуникацию и распределение ролей в команде, а делать рефакторинг в такой ситуации — действительно, пустая трата времени и денег заказчика. Это симптомы того, что у кода нет владельца (aka code owner) или он не справляется.


  1. OnYourLips
    15.09.2015 09:25
    -3

    > Обычно старый код на 100% работоспособен. Ну или, если быть честным, на 95%.

    Это не так. Иногда код «старый» еще до релиза.

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


    1. Tomcat
      15.09.2015 10:10
      +2

      Это просто ваш уровень вырос ;)


  1. vlreshet
    15.09.2015 10:07

    Такое ощущение что статья написана вот прям-прям про наш проект. То же самое — огроменное количество крайне низкосортного кода, никаких тестов, никакой документации, ничего. Но в это же время проект ворочает миллионы долларов, и трогать его нужно крайне и крайне аккуратно. Его нужно, просто необходимо брать и переписывать, но клиент понимающе кивает и говорит «да, я понимаю, но у нас сейчас слишком много других задач». Позже, конечно, всё стало ползать, и всё-равно пришлось нанять чертовски дорогих ребят (250$ в час, шутка ли) для оптимизации БД, и переписать целый кусок системы заново, но всё-равно дела остались плохи. Корпоративный монстр, что с него взять.


    1. Tomcat
      15.09.2015 10:31
      +1

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


      1. vlreshet
        15.09.2015 12:16
        +1

        Да мы сейчас так и работаем по сути — всё новое пишется более-менее аккуратно (насколько это можно вкатать в существующую архитектуру), над старым пишутся какие-нибудь аккуратные обёртки, а совсем-совсем плохой но важный код — переписывается. Так и живём.


  1. freakru
    16.09.2015 23:20
    +1

    В нашем легаси проекте (Java 7, hybris, SAP и другие страшные слова) мы поставили на микросервисы. А что не получается выделить как сервис, делаем плагинами на scala. Но рефакторинг как таковой это конечно не заменит.


  1. IvanIDSolutions
    18.09.2015 13:41

    а еще есть старая инженерская поговорка, которая гласит, «если что-то работает — не трогай его»)))