Небольшая метафора

Написание кода похоже на соединение двух точек. Ожидаемо, что самым простым путем будет нарисовать прямую линию между точками А и Б. 

Writing code is like connecting two points

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

Давайте увеличим количество препятствий на порядок. Линия становится все более извилистой.

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

А если мы заставим двигаться не только препятствия, но и сами точки? Вдобавок убедимся, что эти точки не приклеены к линиям, и вам придется следить за ними, чтобы они оставались соединенными. Начинает немного бесить?

Но и это еще не всё. Представьте, что мы знаем лишь приблизительное положение точек, и единственный способ узнать его — запрашивать их местоположение каждые 5 минут? Походит на безумие, или еще нет?

What if we only knew the approximate location of points?

Усложним еще немного. Допустим, мы знаем только приблизительное положение точек.

Вот это уже в значительной степени похоже на то, как происходит разработка в реальном мире.

Обсуждение

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

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

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

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

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

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

Как решить эту проблему?

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

Но шаблонов проектирования недостаточно. Гораздо важнее знать, что мы быстро движемся в правильном направлении, нежели правильно двигаемся в неправильном. И да, в мире полно устаревшего кода, который следует лучшим шаблонам проектирования. Фактически, чрезмерное использование шаблонов проектирования сверх необходимого называется "оверинжинерингом".

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

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

  1. Как можно быстрее узнавать, что мы быстро движемся в правильном направлении (обратная связь о правильности в виде черного ящика тестов)

  2. Использовать заранее продуманные решения для быстрого и контролируемого построения пути (шаблоны проектирования).

Code with correctness feedback and design patterns in place

Что в итоге

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

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

Почему разработка пользовательского интерфейса особенно склонна к Legacy?

Используя метафору с линией из примера выше, мы пришли к определению того, что такое legacy-код:

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

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

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

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

От идеи до legacy

Greenfield — проект одного человека, создаваемый с нуля

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

Greenfield project

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

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

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

С некоторыми неудачами (мы же с вами в реальном мире живем) вы справляетесь. Всё сделано вовремя, все довольны.

Конец.

Проект Brownfield

Более вероятен вариант, при котором вы окажетесь в другой ситуации и будете работать над чьим-то проектом «с нуля».

Brownfield project

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

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

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

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

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

Шаг за шагом, и кодовая база станет тем, что называется legacy.

Legacy-проект

И даже пример выше не такой печальный, как самая частая ситуация —  работа над legacy-проектом в качестве разработчика.

Legacy project

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

Заключение

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

Далее в цикле:

  • Так как же сделать пользовательский интерфейс легкотестируемым и легкоизменяемым?

  • Decoupler MVU или как реализовать MVU архитектуру для UI приложений

Полезные ссылки

  1. Black Box Testing: An In-Depth Tutorial — Этот туториал на Guru99 предоставляет всестороннее руководство по тестированию черного ящика. В нем рассматриваются его методы, типы, приемы, преимущества и недостатки.

  2. Design patterns — Подробное руководство по шаблонам проектирования программного обеспечения. Каждый шаблон подробно объясняется с примерами, чтобы лучше понять, когда и где их использовать.

  3. SOLID Principles: Explanation and examples — Этот пост на FreeCodeCamp разбирает принципы SOLID в понятной форме с большим количеством примеров.

  4. Understanding Legacy Code: Этот сайт — отличный источник информации для понимания legacy-кода и стратегий эффективной работы с ним. Поможет вам ориентироваться и улучшать legacy-системы.

  5. Greenfield vs Brownfield: В этой статье на LinkedIn Giorgi Bastos подробно рассматривает концепции проектов greenfield и brownfield. Автор дает отличное представление о различиях, преимуществах и вызовах обоих подходов.

  6. Greenfield vs Brownfield in Software Development: Synoptek предоставляет подробное сравнение между разработкой программного обеспечения greenfield и brownfield, дополнительно улучшая понимание этих концепций и их влияния на разработку пользовательского интерфейса.

  7. Advantages and Disadvantages of White Box Testing: На JavaTpoint опубликовано сбалансированное представление о тестировании белого ящика и подробное обсуждение его преимуществ и ограничений.

  8. White Box Testing: Pros and Cons: Segue Technologies предлагает другую точку зрения на тестирование белого ящика, подробно описывая его плюсы и минусы. Эта статья поможет вам понять, почему тестирование белого ящика может быть частью проблемы при поддержке кода пользовательского интерфейса.

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


  1. DarthPadla
    31.08.2023 11:12
    +4

    Легаси код - это код, написанный человеком, который уже не работает в вашей компании


    1. SpiderEkb
      31.08.2023 11:12

      Сплошь и рядом такое. И что? Если код написан нормально (и внятно документирован), то он легко модифицируется. В противном случае его не сможет (без лютого костылинга) модифицировать даже тот, кто его писал.


      1. Twiling
        31.08.2023 11:12
        +1

        Хороший код не требует документации, он и так понятен и структурирован.


        1. SpiderEkb
          31.08.2023 11:12
          +2

          Не всегда, увы.

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

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

          У нас написанный когда-то кем-то модуль может жить и работать годами пока вдруг не потребуется внести какие-то изменения. И вносить их будет совсем не тот, кто этот модуль писал. Да, у него будет FSD на модуль с указанием что именно нужно поменять по логике, но наличие в коде "привязок" к FSD очень сильно упрощает задачу нахождения конкретного куска где нужно что-то поменять и при этом не ухудшить эффективность всего модуля в целом.


  1. dsh2dsh
    31.08.2023 11:12
    +1

    Так ведь не бывает не legacy кода. Любой код становится legacy через несколько лет от его написания. И это нормально. Это не код legacy, это новый разработчик не осилил. Вот для него ярлык legacy - это типа оправдание.


    1. iOneM
      31.08.2023 11:12
      +6

      Несколько лет это еще хорошо, часто код становится легаси уже на этапе влития в репозиторий


    1. mrobespierre
      31.08.2023 11:12
      +1

      Ещё как бывает, хоть и понять это непросто. Legacy не просто называется этим словом. Оно буквально означает наследие, наследство. То, что осталось с каких-то времён, досталось от кого-то, кто был до. Т.е. оно очень тесно связано со врменем, его течением. Система, которую дописали сегодня утром, не может быть легаси. Достаточное время ещё не прошло. А теперь сложная часть: время это не ход стрелки часов, время - поток событий, пусть даже отдельных движений стрелки часов. Т.е. оно относительно, ведь поток событий везде разный, для всех разный. Для разных систем, написанных с использованием разных инструментов превращение в legacy происходит с разной скоростью. Взять фронт например. У js-ников каждый год новый, более прогрессивный фреймворк - проще, быстрее, удобнее, логичнее, лаконичнее. А у банковских систем на COBOL каждый год одно и тоже т.к. ни COBOL, ни процессы в бэкофисе банков могут не меняться десятилетиями. Соответственно "свеженькая" банковская система, которую для какого-нибудь швейцарского банка написали 7 лет назад вполне может быть на 100% современной и актуальной т.к. даже непонятно как её обновить.


      1. SpiderEkb
        31.08.2023 11:12
        +2

        А у банковских систем на COBOL каждый год одно и тоже т.к. ни COBOL, ни процессы в бэкофисе банков могут не меняться десятилетиями. Соответственно "свеженькая" банковская система, которую для какого-нибудь швейцарского банка написали 7 лет назад вполне может быть на 100% современной и актуальной т.к. даже непонятно как её обновить.

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

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

        Третье. Банк развивается, развиваются технологии, появляются новые сервисы. Если разработчики мобильного приложения или интернет-банка придумали "новую креативную фичу", она должна поддерживаться на уровне АБС на центральных серверах. И при этом ее реализация не должна ухудшать все то, что уже работает.

        Далее. Количество клиентов постоянно растет. И то, что три-пять лет назад всех устраивало по скорости, сейчас начинает работать неприлично долго, сильно грузить сервер и местами тормозить какие-то бизнес-процессы. И тут требуется оптимизация кода, реорганизация данных и т.п. Пример из личной практики - в модуле комплаенса есть операция построения стоп-листов по спискам РосФина (всякие злодеи-бармалеи). Когда-то давно она работала пару часов и все было хорошо. Потом и списки стали расти и количество клиентов выросло. Операция стала занимать 9 часов. Что уже никому не нравилось. Пришлось заняться оптимизацией. Уменьшили время до 4-х часов (приемлемо). Дальше все опять покатилось вниз - добавилось списков, выросло количество клиентов. 15 часов. Вообще никуда не годится. А тут еще РосФин подкинул - изменили формат списков. Под это дело переписали вообще все. Алгоритмы разбора списков, форматы БД их хранения, добавили витрины изменений, полностью переделали всю процедуру построения стоп-листов. Сейчас оно работает 10 (!!!!) минут. Но мама дорогая, сколько туда труда и времени вложено (тут к вопросу о TTM - можно сделать быстро и просто, как это изначально делалось, но потом будут проблемы с масштабированием, а можно делать долго и сложно, но и результат будет совсем иным).

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

        В целом, легаси кода тут очень много. Частично он "вымывается", частично живет очень долго.


  1. shadwar
    31.08.2023 11:12

    Если человек не считает легаси кодом код своего прошлого проекта - то за всё это время с того проекта он ничему не научился и ничего нового не узнал, остался стоять на месте.


    1. Kanut
      31.08.2023 11:12
      +2

      Вот только есть люди, которые не на месте стоят, а по кругу ходят :)


  1. tachyon-tomsk
    31.08.2023 11:12
    +1

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


  1. LyuMih
    31.08.2023 11:12

    У меня по legacy такие мысли с точки зрения фронтендера.
    Любой код превращается в legacy c течением времени из-за развития проекта и изменение технологического стека / подходов к разработке.
    И нужно стремиться к уменьшению количества самого кода.

    Пример развития проекта с работы.
    1. NX Монорпеозиторий
    У нас было 3 хостовых приложения и 50 модулей (отдельные страницы) к ним в отдельных репозиториях.
    Мы приняли решение перейти на NX монорепозиторий и объединить модули и хосты.
    После объединения сразу появился лишний legacy код (в виде регистрации модулей и т.п.), который мы методично убрали. Ушли лишние webpack.config.js в модулях и т.п. Меньше файлов - меньше legacy.

    2. Наш любимый React Redux
    Мы приняли решение отказаться от redux в пользу локальных State по возможности и заменой его на легковесный zustand и React Context.
    Каждый модуль уменьшился процентов на 30-50 legacy кода, который был из-за Redux boilerplate.
    Модули сами по себе остались legacy без изменения бизнес логики.

    Не получится полностью избавится от legacy, но можно существенно сократить его объём.


    1. SpiderEkb
      31.08.2023 11:12
      +1

      У нас было 3 хостовых приложения и 50 модулей (отдельные страницы) к ним в отдельных репозиториях.

      Хорошо когда объемы небольшие и есть время все это регулярно переписывать на "модный в этом сезоне стек".

      Когда у вас в команде будет сотни репозиториев с десятками и более объектов в каждом, а ресурсы разработчика будут планироваться на 2 и более квартала вперед (это только по плановым проектным задачам, не считая срочных - дефекты промсреды, "нормативка"...), а каждый разработчик будет параллельно тянуть по 2-3 задачи, вам станет не до этих развлечений.


      1. SpiderEkb
        31.08.2023 11:12

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