Кровавое легаси: как в одиночку раздробить монолитный сервис и не сойти с ума

image-loader.svg

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

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

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

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

В воздухе чувствовалось нарастающее напряжение. Руководитель заговорил, и я почувствовал себя, словно на гильотине. 

Произошло действительно страшное: мне предстояло путешествие по удивительному миру legacy-кода в старом корпоративном сервисе.

Часть первая: заглядываем врагу в лицо

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

image-loader.svg

Это было древнее корпоративное зло, которое пришло и отравило наш мир где-то в 2011 году. Оно представляло собой монолитный Windows-сервис, использующий технологию WCF. MS SQL Server 2008 R2 использовался в качестве СУБД, а NHibernate — в качестве ORM. Для взаимодействия с конечным пользователем он использовал несколько Silverlight-сайтов с разным функционалом, который обращался к этому Windows-сервису через WCF-клиент.

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

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

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

image-loader.svg

Сервис решал ряд задач:

1.       строил отчеты — как правило, в формате Excel: как по требованию, так и по расписанию; затем они рассылались по почте или скачивались напрямую;

2.       собирал статистику по выполненным на проектах работам путем загрузки в БД файлов статистики, которые выкладывались другим сервисом;

3.       позволял настроить цены и коэффициенты для тех или иных проектных работ, которые затем использовались при построении отчетов;

4.       позволял гибко настроить правила обработки статистических файлов.

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

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

Забегая вперед, скажу, что, на мой взгляд, по итогу у нас так и не получилось «чистых» микросервисов, так как некоторые вещи были слишком дорогими в переписывании, и с ними пришлось мириться. Например, некоторые отчеты строились монструозными процедурами со стороны SQL Server, которые от греха подальше решено было не трогать, и в них могло идти обращение к таблицам с настройками и всем остальным.

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

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

Часть вторая: укрощение

Итак, было решено все это поставить на рельсы:

  • ASP.NET Core 2.0 WEB API, что давало возможность без проблем взаимодействовать с любыми технологиями;

  • Entity Framework Core, поскольку он доступен из коробки, и за многие годы Microsoft его напильником довели до ума;

  • Angular + TypeScript, так как наш frontend-разработчик работал с этими технологиями, да и в целом Angular — это стильно, модно, молодежно.

Концепция нового проекта была довольно простой:

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

  1. перечень DataSource, так как данные для отчета могли браться из нескольких источников,

  2. вспомогательные service-классы.

В метод Build, когда нужно было построить отчет, передавался объект, который содержал необходимый набор параметров, нужных для построения, — например, на какую дату строить отчет. Сам метод возвращал объект byte[], который либо отсылался назад пользователю, и он его скачивал браузером, либо выполнялась рассылка на почту.

  • Так как на тот момент в Entity Framework Core было проблематично использовать SQL-процедуры и вызывать «грязные запросы», которые передались по наследству от старого сервиса (а их приходилось использовать в местах, которые трудно было переписать или сложно быть выразить через EF), была заведена сущность DataSource, которая скрывала внутри себя использование тех или иных запросов;

  • Контроллеры должны быть очень простыми, и в этом пришла на помощь библиотека MediatR, которая помогла перенести код в отдельные классы, при этом не создавая сильной связанности;

  • Разумеется, все зависимости должны были внедряться через конструктора, и для этого стала применяться мощная библиотека autofac;

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

«Legacy как коробка шоколадных конфет: никогда не знаешь, какая начинка тебе попадется»

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

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

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

Открыть старый Silverlight-проект в новенькой Visual Studio 2019 было делом не таким простым, как я думал, скажу я вам, так как поддержка этого типа проектов когда-то была просто выпилена, и он корректно не открывался. Пришлось выйти в Интернет с этим вопросом, ведь не ставить же мне Studio 2013 для того, чтобы зайти и просто посмотреть, что там в документе. Можно было, конечно, воспользоваться Notepad++, но без удобной навигации IDE это было бы не так комфортно. К счастью, решение было найдено в виде отдельного плагина от сообщества, который позволил открыть проект и сделать то, что я планировал: сопоставить контракт с UI и понять, за что тот или иной параметр при построении отвечает.

В первом же отчете я увидел то, что количество статики зашкаливает. Дело в том, что нельзя сразу понять, что требуется тому или иному классу для работы, и смотря на класс, которому в конструкторе передается два каких-то базовых типа, создается обманчивое впечатление, что внутри класса ничего сверхъестественного не происходит. Но потом, когда заглядываешь в сам класс, приходит понимание, что там творится вакханалия — например, работа с БД или запрос к AD. Временами даже доходило до абсурда, когда в конструкторе какого-то класса шло прямое обращение к БД за данными, которому, как мне кажется, там не самое лучшее место.

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

В целом статика оказалась не такой уж и большой проблемой, ведь там выполнялся вполне конкретный и ограниченный набор действий: контекст доступа к БД, доступ к AD и т, д. В остальном коде было много участков, которые так и порождали в голове вопросы в духе: «Зачем это нужно именно в этом классе?», — что приходилось также выносить во вспомогательные классы-сервисы.

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

Помимо этого, в коде временами встречались пугающие комментарии, которые предостерегали: «Если кто-то захочет поменять <>, то лучше его отговорить от этого».

Но я не мог предполагать, что меня ждут еще более страшные вещи: большое количество бизнес-логики в хранимых процедурах, Views и прочих объектах SQL Server. Вы когда-нибудь видели, как из MS SQL Server напрямую обращаются к Active Directory через OpenQuery, чтобы получить список пользователей для фильтрации людей, которые должны или не должны попасть в отчет, или что делается через xp_cmdshell? Так вот, я увидел эту воочию. Кроме того, было много самописных CLR-функций на C#, которые выполняли тоже какую-то особую логику — например, алгоритм, который вычисляет, насколько строка А соответствует строке Б. Но и это еще не все. Были монструозные SQL-процедуры — строк на 500, если не больше, — с множеством вложенных подзапросов с различными типа соединений, также попадались обращения ко вьюхам и прочим вещам.

«Я видел такое, что вам, разработчикам, и не снилось… Конструкторы классов, которые занимали несколько экранов. Я наблюдал, как SQL Server делает запросы к AD, чтобы фильтровать результаты запроса по пользовательским группам. Все эти мгновения затеряются во времени, как слезы под дождем. Время рефакторить».«Я видел такое, что вам, разработчикам, и не снилось… Конструкторы классов, которые занимали несколько экранов. Я наблюдал, как SQL Server делает запросы к AD, чтобы фильтровать результаты запроса по пользовательским группам. Все эти мгновения затеряются во времени, как слезы под дождем. Время рефакторить».

Справиться с бизнес-логикой было крайне тяжело на стороне SQL Server, поэтому пришлось пойти на компромиссы: что понятно и может быть выражено на стороне новой службы в виде какого-то объекта (например, доступ к АД) или понятным EF, рефакторилось, а с остальным в той или иной мере пришлось мириться и оставлять как есть.

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

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

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

«Я уже говорил тебе, что такое безумие, а? Безумие — это точное повторение одного и того же действия. Раз за разом, в надежде на изменение. Это есть безумие».«Я уже говорил тебе, что такое безумие, а? Безумие — это точное повторение одного и того же действия. Раз за разом, в надежде на изменение. Это есть безумие».

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

Стоит ли мне говорить, что это все сильно выматывало?

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

image-loader.svg

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

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

В заключение

«Все отчеты были переписаны. Последний тап поставил жирную точку в этой истории. Я убрал пальцы с клавиатуры — все было кончено».

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

image-loader.svg

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

Что я усвоил, работая над этим проектом?

  • Корпоративный проект любой сложности можно переписать, как бы страшен он ни был;

  • Иногда приходится идти на компромиссы в силу сжатых сроков и нехватки ресурсов — и что-то оставлять в неизменном, пусть и слегка переработанном виде;

  • Правило «работает — не трогай» далеко не универсально: то, что работало годами, могло делать это неправильно;

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

  • Важно не закапываться в работу и в свободное время переключаться на что-то другое — соблюдать work-life balance.

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

© Habrahabr.ru