Вы собираетесь войти в другое измерение. Измерение не только зрения и звука, но и разума. Путешествие в удивительную страну воображения. Следующая остановка — Legacy Код.
Для тех, кто не узнал или не смотрел шоу, это (измененное) вступление к «Сумеречной зоне». Это культовый научно-фантастический сериал. Каждый эпизод рассказывает отдельную историю, в которой персонажи сталкиваются с тревожными или необычными событиями, которые приводят к неожиданному финалу.
Наше путешествие по строкам унаследованного кода иногда может показаться очень похожим. Но, как и в эпизодах «Сумеречной зоны», в каждой загадке legacy кода есть урок, который нужно усвоить.
Заимствованные знания
Программированию можно научиться разными способами. Но как бы мы это ни делали, мы делаем это не сами по себе. В нашем путешествии есть много книг, онлайн-курсов и документации. Просматривая эти материалы, мы натыкаемся на кучу советов о том, как что-то можно сделать и чего следует избегать. Все эти советы — накопленный опыт людей, которые писали эти материалы, и многих других, которые делились своим опытом до них.
Это то, что я люблю называть заимствованными знаниями. Это не наши знания, мы их «заимствуем». Мы делаем что-то определенным образом, но это не гарантирует, что мы понимаем, что делаем.
— Мы занимаемся микросервисной архитектурой.
— Почему? Какую проблему мы пытаемся решить? Каковы преимущества? Каковы недостатки? Мы действительно делаем микросервисную архитектуру или мы делаем распределенный монолит? Нужны ли нам микросервисы?
Это, наверное, самый простой пример. В последнее время микросервисы используются как мантра. Они представляются как окончательное решение всех наших проблем. Не поймите меня неправильно, микросервисная архитектура — это красивый образец, но у него есть свое место.
Пока мы не знаем, что это за место, микросервисы не являются нашим знанием. Это то, что кто-то другой сказал нам сделать. И рекомендовать их было бы то же самое, что рекомендовать фильм, который мы не смотрели. Мы доверяем чужому вкусу в кино. Мы его «заимствуем».
Это не обязательно плохо. Примите во внимание, что мы работаем, чтобы жить, а не живем, чтобы работать. Поэтому знание всего не является конечной целью, к которой мы должны стремиться. Вот почему у нас есть самоуверенные (opinionated) технологии и стандарты кодирования. Чтобы показать нам путь и избавиться от необходимости учиться на собственных ошибках. Нам не нужно попадать под машину, чтобы научиться смотреть по сторонам, переходя улицу.
Иметь структуру проекта, инструмент или шаблон, который мы можем просто «вставить» в наше решение без лишней суеты, — это здорово. Но что определенно должно быть нашей целью, так это понять, что мы делаем, почему и когда мы не должны делать это именно таким образом.
Устаревший Legacy код
Как унаследованный код вписывается в эту историю? Что может быть лучше, чем увидеть, почему мы должны или не должны что-то делать, чем увидеть это в действии? Иногда код — это просто жертва времени или обстоятельств, но в большинстве случаев устаревший код — результат неверных решений.
Вначале труднее заметить эти плохие решения. Когда мы начинаем новые проекты, мы в основном сосредоточены на том, что нужно сделать. По прошествии времени и использовании программного обеспечения начинают всплывать неверные решения. И нет лучшего учителя, чем плохое решение.
При разработке программного обеспечения мы должны избегать тесной связи, сильных зависимостей насколько это возможно. Но действительно ли мы понимаем, почему? И как мы можем этого добиться? Иногда можно тесно связать вещи, если мы знаем, что делаем.
Например, если мы используем определенный ORM, можно напрямую использовать его в бизнес-логике… если мы точно знаем, что не собираемся его менять. Если мы не уверены, тогда мы должны установить уровень абстракции между нашей бизнес-логикой и уровнем доступа к данным, используя репозитории, объекты доступа к данным или что-то подобное.
Если мы когда-либо были в ситуации, когда мы использовали ORM, который больше не поддерживается, специальное решение, которое больше не соответствует нашим потребностям, или просто большинство сотрудников не знают, как его использовать (и нет хорошей документации), тогда мы точно понимаем, зачем нам нужен этот уровень абстракции. В такой ситуации изменение ORM станет кошмаром, а использование существующего ORM ограничивает наши возможности. Это проигрышная ситуация.
Другим примером, столь характерным для legacy кода, может быть дублирование кода. Мы все (должны) согласиться с тем, что дублирование кода — это плохо. Тем не менее есть так много разработчиков, которые все еще дублируют код. Иногда даже в массовом порядке. Любой из нас, кто когда-либо исправлял какую-либо ошибку, а в продакшене все еще наблюдается ошибочное поведение из-за наличия двух, трех или более копий одного и того же кода, содержащего ошибку, знает, почему мы не должны дублировать код.
В новой кодовой базе нам это может сойти с рук, потому что кода все еще не так много, и мы все еще помним каждое место, которое нам нужно изменить, но это не делает дублирование менее неправильным.
Выученные уроки
Все это хорошо сказано, но слишком абстрактно, чтобы показать, о чем я на самом деле говорю. Итак, позвольте мне описать один из моих опытов с унаследованным кодом.
В начале своей карьеры я пришел работать над одним проектом. Это была вторая версия, которая в то время находилась в разработке уже 4 года. Это был MVC-проект, охватывающий множество функций, и в нем было значительное количество кода. В базе было более 100 таблиц, и более половины из этих сущностей должны были иметь вложения (attachment). И эта фича (вложения) — это то, что я хотел бы рассмотреть в этой статье.
Чтобы ускорить разработку, первоначальная команда использовала универсальные контроллеры и генерацию кода. Помимо прочего, они генерировали представления для операций CRUD для большинства сущностей. Эти представления создавались, а затем менялись в соответствии с требованиями. Большинству сущностей необходимо иметь вложения. Чтобы решить эту проблему, они поместили код для обработки отображения, загрузки и изменения вложений внутри шаблона представления.
Урок: дублированный код сложно поддерживать, и его следует избегать
Поскольку более 70 сущностей должны были иметь вложения, логика, связанная с этой функцией, была сгенерирована более чем в 70 местах. Там был список форматов, которые система поддерживала для загрузки. Когда потребовался новый формат, мне нужно было просмотреть все эти 70+ мест, чтобы обновить бизнес-логику.
Как только я это сделал, новый формат стал поддерживаться. Но через некоторое время пользователь пожаловался, что он не работает. Мне потребовалось некоторое время, чтобы понять, что я забыл обновить шаблон представления. Следующее представление, созданное после изменения, не содержало нового поддерживаемого формата. Это была глупая ошибка, но в свою защиту скажу, что в то время я был довольно зеленым разработчиком.
Урок: если вы генерируете код, не создавайте дублированный код. Или, если вы это делаете, убедитесь, что вы сможете позже перегенировать его, чтобы отразить новую логику
Честно говоря, это была не только моя вина. Шаблон представления не должен содержать часть представления для вложений. Этот код не был кандидатом на генерацию. Это должен был быть отдельный компонент, на который ссылаются только в представлении. Хотя это был сгенерированный код, дублированный код остается дублированным кодом.
Если бы представления можно было переопределить, сгенерировав новую версию, не было бы никаких проблем. Вновь сгенерированные версии будут содержать изменения, и все будет работать как положено. Но представления были изменены с помощью написанной вручную логики, так что это был не вариант.
Урок: дженерики — это здорово. Используйте их, когда можете, но не форсируйте такой подход
Я все равно решил создать компоненты для вложений. Замена всего этого дублированного кода может быть невозможна сразу, но я мог бы использовать его в новом коде. Таким образом, я не создам для себя больше работы, как только начну переключаться на такой подход позже.
До этого я работал над проектами, в которых использовались дженерики, но лично я никогда не писал ни строчки универсального кода. Хотя я знал, что такое дженерики, я не понимал их полного потенциала.
Первоначальная идея компонента заключалась в том, чтобы иметь унифицированную часть представления. Но когда я начал его использовать, я понял, что каждый раз, когда я ссылаюсь на него в представлении, я также дублирую код в контроллере. Тогда может быть хорошей идеей поместить эту логику извлечения внутрь компонента.
Это было открытием для меня. С помощью отражения и дженериков мне удалось создать инкапсулированный компонент, который занимался извлечением, отображением и авторизацией действий над вложениями.
Но некоторые страницы должны иметь определенный список вложений, которые не связаны только с конкретным объектом. Я попытался вставить это в компонент, и он быстро перестал работать. Поэтому я решил разделить часть представления и логику компонента и использовать только часть представления. Бизнес-часть каждый раз писалась вручную, потому что не вписывалась в общую логику.
Урок: знайте инструменты, которые вы используете, и что ваш код делает под капотом
В какой-то момент пользователи начали жаловаться на производительность определенной страницы. Опять же, чтобы ускорить первоначальную разработку, команда разработчиков решила получать все данные при загрузке страницы. Пейджинг, фильтрация и упорядочение выполнялись на стороне клиента.
Со временем количество строк выросло более чем на 5 000. Само по себе это не было проблемой. Что действительно убивало производительность, так это вложения.
Похоже, кто-то пришел к выводу (как и я, когда извлекал компонент), что при извлечении вложений происходит много дублирования кода.
Чтобы предотвратить это дублирование, была включена активная загрузка. Для каждого объекта ORM по умолчанию загружала все его вложения. Отношения между сущностью и вложениями были «один ко многим», поэтому ORM не могла получить эти вложения в рамках одного запроса. Это означало, что для каждой записи (строки) ORM будет делать дополнительный запрос к базе данных.
Поскольку первоначальный запрос требовал извлечения двух объектов для отображения данных в представлении, результатом для 5 000 строк было более 10 000 запросов к базе данных при загрузке страницы. И вложения даже не использовались на этой странице.
Я не мог просто отключить такую загрузку, потому что она использовалась слишком во многих местах, и я не мог быть на 100% уверен, что охватил их все. В конце концов, «решение» состояло в том, чтобы просмотреть все страницы, на которых отображаются списки, и вручную отключить активную загрузку вложений.
Заключение
Поддержание унаследованного кода часто угнетает. Поддержание кода в рабочем состоянии, имея дело с кучей плохих решений других людей (или ваших), быстро разочаровывает. Каждое письмо, звонок или сообщение, которое мы получаем, означает, что что-то не работает, и нам нужно это исправить. Трудно не попасть под влияние этого.
Но работа с унаследованным кодом не должна быть кошмаром. Мы можем смотреть на это как на возможность отточить наши навыки. Любой код является конечным продуктом цепочки решений, принятых для решения какой-либо проблемы. Зная, когда, почему и как что-то пошло не так, мы можем многое узнать о разработке программного обеспечения. И, возможно, код, который мы сделаем, будет лучше кода, доставшегося нам по наследству.