Я 20 лет наслаждаюсь разнообразием архитектур и хочу поделиться мыслями
Сначала хотел написать комментарий к статье «Я десять лет страдал от ужасных архитектур в C#…», но понял две вещи:
- Слишком много мыслей, которыми хочется поделиться.
- Для такого объёма формат комментария неудобен ни для написания, ни для прочтения.
- Давно читаю Хабр, иногда комментирую, но ни разу не писал статей.
- Я не силён в нумерованных списках.
Disclaimer: я не критикую @pnovikov или его задумку в целом. Текст качественный (чувствуется опытный редактор), часть мыслей разделяю. Архитектур много, но это нормально (да, звучит как название корейского фильма).
Однако давайте по порядку. Сначала моё мнение о том, что влияет на архитектуру, потом про спорные моменты в статье об «исправлении архитектур». Ещё расскажу о том, что у нас хорошо работает — может, пригодится кому-нибудь.
И, конечно, всё сказанное здесь является личным мнением на основе моего опыта и когнитивных искажений.
О моём мнении
Я часто принимаю архитектурные решения. Когда-то — большие, когда-то — маленькие. Изредка придумываю архитектуру с нуля. Ну, как с нуля — наверняка всё придумано до нас, но про что-то мы не знаем, поэтому приходится придумывать. И не из любви к велосипедостроению (скажем так, не только из любви к нему), а потому что для некоторых задач не нашлось готового решения, которое устроило бы по всем параметрам.
Почему я считаю, что совершенно разные архитектуры имеют право на жизнь? Можно порассуждать о том, что программирование — искусство, а не ремесло, но я не буду. Моё мнение: когда-то искусство, когда-то — ремесло. Речь не об этом. Главное, что задачи разные. И люди. Уточню — под задачами подразумеваются требования бизнеса.
Если когда-то мои задачи станут однотипными, я напишу или попрошу кого-то написать нейросеть (а может, хватит и скрипта), которая меня заменит. А сам займусь чем-то менее безрадостным. Пока же мой и, надеюсь, ваш личный апокалипсис не наступил, давайте подумаем, как влияют задачи и прочие условия на разнообразие архитектур. TL&DR; — разнообразно.
Производительность или масштабируемость
Вот это самая правильная, пожалуй, причина поменять архитектуру. Если, конечно, не проще старую архитектуру адаптировать к новым требованиям. Но тут сложно кратко рассказать что-то полезное.
Сроки
Допустим, сроки (дважды убедился, что написал через «о») сильно сжаты. Тогда нам не до того чтобы выбирать, а, тем более, придумывать архитектуру — бери знакомые инструменты и копай. Но есть нюанс — иногда сложные проекты можно сделать вовремя только применив (и, возможно, придумав) что-то принципиально новое. Кто-то может сказать, что пригласить заказчика в баню — старый прием, но я сейчас про архитектуру…
Когда сроки комфортные — часто получается парадоксальная ситуация — вроде и можно придумать что-то новое, но зачем? Правда, многие успешно поддаются соблазну заняться другим более горящим проектом и сводят ситуацию к предыдущей.
В моей практике сроки редко приводят к революциям в архитектуре, но бывает. И это прекрасно.
Скорость и качество разработки
Бывает так — команда (или кто-то из руководства) замечают, что скорость разработки снизилась или за итерацию багов многовато набежало. Нередко виновной в этом признают «неправильную архитектуру». Иногда — заслуженно. Чаще — просто как самого удобного обвиняемого (особенно, если в коллективе нет её «родителя»).
В принципе, в одних случаях всё сводится к фактору сроков. А в других — к поддерживаемости, о ней далее.
Поддерживаемость
Неоднозначная тема. Потому что всё очень субъективно и зависит много от чего. Например — от команды, языка программирования, процессов в компании, количества адаптаций под разных клиентов. Давайте про последний фактор, мне он кажется самым интересным.
Вот вы сделали заказной проект. Успешно, в сроки и бюджет уложились, заказчик всем доволен. Было и у меня такое. Теперь вы смотрите на то, что использовали и думаете — так вот она — золотая жила! Мы сейчас используем все эти наработки, быстро сделаем один B2B-продукт, и… Сначала всё хорошо. Продукт сделали, пару раз продали. Наняли ещё продавцов и разработчиков («нужно больше золота»). Заказчики довольны, платят за сопровождение, случаются новые продажи…
А потом один из заказчиков говорит человеческим голосом — «мне бы вот эту штуковину совсем по-другому сделать — сколько это может стоить?». Ну, подумаешь — несколько if«чиков с другим кодом воткнуть (допустим, некогда было DI прикрутить), что плохого может случиться?
И в первый раз действительно ничего плохого не случится. Я бы даже не советовал в такой ситуации что-то специальное городить. Преждевременное усложнение архитектуры сродни преждевременной оптимизации. Но когда это случается во второй и третий раз — это повод вспомнить про такие штуки как DI, паттерн «стратегия», Feature Toggle и иже с ними. И, на время, это поможет.
А потом наступает день, когда вы смотрите на настройки проекта (всего-то несколько сотен опций) для конкретного тестового стенда… Вспоминаете, как посчитать число сочетаний и думаете — как же, вашу мать, это можно протестировать? Понятно, что в идеальном мире это просто — ведь каждая фича спроектирована и реализована так, что она никак не влияет на другую, а если влияет, то всё это предусмотрено и вообще наши разработчики никогда не ошибались.
Конечно, я сгустил краски — можно выделить какие-то наборы фич, которые используются у реальных заказчиков, написать больше тестов (как и каких — тема отдельного разговора) и немного упростить задачу. Но вдумайтесь — каждый серьезный релиз нужно протестировать для всех заказчиков. Напоминаю, это не B2C, где можно сказать «выкатим фичу для 5% пользователей и соберём фидбек» — для B2B фидбек можно по судам начать собирать…
Решения? Например, разделить продукт на модули с отдельным жизненным циклом (не забывая тестировать их взаимодействие). Это снизит сложность сопровождения, хотя и усложнит разработку. И я сейчас не о благодатной для холиваров теме «монолит vs. микросервисы» — в монолите тоже можно устроить подобное (хотя и сложнее, на мой взгляд).
И, заметьте, с прагматической точки зрения на каждом этапе у нас была неплохая архитектура.
И к чему всё это?
Я не хочу вас (и себя) утомлять перечислением других причин для изменений в архитектуре. Давайте сейчас согласимся, что архитектуры имеют свойство со временем меняться, в зависимости от многих факторов. А значит: идеальная архитектура, решающая «ну вот все проблемы» не существует.
Если я вас в этом еще не убедил — посмотрите на разнообразие языков программирования и фреймворков (только не во фронтенде — не надо вскрывать эту тему). Если кто-то скажет, что это плохо, предлагаю провести мысленный эксперимент — представьте мир, в котором есть один конкретный язык программирования. С одним важным условием — он вам не нравится. Например, потому что вы никогда его не использовали, да и не собирались этого делать.
И, признаюсь, есть еще один веский довод — придумывать что-то новое, оптимизируя несколько параметров, играя компромиссами — это чертовски увлекательно. А теперь, когда мы все (правда?) согласны, что разнообразие архитектур — это нормально…
Обсуждение статьи про «исправление архитектур»
А что IoC?
Про IoC соглашусь, что портянкам место в армии, а модули — это вселенское добро. Но вот всё остальное…
Если, конечно, послушать некоторых апологетов «чистого кода», то можно накодить гору сервисов, в каждом из которых будет в среднем полтора метода, а в методе — две с половиной строки. Но зачем? Вот честно, вы точно хотите соблюдать принципы, которые помогут справиться с маловероятными проблемами в далеком будущем, но размазывают даже несложную логику по десяткам файлов? Или вам, всё-таки, достаточно сейчас написать прилично работающий код?
К слову сказать, сейчас работаю над модулем, который точно будет использоваться в разных продуктах и, скорее всего, будет активно «тюниться». Так и там стараюсь не «мельчить». Не окупается. Хотя вот в нём использую единственные реализации интерфейсов чаще обычного.
Так вот, если у нас есть модули и мы не «мелочны», то откуда взяться проблемам производительности IoC или неподдерживаемых «портянок IoC-конфигураций»? Я не сталкивался.
Правда, уточню наши условия работы:
- Модули у нас не те, которые «предоставляются почти любым IoC-фреймворком», а вот «прям модули» — которые общаются между собой удалённо через API (иногда можно, из соображений производительности, поселить их в одном процессе, но схема работы не поменяется).
- IoC используется максимально простой и максимально просто — зависимости втыкаются в параметры конструктора.
- Да, у нас сейчас микросервисная архитектура, но и здесь стараемся не мельчить.
Совет: интерфейсы можно держать в том же файле, что и класс — удобно (если, конечно, пользуетесь нормальной IDE, а не блокнотом). Исключения делаю, когда интерфейсы (или комментарии к ним) разрастаются. Но это всё вкусовщина, конечно.
А что не так с ORM и зачем прямой доступ к БД?
Да я и сам скажу, что не так — многие из них слишком далеки от SQL. Но не все. Поэтому, вместо того, чтобы «терпеть, пока O/RM удаляет 3000 объектов» или придумывать ещё один, найдите тот, который вас устроит.
Совет: попробуйте LINQ to DB. Он хорошо сбалансирован, есть методы Update/Delete для нескольких строк. Только осторожно — вызывает привыкание. Да, нет каких-то фич EF и немного другая концепция, но мне понравился намного больше EF.
Кстати, приятно, что это разработка наших соотечественников. Игорю Ткачеву — респект (не нашёл его на Хабре).
А что не так с тестами на БД?
Да они будут медленнее, чем на данных в памяти. Фатально ли это? Да нет, конечно же. Как решать эту проблему? Вот два рецепта, которые лучше применять одновременно.
Рецепт №1. Берёшь крутого разработчика, который любит делать всякие прикольные штуки и обсуждаешь с ним, как красиво решить эту проблему. Мне повезло, потому что force решил проблему быстрее, чем она появилась (даже не помню, обсуждали её или нет). Как? Сделал (за день, вроде) тестовую фабрику для ORM, которая подменяет основное подмножество операций на обращения к массивам.
Для простых юнит-тестов — идеально. Альтернативный вариант — юзать SQLite или что-то подобное вместо «больших» БД.
Комментарий от force: Тут надо сделать пару уточнений. Во-первых, мы стараемся не использовать сырые запросы к базе данных в коде, а максимально используем ORM, если он хороший, то лезть в базу с SQL наголо не требуется. Во-вторых, разница поведения с базой есть, но ведь мы не проверяем вставку в базу, мы проверяем логику, и небольшое различие в поведении тут несущественно, т.к. особо ни на что не влияет. Поддержка корректной тестовой базы гораздо сложнее.
Рецепт №2. Бизнес-сценарии я предпочитаю тестировать на настоящих БД. А если в проекте заявлена возможность поддержки нескольких СУБД, тесты выполняются для нескольких СУБД. Почему? Да всё просто. В утверждении «не хочется тестировать сервер баз данных», увы, происходит подмена понятий. Я, знаете ли, тестирую не то, что join работает или order by.
Я тестирую свой код, работающий с БД. А зная, что даже разные версии одной СУБД могут выдавать разные результаты на разных запросах (пруф), я хочу основные сценарии проверять именно на тех БД, с которыми этот код будет работать.
Обычно подобные тесты у меня выглядят так:
- Для группы тестов (Fixture) с нуля генерируется по метаданным БД. Если нужно — заполняются необходимые справочники.
- Каждый сценарий сам добавляет нужные данные в процессе прохождения (пользователи ведь тоже это делают). В тестах на производительность не так, но это уже совсем другая история…
- После каждого теста лишние данные (кроме справочников) удаляются.
Совет: если такие тесты у вас выполняются объективно долго (а не потому, что пора оптимизировать запросы к базе) — сделайте билд, который будет запускать их реже (категории тестов или отдельный проект в помощь). Иначе разработчики не захотят сами запускать и остальные — быстрые тесты.
Транзакция и email
Просто дополню историю «транзакция в БД по каким-то причинам упала, а e-mail ушёл». А какое веселье будет, когда транзакция подождёт недоступного почтового сервера, поставив колом всю систему из-за какого-нибудь уведомления, которое пользователь потом отправит в корзину, не читая…
Правда, я всегда считал, что письма в транзакции отправляют только джуны в дикой природе в отсутствие ревью. В нашей команде за такое канделябром бьют (пока виртуальным).
Итоги
В целом, если @pnovikov не имеет планов по захвату мира с помощью единственно верной идеологии архитектуры, других, достойных упоминания, расхождений во взглядах не нашёл. Для каких-то задач, безусловно, озвученные им принципы подойдут. С удовольствием прочитаю следующие статьи и комментарии, может, найду какие-то полезные идеи для себя.
Предлагаемый фреймворк вряд ли буду использовать. Причина проста — у нас уже есть идеальная архитектура…
P.S. Если у вас будет желание обсудить что-то в комментариях, буду рад принять в этом участие.