Внедрение Django приложения в legacy-проект

Мой опыт по внедрению django приложений в среду с legacy-приложениями.

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

Для начала – что такое legacy.

Есть много разных определений legacy кода и приложений.

На моем опыте легаси это:

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

Цели и причины внедрения Django приложения

Причины для доработки или переработки legacy проекта могут быть следующие:

  • Компания развивается, бизнес требует дополнительных возможностей от системы, которые один программист внедрять уже не успевает.
  • Производительности legacy-системы не хватает, а программист либо не в силах исправить проблемы, либо уже уволился, либо в ужасе побежал писать заявление на увольнение, когда узнал, что весь его говнокод, надо переписать, а он и сам не хочет уже его касаться. 
  • Legacy-проект представляет собой набор всяких скриптов, которые автоматизировали разные части деятельности компании, но между друг другом они плохо или совсем не связаны. Обороты компании возрастают и требуется качественная автоматизация, а имеющиеся инструменты уже устарели, не справляются по производительности, на них сложно "посадить" нового сотрудника, ошибки связанные с человеческим фактором при использовании этих инструментов переваливают за допустимые рамки.
  • Кто-то в компании наконец-то понимает, что legacy-проект работает не правильно, не дает возможности расти.
  • Кто-то в компании давно знает, что legacy-проект унылое 💩 и компания наконец-то заработала денег на нормальную систему.

Что я подразумеваю под [legacy] системой?

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

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

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

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

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

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

Я буду вести речь о трех типовых вариантах, которые мне встречались наиболее часто.

  1. Есть legacy система, которая работает, её никто не собирается менять, но нужно реализовать значительную по объему дополнительную функциональность.
  2. Есть legacy система, которая работает, но её нужно заменить.
  3. Есть набор разрозненных систем/скриптов, которые как-то работают и нужно их всех заменить одной целостной системой.

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

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

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

Именно на вопросы сопряжения я и хочу заострить внимание.

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

Некоторые характеристики legacy-систем с которыми я сталкивался наиболее часто:

  • Набор самописных PHP-скриптов или кастомизированная CMS-ка.
  • Mysql в качестве СУБД.
  • Хостинг: редко свой сервер, как правило дохлая VPS с какой-нибудь панелью (ISPManager, реже – cPanel), а может быть вообще Shared хостинг.
  • Никакой документации, все знания в головах, передаются "оральным" путём.
  • Данные в кодировке windows-1251, а даты без timezone, иногда timestamp.
  • Исходный код системы без контроля версий, все изменения вносятся напрямую на production среду по ftp.
  • Как следствие из предыдущего пункта – тестирование происходит вяло разработчиком после сохранения файла либо Customer Driven Testing, когда тестируют пользователи, для которых это предназначалось – сотрудники или клиенты.

Для реализации новой функциональности хочется выбрать Django. Как её подружить имеющейся системой?

Разумеется, надо определиться какими именно частями системы надо подружить.
Вот топ3 пунктов для интеграции:

  • Пользовательская(сотрудники) база, единые данные для авторизации.
  • Клиентская база,
  • Данные о заказах клиентов.

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

Варианты обмена данными:

  1. Селим Django в туже БД, где живет исходная система
  2. Селим Django в отдельную БД(может быть даже СУБД - здесь уже не важно), также подключаем Django к БД исходной системы
  3. Джанго в отдельной БД, интеграция с исходной системой по API.

Плюсы и минусы каждого варианта.

1. Django-проект в той же БД, что и исходная система.

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

Минусы:

  • исходная база может быть так спроектирована, что ORM django не сможет с ней работать. Самый простой пример – составные Primary ключи, django ORM с ними не работает, а написание запросов вручную – убивает всю скорость разработки и возможность поддержки – будет еще одна legacy-система.
  • целостность БД исходной системы скорее всего ничто гарантировать не сможет, поэтому будем ловить ошибки при работе с данными, а чтобы их избежать придется писать уйму дополнительных обработок. Здесь, кроме скорости разработки, снижается стабильность нового компонента.
  • Данные в БД исходной системы могут храниться так, что стандартные или популярные Python/django библиотеки не смогут работать или будут работать некорректно. Одним из самых прекрасных примеров был, когда в исходной системе я увидел интересный подход к работе с деревом элементов. Корневой элемент с id=1 имел parent_id=1, а не parent_id=None. Соответственно, я получал рекурсию при попытке работать с этим данными. Разработчики исходной системы конечно же сильно удивились, что деревья по моему мнению пишутся как-то не так, но переписывать логику пришлось на моей стороне.
  • Теперь, аспект не имеющий отношения к техническим моментам, а скорей к человеческому фактору, который еще называется просто как "раздолбайство": если таблицы Django-приложения будут жить там же где и исходная система, то кто-то из сотрудников, поддерживающих или разрабатывающих исходную систему обязательно залезет в ваши таблицы, чтобы что-то побыстренькому поправить. При этом есть шанс, что "ляжет" именно Django-проект, собак спустят на разработчика Django-проекта, разбираться вам, а на самом деле виноват тот, кто "пошел быстренько напрямую в БД всё уже поправил". Это бывает. Часто. Что вы хотите от людей, чье творение является legacy-💩?

2. Django-проект в отдельной БД, а БД исходной системы прописываем вторым подключением в Django проекте.

Плюсы: Можно ограничить доступ других сотрудников к этой БД, можно использовать другую СУБД, например Postgres, в то время как исходная СУБД: Mysql. Проект полностью изолирован, данные приложения могут жить на другом сервере в другой СУБД, влияние на внутренние данные извне практически отсутствует.

Минусы:

  • Нельзя делать JOINы между объектами разных БД. Причем в случае с Django не важно используется у нас другая СУБД или другой сервер БД – DjangoORM не дает возможности использовать таблицы, лежащие не в той БД, что прописана в подключении. Это может сильно отрицательно сказаться на производительноти. Как выйти из этой ситуации: если БД и сервер БД тот же самый, то можно из исходной системы "пробросить" таблицы в БД приложения с помощью создания VIEWs. Важно, чтобы VIEWs были полной копией без всяких изменений, так как только в этом случае будут работать индексы. Если структура таблицы изменится во вьюхе, то индексы использоваться не будут, а производительность упадет. 
  • Бестолку использовать ForeignKey на объекты из другой БД - целостность не гарантируется. Чтобы сделать ForeignKey на объект другой таблицы, нужно в поле FK написать db_constraint=False. С этого момента придется ставить дополнительные проверки на то, существует ли до сих пор связанный объект или нет. Важно понимать, что многие разработчики считают нормальным физически удалять записи из таблицы, а не помечать их удаленными. Это также применимо для ситуаций, когда это критичные для целостности данных записи, вроде адреса клиента для доставки заказа. Я участвовал в переработке ecommerce проекта, где клиент мог удалить ненужный ему адрес доставки по которому уже есть заказ, а в отчетах менеджерам интернет-магазина таким образом не попадали заказы без адресов :)

3. Django-проект в отдельной БД, интеграция с помощью API.

Плюсы:

  • Стабильность работы приложения больше не зависит от кривой БД и структуры данных исходной системы.
  • Проект полностью изолирован от "дурного влияния" исходной системы в следствие их сопряжения через БД.
  • Проект полностью отцеплен от структуры данных исходной системы и нет потери производительности на работе с ними, цикличных запросах ввиду отсутствия возможности сделать JOIN и прочих моментах.

Минусы:

  • API кто-то должен реализовать. Причем реализация скорее всего должна быть с обеих сторон. Особенно важно, если исходная система должна получать данные из Django-проекта.
    Если поддерживать исходную систему некому, то придется писать интеграционный слой самому, например на том же PHP.

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

Например, если Django-проект занят обработкой заказов клиентов, а исходная система это CMS-ка, в которой клиенты оформляют заказы, то надо быть уверенным, что работая с данными по заказу и клиенту, данные клиента акутальные и не поменялись с момента последней синхронизации данных. В противном случае, можно попасть в неприятные ситуации, когда клиента уже нет в базе(я же говорил, что многие программеры любят удалять данные из БД?), email клиента поменялся, это был primary ключ, таким образом клиента такого у нас опять в базе нет, а мы ему еще и письмо на эту почту отправить пытаемся.

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

Кроме всего прочего, интеграция с помощью API это единственный способ при котором можно весь код покрыть тестами и быть уверенным хотя бы на 90%, что новое приложение будет работать стабильно. В случае с интеграцией через общую БД все тесты теряют всякий смысл, когда вы не знаете какие данные окажутся в БД, так как не ваше приложение их туда положило. Это приведет к множеству бессонных ночей, многочисленных писем от Sentry и сотрудников, которые будут жаловаться, что всё упало. В добавок – тесты это одно из проявления документирования кода и проверки его соответствия бизнес-требованиям. Можно даже сказать, что если нет тестов, то проект – УЖЕ legacy.

Выводы:

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

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

Опубликовано: 26 сентября 2016 г.

There should be one-- and preferably only one --obvious way to do it.

PEP20: The Zen of Python