
2024-05-10
Как спроектировать продукт без опыта
2023-10-26
Перед каждым разработчиком рано или поздно встает вопрос проектирования системы. Ни для кого не секрет, что по-правильному нужно систему сначала спроектировать ( не в голове представить ), а уже потом начинать писать код. Но далеко не все разработчики сначала проектируют, а потом пишут. Наоборот - большая часть разработчиков в обычных среднестатистических компаниях обычно пропускают стадию проектирования и сразу приступают к разработке на привычном технологическом стеке, даже не думая ни о чем вообще имея в голове лишь призрачную тень того как должен выглядеть продукт. Это может привести сразу ко многим печальным последствиям.
Во-первых, тень в голове разработчика, которую он представил в виде рисунка конечного продукта может кардинальным образом не совпадать с видением продукт оунера. И это является чуть ли не самой страшной проблемой, которая может быть, потому что с ней по количеству боли не сравнится даже рефакторинг только что написанного продукта.
Во-вторых, разработчик может и скорее всего не сможет предусмотреть все возможные сложные аспекты продукты только в голове не расписав их "на бумаге". И скорее всего произойдет так, что либо продукт придется рефакторить прямо во время написания, либо сразу после написания. Он может не выдерживать нагрузок, может получиться не расширяемым, может случиться всё что угодно.
В-третьих, без мнения со стороны будет очень сложно написать хоть сколько-нибудь качественный документ, потому что глаз замыливается и вы можете не видеть каких-то нюансов, которые сможет увидеть другой разработчик или можете не предусмотреть какие-то стечения обстоятельств.
Итак... Мы определились, что нам стоит всё же составить документ, который будет отражать всю структуру нашего продукта, его компоненты и их взаимодействие друг с другом. Предположим, что мы будем проектировать какой-то сервис внутри микросервисной архитектуры. По-факту, для более простого понимания - это монолит внутри микросервисной архитектуры.
ВНИМАНИЕ - в данной статье не показан правильный или неправильный пример архитектуры. Я постарался описать процесс ведения мысли внутри головы архитектора / разработчика с пустого места до реализации продукта. Как стоит выстраивать процессы в своей голове, чтобы на выходе получился готовый продукт. Я не претендую на идеальную архитектуру вымышленного продукта вымышленной компании. Цель данной статьи - научить новичков в этом деле правильно мыслить и дать отправную точку для старта.
Первым делом нам необходимо собрать максимально полную информацию о том, что за продукт мы собираемся реализовать. Какова его цель? Кто будут его пользователи? Кто являются заказчиками? Необходимо найти человека, кто максимально заинтересован в этом продукте внутри компании. Необходимо постараться узнать и рассчитать какие предполагаются нагрузки на этот продукт.
Вам потребуется по-возможности максимально детальное описание продукта с бизнесовой точки зрения.
Скорее всего заказчик, продукт оунер и аналитик не дадут вам с ходу ответы на все ваши вопросы, а информация будет зачастую максимально расплывчатая. Но даже из информации вида "хочу то не знаю что" всегда можно выцепить какую-то ниточку, которая приведет вас к конечному видению продукта целиком. Ваша задача на первом этапе как раз таки и состоит в сборе этой информации. Первоначально она будет вся смешанная и неоднородная. Вам нужно будет всё это собрать в единый документ, логичный и структурированный.
После того как вы собрали всю возможную информацию о том, что же именно должна делать новая система по мнению заказчиков - вам необходимо сесть и продумать нефункциональные требования.
Нефункциональные требования - это такие аспекты системы, которые напрямую не определяют бизнес-функциональность, но имеют значительное влияние на эффективность и полноту реализации системы для конечных пользователей, а также для тех, кто поддерживает систему.
Вам как разработчику в условиях того, что мы проектируем монолит внутри микросервисной архитектуры в первую очередь необходимо думать о следующих факторах:
Каждый из этих факторов очень важен для конечного продукта.
Доступность - период времени, в течение которого система функционирует нормально, без сбоев. Это речь как раз таки про пресловутые 99,99% uptime. Все стремятся к этой цифре. И в целом это оправдано, потому что продукт должен работать в режиме 24/7 и не сбоить.
Изменяемость - стоимость изменений и легкость, с которой проектируемая система может аккумулировать изменения. Другими словами - это насколько вам будет легко внести корректировки в ваш продукт или его часть, не задев при этом остальную функциональность не связанную с текущей задачей. Если простыми словами - систему надо проектировать таким образом, чтобы при модификации того или иного куска кода - у вас бы не начинало "отрывать" то тут то там.
Производительность - наверное один из самых простых и понятных факторов, который говорит нам о том, что система должна держать нагрузки и должна не падать при нагрузках сверх нормы ( ~+20% от нормы ). Всегда стоит ориентироваться не на среднестатистические показатели, а закладываться на самые плохие условия в которые может попасть ваша система. И вот к самым плохим условиям прибавьте еще 20%.
Безопасность - тут тоже всё вполне себе очевидно. Система не должна иметь уязвимостей, через которые могут ее взломать. Как минимум стоит изучить OWASP top-10 и посоветоваться с it-sec специалистами в компании, если таковые имеются.
Тестируемость - система признается тестируемой, если она легко "отдает" свои ошибки. Тестирование системы очень сильно влияет на стоимость конечного продукта для бизнеса. И если продукт будет сложно тестировать - то стоимость его значительно возрастет, что плохо.
Удобство использования - отвечает на вопрос, насколько легко пользователю выполнить желаемую задачу, а также о том, какую пользовательскую поддержку обеспечивает система. Соответственно, чем выше уровень юзабилити - тем лучше.
Расширяемость - представим, что вы уже почти закончили продукт, но тут снова приходит продукт оунер со словами "а давайте еще сделаем тут надстройку функционала". В идеале - в этот момент вы должны потерять ровно ноль нервных клеток, так как вы заранее спроектировали систему расширяемой. Чем больше нервных клеток потеряно - тем хуже расширяемость. В крайних случаях - вам придется развернуть продукт оунера с его идеями и он будет огорчен, а соответственно будет огорчен и бизнес.
Поддерживаемость - когда вы закончите работу над продуктом - его необходимо будет поддерживать, не только код, но и серверную архитектуру. Поэтому очень важно подумать об админах и о том технологическом стеке, который вы собираетесь применять. Если у вас внутри компании принят какой-то технологический стек - то лучше придерживаться его, но не ограничиваться. Всегда можно запросить новые технологии, но это нужно очень четко обосновать и запрашивать только принципиальные вещи, без которых нельзя обойтись и которые дают прямой аффект на один из вышеперечисленных факторов.
Взаимодействие с другими системами - так как мы находимся внутри микросервисной архитектуры - скорее всего вам придется взаимодействовать с другими ее компонентами. Это тоже необходимо заранее продумать, чтобы не вышло так, что придется подстраивать другие системы под вашу - вряд ли это кто-то из другой команды будет делать.
Все эти факторы необходимо также расписать и оформить в тот же документ, что и функциональные и нефункциональные требования. На выходе у нас должен получиться вполне себе вменяемый документ, который можно передать продукт оунеру на согласование.
После того как вам согласовали все требования - вы теперь можете быть уверены, что картинка в вашей голове и в головах продукт оунера и заказчиков совпадают и это хорошо!
Теперь нам необходимо погрузиться в технические детали. Мы рассмотрим дальнейшие действия на примере одного выдуманного микросервиса.
Предположим, что наша компания - это какой-то крупный застройщик, который строит элитные жилые комплексы по всей стране и за рубежом. Все комплексы объединены в одну it-инфраструктуру или как сейчас модно говорить "экосистему", которая поразумевает под собой все востребованные услуги, связанные с жильем. Но мы остановимся, например на оплате услуг. Предположим, что вот у нас есть тысячи жилых комплексов и коттеджных комплексов и им всем нужно оплачивать ЖКХ, Телевидение, Интернет, Газ, и прочие какие-то услуги. То есть платежи происходят не разовые, а на постоянной основе в совершенно разное время дня и ночи. Каждый комплекс имеет управляющую компанию, которая является отдельным юридическим лицом, не являющимся частью нашей организации. Часть этих платежей приходит в нашу компанию и другой микросервис нашей организации их обрабатывает и складывает данные о проведенных транзакциях в DWH (хранилище, предназначенное для сбора и аналитической обработки исторических данных организации). Это один наш источник информации. Вторым источником информации являются разнообразные отчеты от управляющих компаний. DWH имеет API для получения данных, а вот обслуживающие организации разнообразят форматами предоставления данных у кого-то это csv присылаемое на почтовый ящик, у кого-то это API, у кого-то это Excel, который скачивается по определенному урлу в определенное время, и т.д.
Задача нашего микросервиса в том, чтобы сверять данные в нашем DWH с теми данными, которые предоставляют управляющие компании для того, чтобы дополнительно контролировать И управляющие компании И наши внутренние системы на предмет сбоев и ошибок.
Например мы предполагаем, что пользователями нашей системы будут наши же внутренние сотрудники ( далее саппорты ), которым нужна будет админка для отображения данных. Админка находится у нас в отдельном микросервисе и может взаимодействовать с нашим сервисом по API.
Теперь мы идем к саппортам, рассказываем им о новом продукте, который им через какое-то время предоставят и собираем с них информацию о том, что им вообще нужно для того, чтобы этот новый инструмент был для них удобным. За одно стараемся собрать дополнительную информацию о том как они сейчас вообще взаимодействуют с управляющими компаниями. Оказывается они и сейчас проводят сверки, но всё это происходит очень долго и на ручном приводе в каких-то своих эксельках с помощью макросов, составленными для них админами в каком-то мохнатом году.
Итак, мы уже имеем некоторую полезную информацию, которая поможет нам с проектированием нашего сервиса.
Также у нас есть ряд технических требований к продукту:
От этих данных уже можно отталкиваться.
Итак, чтобы лучше визуализировать наш продукт изнутри - нам лучше всего накидать его в виде диаграммы. Но прежде чем это сделать нам стоит прикинуть основной технологический стек.
Предположим у нас есть команда php разработчиков в составе из 2 человек и 1 фронтэнд разработчик и вы как senior разработчик / архитектор / лид / скрам-мастер. Соответственно будем исходить из того, что мы не хотим собирать новую команду, а значит у нас будет язык php, возьмем свежую на текущий момент версию php 8.2.
Смотрим далее... У нас нет ограничений по использованию фреймворков, а следовательно мы можем выбрать один из самых популярных на текущий момент фреймворков - это или Symfony или Laravel. В этот момент стоит почитать статьи и сравнения, а так же посоветоваться с командой, чтобы лучше выяснить различия, достоинства и недостатки фреймворков. Предположим, что всё это мы сделали и определились, что писать мы будем на Symfony, на текущий момент самая актуальная версия - это 6.3.
Нам также потребуется база данных, проще всего взять реляционную базу данных, например mysql или postgres - про них также стоит почитать, осознать различия, оглядеться внутри компании в поисках специалистов по работе с той или иной БД и сделать осознанный выбор. Например мы остановились на mysql percona 5.7, так как с ней все умеют работать, а у админов есть готовые настроенные пресеты, которые они смогут быстро развернуть нам на стенд.
Вот у нас уже появился базовый технологический стек:
php 8.2
symfony 6.3
mysql percona 5.7
И теперь мы можем сделать первый набросок архитектуры нашего приложения. Для этого хорошо подойдет онлайн сервис app.diagrams.net или любой другой аналогичный.
Итак... Сделаем первый набросок...
Что же мы теперь можем видеть на нашей диаграмме.
Мы видим, что слева сверху у нас есть DWH. Справа сверху у нас есть несколько интеграций с управляющими компаниями ( будем называть их интеграциями для краткости ), а так же есть админка снизу слева и база данных снизу по центру. А в самом центре расположен наш сервис.
Пока что мы отрисовали только взаимодействие нашего сервиса с окружающим миром, а сам сервис для нас пока что не ясен и видится белым пятном. Наша задача сделать из этого белого пятна понятный и прозрачный сервис.
Итак наша цель - сверять пачки операций посуточно или около того. В общем, за какой-то Nный интервал, который редко превышает 3 дня, в основном - это сутки. Первым делом давайте определимся с тем какие у нас будут ключевые сущности внутри нашего сервиса.
Самые очевидные сущности, которые первыми бросаются в глаза это:
Давайте подумаем какие у нас могут быть атрибуты у каждой сущности:
DwhOperation
operationId - основной идентификатор операции на стороне нашей компании
transactionId - транзакция в которую входит несколько операций
amount - сумма операции в минорных единицах
currency - валюта проведения операции
createdAt - дата и время создания операции
vendorPaymentId - идентификатор операции на стороне вендора
operationTypeId - тип операции ( оплата / возврат / кэшбэк / комиссия / списание по подписке и т.д. )
operationStatusId - статус операции ( success / decline / outdated и т.д. )
VendorOperation
vendorPaymentId - идентифиактор операции на стороне вендора
amount - сумма операции в минорных единицах
currency - валюта проведения операции
createdAt - дата и время создания операции
operationTypeId - тип операции по версии вендора
operationStatusId - статус операции по версии вендора
ReconciliationResult
dwhOperationId - идентификатор операции DWH
vendorOperationId - идентификатор операции вендора
reconciliationTaskId - идентификатор таска сверки, к которому относится результат сверки
status - статус сверки
ReconciliationTask
id - идентификатор таска сверки
reportDate - дата отчета
status - статус таска сверки
vendor - название управляющей компании
createdAt - дата и время создания таска сверки
updatedAt - дата и время обновления таска сверки
error - текст ошибки если таковой имеется
ReconciliationCalendar
id - идентификатор записи в календаре
date - дата
process - запускать процесс или нет boolean поле
vendor - название вендора
createdAt - дата и время создания записи в календаре
updatedAt - дата и время обновления записи в календаре
Откуда мы вообще взяли сущности - ответ довольно прост - придумали. Нам нужно подключить голову, немного воображения и придумать сущности, которые будут описывать наши бизнес-процессы и сущности из реального мира. Таким образом у нас есть операция на нашей стороне, которую надо сверить с операцией на стороне вендора в результате чего мы получим некий результат сверки. Все это будет происходить в некоем процессе, который мы назвали таск сверки, а управляться запуски будут в том числе календарем сверок.
Давайте отобразим теперь на схеме наши базовые сущности:
Итак, мы отобразили на схеме наши базовые сущности, но пока что это всё ещё не похоже на какой-то продукт. Мы обозначили только лишь основные понятия на вскидку.
Теперь самое время продумать процесс самой сверки.
Нам необходимо продумать как будет с технической точки зрения проходить сам процесс сверки. Сейчас мы знаем только отправную точку и конечную точку, а что происходит посередине и как это происходит - мы не знаем, а должны знать четко и ясно. И здесь нам придется задавать все много правильных вопросов.
Начать стоит с глубокого изучения данных и попробовать сверить какой-то кусок данных вручную или написать какой-то простейший однопоточный скрипт, который сверит данный напрямую. Такой подход поможет выявить довольно большое количество интересных деталей, которые помогут сформировать правильную архитектуру нового приложения и избежать досадных просчетов при построении архитектуры продукта.
Итак, мы проанализировали данные и понимаем, что нам нужно как-то подбирать операции друг к другу по какому-то свойству. И у нас в текущей версии событий есть единственное свойство по которому мы можем матчить операции - это vendorPaymentId, так как этот ключ если и в операции DWH и в отчетах вендора. Соответственно мы выбираем это свойство как ключ матчинга операций по которому будем осуществлять основной подбор операций. Но мы знаем, что у многих вендоров это поле не является уникальным и например может содержать под собой несколько различных операций, например продажу и возврат. Тогда мы делаем вывод, что ключ матчинга у нас будет составным и будет включать в себя vendorPaymentId и тип операции.
С ключами определились - теперь стоит рассмотреть сценарии сверки, которые могут происходить в процессе сверки.
Отправной точкой сверки является отчёт, так как в нём данные уже каким-то образом нарезаны по временному интервалу. Мы понимаем, что данных у нас может быть довольно большое количество, а это значит, что осуществлять поиск поштучно для нас - не вариант. Значит нам нужно сначала получить данные от вендора, а потом получить данные из ДВХ, которые коррелируют с данными вендора. Мы это можем сделать запросом по ключам матчинга. Но если мы ограничимся только такой выборкой, то мы не сможем закрыть сценарии, когда мы нашли операции на стороне DWH, но не нашли данные на стороне вендора данных...
Нужно подумать, как мы можем закрыть этот сценарий... Самый очевидный способ - это делать выборку по времени, например, с даты и времени создания операции, сверенной последней в предыдущем таске сверки и до даты и времени создания операции сверенной последней в текущем таске сверки. Таким образом мы сможем выбрать все операции из DWH, которые подпадают под интервал операций в отчете вендора данных.
Но как же это реализовать? Немного поразмыслив и почитав паттерны проектирования - мы понимаем, что нам нужна стадийность сверки, так как некоторые процессы внутри сверки у нас не могут проходить в параллель, а их нужно запускать последовательно друг за другом, так как они опираются на данные, полученные в результате работы предыдущих стадий.
Таким образом у нас в голове вырисовывается концепт, что у нас как-то должна запускаться сверка, у нее должны быть стадии и по итогу всех стадий - она должна выдать результаты своей работы.
Тут же сразу возникают дополнительные вопросы...
На все эти вопросы нужно найти ответы.
Итак прикинем, какие у нас могут быть стадии сверки...
1 - initiated - стартовая стадия, когда мы поняли, что нам нужно что-то начать сверять. На этой стадии мы создадим таск сверки и зарядим в него все необходимые для сверки параметры - это название вендора, сверяемая дата и т.д.
2 - get_vendor_operations - стадия, когда мы получаем данные от вендора данных и запоминаем их ( кстати куда мы их запоминаем - пока что вопрос )
3 - get_dwh_operations - стадя, когда мы получаем данные из DWH для сверки по ключам матчинга
Мы могли бы объединить 2 и 3 стадии в одну, но в этом случае стадия была бы перегружена и выполняла бы 2 логических действия одновременно и получение данных от вендора и из DWH, а в целом придерживаться SOLID стоит везде, даже в таких вопросах это бывает полезно.
4 - reconciliation_by_keys - сверка по ключам матчинга - в этот момент мы получим список первых результатов сверки, а так же дату и время последней сверенной в текущем таске сверки операции
5 - get_dwh_operations_by_interval - получение данных из DWH по интервалу - интервал, как мы помним, определяется датой и временем операции, сверенной последней в предыдущем таске сверки и датой и временем операции, сверенной последней в текущем таске сверки ( эту дату мы выявили в предыдущей стадии как раз )
6 - reconciliation_by_interval - сверка операций по интервалу
7 - reconciliation_results_saving - сохранение результатов сверки - предположим, что нам позволили сохранять результаты сверки в DWH, а не писать под это дело своё отдельное хранилище
8 - finish_reconciliation - на данном этапе мы закрываем все процессы, формируем данные для админки и завершаем процесс.
Также нам потребуется API для работы с админкой - давайте его тоже сразу запланируем ( хотя бы его наличие ).
Давайте теперь отразим это на схеме:
Итак на текущий момент у нас уже появилась какая ни какая схема работы нашего приложения.
Но все ещё есть много открытых вопросов. Как оно будет запускаться, транспорт, взаимодействие классов и т.д.
После того как мы сделали эскиз схемы работы приложения - нам нужно детализировать все открытые вопросы. Начнем пожалуй с того как будет запускаться каждая сверка. Потому что на текущий момент не понятно что будет триггером и как детально это будет работать.
Для того, чтобы понять как запускать - надо понять на чем будет основана работа нашего приложения. Это будут CLI команды? Этим можно будет управлять из интерфейса? Каждая стадия должна запускаться автоматически? Что будет переключать стадии между собой? На чем будет работать стадийность, на кронах или на очередях или возможно какой-то другой вариант?
Допустим мы определились, что мы не хотим ничего запускать вручную, мы хотим, чтобы все происходило автоматически, из интерфейса стадиями управлять нельзя. Осталось выбрать на чем будет построена система стадий. Да и в целом вся сверочная линия. Нужно подумать какие варианты вообще существуют...
1) сделать всё приложение линейным и в одном процессе, запускаемое по крону
+ максимально простая реализация
+ минимальная скорость разработки
- низкая производительность
- если где-то что-то падает - то падает весь процесс без возможности восстановления
- тяжелая поддержка, так как слишком долгие процессы сложнее мониторить
- отсутствие расширяемости
- отсутствие возможности перезапуска конкретной стадии
- расписание запусков придется хранить в операционной системе, что не хорошо, так как это бизнесовый функционал
- аллокация сразу большого количества ресурсов сервера на долгий срок
2) сделать приложение на очередях с запуском из-под супервизора
+ аллокация ресурсов сервера на короткие промежутки времени
+ расширяемость
+ возможность перезапуска стадий
+ более простой мониторинг
- более сложная реализация в сравнении с кронами
- дольше срок разработки
3) сделать приложение на очередях с запускаемыми в параллель стадиями
+ потенциально можем выиграть время и повысить скорость обработки данных
- очень сложная реализация
- очень долго разрабатывать
- не факт, что нам вообще это надо в текущих условиях
Исходя вот из такой раскладки можем сделать вывод, что реализовывать нужно на очередях с использованием супервизора.
ВНИМАНИЕ это всего лишь пример придуманный на ходу, который не претендует на звание лучшей архитектуры в мире. Возможно есть и лучшие варианты реализации.
Итак мы решили, что у нас будет реализация на очередях, но всё ещё не понятно как это запускать и кто будет управлять переключением стадий.
Предположим, что время запуска нам не нужно указывать с точностью до секунды, а значит в настройках каждой интеграции мы сможем использовать CRON-like расписание. Значит нам нужен какой-то класс, консольная команда, которая будет запускаться раз в минуту, сверять часы и анализировать исходя из настроек интеграций и календаря ( ReconciliationCalendar ) какие сверки стоит запустить в конкретную минуту времени. Проще всего это сделать консольной командой со sleep, которую поставим потом под супервизор. Назовем мы этот класс ReconciliationStarter. Таким образом мы определились откуда у нас будут появляться таски в очередь на обработку и таски в БД под сущность ReconciliationTask - их будет создавать ReconciliationStarter.
Теперь нужно решить вопрос о том кто будет менеджерить процессы стадий. Для этого создадим класс ReconciliationManager, который будет всем этим делом управлять. Это также будет демон обрабатывающий свою собственную очередь и переключающий стадии сверок по их завершении.
Также, глядя на схему мы понимаем, что мы не предусмотрели все необходимые поля наших сущностей, давайте их сейчас добавим:
ReconciliationResult.additionalInfo - поле для хранения детализированной информации о расхождениях найденных между операциями.
ReconciliationTask.attempts - счетчик количества итераций попыток свериться, так как мы помним, что от саппорта была информация о том, что ряд вендоров могут присылать отчеты не по расписанию, а с опозданием. В этом случае нам нужна будет механика перезапуска отдельной стадии по отложенному таймеру, что мы как раз таки и рассматривали среди достоинств и недостатков трёх реализаций стадийности.
ReconciliationTask.duration - нам бы пригодилось иметь статистику среднего времени работы одной сверки.
ReconciliationTask.lastOperationDt - поле для сохранения даты и времени последней сверенной операции в таске сверки.
ReconciliationTask.comment - комментарий от саппорта к таскам сверки ( по просьбе саппорта например )
Мы решили часть вопросов, но осталось еще несколько довольно важных технических и архитектурных.
1) транспорт данных между стадиями - нам же нужно как-то передавать данные между стадиями, иначе как они узнают с какими данными работать
2) на чём реализовывать очереди
3) как обрабатывать разные форматы отчетов
4) как обрабатывать специфичные случаи
Давайте начнем с транспорта. И тут мы опять же применим подход плюсов и минусов. Какие технологии нам доступным? Что принято использовать в компании? С чем наши разработчики из команды умеют работать и имели до этого опыт работы? Например мы ответили на все эти вопросы и сузили круг до двух вариантов - это memcached и redis. Обе технологии нам доступны и одобрены в компании и с обеими технологиями у нас есть опыт работы. Так как же выбрать? Они ведь обе такие хорошие!
В этом случае нужно смотреть на исходное предназначение инструментов и их различия.
Мемкеш - высокопроизводительный сервис, очень быстрый вне зависимости от количества хранимых данных, длина ключей максимум 250 байт, объем данных под одним ключом - 1 мб. Мемкеш - это хранилище ключ-значние, поддерживает атомарные операции. Быстрее, чем Redis.
Рэдис - поддерживает большое количество типов данных, умеет периодически сбрасывать свои данные на жесткий диск, позволяет хранить до 512 мб в значениях, поддерживает master-slave репликацию, можно использовать как постоянное хранилище данных. Однопоточный.
Здесь возможно выбор будет не совсем очевиден, но в целом Redis в данном случае будет более тяжелым решением и его возможности нам просто ни к чему, а вот мемкеш более легковесный, нам не нужно хранить данные на постоянной основе, а только пробрасывать их между стадиями. Мемкеша нам для этих целей будет более, чем достаточно.
Следующий вопрос - очереди и здесь точно также сужаем круг поисков решений до нескольких вариантов, например:
Kafka vs Gearman vs RabbitMQ
Предположим, что с Gearman у нас в команде никто не работал и под него нужно писать клиент с нуля, а это займет долгое время - поэтому он отпадает.
У кафки большая пропускная способность в сравнении с кроликом, легко масштабируется. Кролик имеет возможности сложной маршрутизации и поддержку разных протоколов - это его преимущества, но они нам не нужны. Поэтому выбор делается в пользу кафки.
У нас остается 2 открытых архитектурных вопроса и уже ощутимо разросшаяся диаграмма продукта, которая к нынешнему моменту приобрела вот такой вот вид:
На схеме мы обновили сущности, добавили связи между админкой и ДВХ, указали какие сущности у нас хранятся и взаимодействуют с БД, указали Kafka и Memcached.
Теперь нам необходимо решить вопросы кастомной обработки ( основная обработка/сверка у нас происходит в общих стадиях ), о которой нам сообщили саппорты, а так же различных форматов данных, поступающих от вендоров данных.
Здесь вступают в силу паттерны и ООП во всю свою мощь.
Первым делом давайте решим вопрос с различными форматами и прочими различиями между вендорами. Мы понимаем, что в целом процесс сверки одинаковый для подавляющего большинства вендоров и для подавляющего большинства операций. Различаются лишь форматы данных и некоторые аспекты реализаций интеграций с вендорами.
Соответственно нам необходимо выделить базовую часть и интеграционную часть. Для интеграционной части нам необходимо оформить какой-то интерфейс с которым будет работать базовая часть. В базовой части у нас будет расположена сверки операции DwhOperation и VendorOperation. Давайте попробуем определить интерфейс для интеграции:
ReconciliationInterface
public function getVendorData(): VendorOperationCollection - метод предполагает, что интеграция с вендором вернет коллекцию объектов VendorOperation - вызываться будет на стадии get_vendor_operations
public function getVendorReconciliationKey(VendorOperation): string - этот метод позволит нам доставать ключ матчинга из любого поля VendorOperation
public function canOperationsBeMatched(DwhOperation, VendorOperation): bool - метод позволяет нам определить имеем ли мы право сравнивать операции, если они совпали по ключу матчинга ( например сравнить тип операций )
public function compareOperations(DwhOperation, VendorOperation): ReconciliationResult - метод, который будет непосредственно сверять данные из двух операций, сравнивая их свойства.
Итак, сформировав такой интерфейс мы теперь может парсить разные отчеты в разных форматах и даже обращаться за данными в API, можем использовать различные ключи матчинга для различных операций, можем использовать дополнительные проверки для проверки фактической возможности сверки двух операций, а так же есть возможность сверять операции в разных интеграциях по-разному.
Соответственно теперь этот интерфейс необходимо отразить на схеме:
Итак, на схеме у нас появился ReconciliationInterface, а также еще одна стадия, которую мы забыли отобразить ранее - это reconciliation_by_interval. Теперь у нас остался последний вопрос - это как позволить сверкам осуществлять кастомизированные транзакционные сверки, где сверять нужно несколько операций.
Тут нам тоже поможет интерфейс, который будет уже опционален для реализации, в отличие от ReconciliationInterface, а назовем мы его CustomReconciliationInterface и у него будет всего один метод:
public function reconcileOperations(DwhOperations, VendorOperations): ReconciliationResultCollection - этот метод позволит сверять те операции, которые имеют полностью кастомный функционал сверки. В него будут передаваться оставшиеся не сверенные операции вендора и DWH, а на выходе будет ожидаться коллекция ReconciliationResult объектов.
Теперь его нужно где-то вызывать - скорее всего стоит завести для него отдельную стадию custom_reconciliation и теперь наша схема будет выглядеть так:
Итак, мы сформировали схему нашего приложения. Теперь нужно проверить исходный список вопросов, который у нас был.
1) нам нужно настроить взаимосвязь с DWH посредством интеграции по API - внутри нашего продукта создадим клиент / сервис для связи по API с DWH, нюансы реализации расписывать тут не будем, потому что всё же это выдуманная компания и выдуманный продукт, все API являются разными, предусмотреть все возможные варианты невозможно - это нужно решать по месту, исходя из конкретики.
2) нам нужно уметь обрабатывать различные отчеты в различном формате - мы это реализовали за счёт ReconciliationInterface
3) нам нужно уметь запускать сверки в разное время в течение дня - для этого у нас есть ReconciliationStarter и ReconciliationCalendar
4) расчетные нагрузки на нашу систему на одну сверку от 1 000 до 500 000 операций - стоит провести нагрузочное тестирование, но в целом такие цифры не выглядят проблемой в текущей архитектуре
5) сверок может быть до 40 в день по текущим данным - точно так же, как и в прошлом пункте - стоит провести нагрузочное тестирование, но в целом цифры не выглядят проблемой
6) некоторые управляющие компании не присылают отчеты по выходным и праздничным дням - для этого у нас существует ReconciliationCalendar, чтобы можно было из админки управлять днями запуска сверок и либо принудительно включать их, либо принудительно выключать их в определенные дни
7) у ряда управляющих компаний есть специальные финансовые операции, которые должны обрабатываться скопом с другими операциями из одной транзакции - для этого мы предусмотрели CustomReconciliationInterface
8) отчеты предоставляются посуточно с данными за предыдущий день - это учтено в архитектуре, на базе этой информации и строилась вся архитектура продукта
9) данные в отчетах бьются не ровно посуточно, а приблизительно посуточно, то есть не получится делать выборки ровно с 00:00:00 до 23:59:59 - это учтено в сверке по интервалу даты и времени, который расчитывается в процессе самой сверки, а не "прибит гвоздями" где-то в конфигах
10) у управляющих компаний, которые не присылают отчеты в выходные и праздничные дни, понедельничные отчеты содержат данные за несколько дней - так же, как и в предыдущем пункте - этот вопрос закрывается интервальной сверкой, интервал для которой формируется прямо в процессе самой сверки
11) часто управляющие компании не присылают отчеты вовремя, могут присылать с задержкой - именно по этой причине мы предусмотрели стадии и уже в процессе реализации сделаем так, чтобы стадии имели возможность перезапуска ( об этом говорилось ранее ), вариантов реализации тут довольно много и это уже можно оставить на усмотрение разработчика
12) покрытие автотестами должно быть 98%+ - так, как у нас в технологическом стеке есть Symfony, а у нее есть в придачу отличный фреймворк для автотестов Codeception с очень детальной документацией - то будем использовать его. Также у Codeception есть инструменты для замеров процента покрытия с подсветкой не закрытого автотестами кода, что тоже очень удобно.
13) должна быть система мониторинга - в качестве инструментов мониторинга можно использовать Grafana или любой аналогичный инструмент принятый внутри компании. Обычно этот вопрос можно уточнить у группы эксплуатации / админов.
14) должна быть система логирования - как и в любом фреймворке, в Symfony есть логгер, который можно использовать. Соответственно при реализации необходимо будет понять ключевые, важные места, где нам необходимо логироваться и добавить туда логирование. Для просмотра логов опять же есть принятые в компании инструменты например такие как Kibana или ее аналоги.
Итак, мы с вами реализовали с нуля архитектуру нового приложения. Прошли путь от идеи до готового концепта, который можно уже описать словами в документе, составить ТЗ, нарезать задачки и передать в разработку.
Ко всему прочему хочу дать несколько рекомендаций:
1) никогда не гнушайтесь спросить мнение коллеги, потому что он может видеть то, чего не видите вы
2) по итогам своей работы вы можете провести презентацию саппортам и коллегам - могут всплыть вопросы, которые вы не задали себе сами во время проектирования
3) после составления схемы - обязательно нужно еще больше детализировать картинку, но уже конкретными словами, расписав как будет работать каждый класс в деталях, как будет работать каждый метод, какие будут свойства у классов и т.д. Так вы можете увидеть дополнительные нюансы и за одно сформировать готовое ТЗ
4) согласуйте список используемого технологического стека с it-sec, если он есть, потому что мало ли вдруг кто-то будет против
5) всегда при сравнении технологий и выборе какого-то инструмента рассматривайте минимум 3 разных подхода / инструмента максимально объективно, потому что вы можете выбрать инструмент, с которым много раз работали по привычке, и это может быть ошибочным выбором, потому что в данной конкретной ситуации он может быть не оптимальным выбором
6) старайтесь не проецировать удачный опыт предыдущих проектов на текущий, потому что он может быть не применим в данной конкретной ситуации. Его можно рассмотреть, но не стоит его приоритезировать из-за того, что он был успешен в другом продукте.
В этой статье я постарался максимально показать пример мышления в процесс проектирования продукта, а так же рассказать с чего начать и с какой стороны к этому вопросу подойти, дабы избежать проблемы "белого листа".
На этом всё. Надеюсь статья будет полезной для тех, кто мало занимался проектированием или не занимался им вовсе. )))
Ура! Я наконец-то дописал статью как собирать собственные бандлы на Symfony 6!!!
Статья про EasyAdmin всё ещё в процессе )))
Не, ну мне же надо на чем-то тестировать твиттер локальный...
Я тут еще много полезного буду выкладывать, так что заходите обязательно почитать.
Сайтик пока что в разработке - это далеко не окончательная версия - по сути это то что удалось слепить за 8 часов.
Комментарии
Tem
2025-01-27 17:19:55