Как стать автором
Обновить

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

Тестирование IT-систем *Программирование *.NET *Проектирование и рефакторинг *C# *

image


Это продолжение текста про архитектуры интерпрайз-систем. Рассуждения это хорошо, но какой в них толк без практического применения. Я покажу свой фреймворк в деле.


Всё началось с того, что я рассказывал про проблематику проектирования приложений на .NET и ныл про нелёгкую жизнь в кровавом интерпрайзе. Затем я описал решение, которое сам придумал и реализовал — Reinforced.Tecture. То была теория, концептуальные рассуждения, визионёрство и снова нытьё. На этот раз о том, что на дворе 2020 год, а HKT в C# так и не завезли.


Сегодня я продемонстрирую свой подход в действии на примере простенького проекта и покажу профиты, которые он даёт: от сокращения количества кода до автоматизации тестирования и оригинального подхода к документации. Как советовал старина Торвальдс: "Болтовня ничего не стоит, покажите мне код".


Итак, надо сделать на Tecture что-нибудь простое, но работающее. Раз мы говорим про интерпрайз — я подберу пример, отдалённо напоминающий настоящий бизнес.


Нам понадобится:


  • Простая сущность. В голову сразу приходят продукты и заказы. Пусть будут продукты;
  • EF-ный DbContext и локальная база данных;
  • Игрушечная бизнес-логика;
  • Простенький web-проект. Всё чин по чину, ASP.NET Core, WebAPI. В него логику и воткнём.

Подготовка


Структура проекта будет такая:


image


Я подключил EF.Core к сборке Data, закинул туда DbContext и glue-код для миграций. Потому что хочу оставить логику на .NET Standard и не тащить EF с собой.


Кстати, интересно

Обычно сущности кладутся в сборку с DAL-ом, рядом с контекстом. Здесь же зависимость развёрнута в обратную сторону — сборка с контекстом знает о всей логике. Это кажется контринтуитивным, но на самом деле вполне нормально для Tecture. Не стоит этому удивляться.


Поведение Tecture мы будем смотреть на примере работы с продуктами. Вот его сущность, а логика вокруг неё будет простая и очень глупая:


image


Код DbContext-а абсолютно шаблонный, спрячу-ка я его под спойлер. В рамках тестового проекта я кладу болт на управление строкой подключения и тонкую настройку контекста — это сейчас не важно. EF я использую как рантайм, не более. Он не оказывает влияния на логику и вообще его поддержка подтягивается из отдельного пакета. EF для меня — удобный инструмент реализации ORM-аспекта. Но моя логика при этом остаётся чистой и свободной от сопутствующих EF-зависимостей.


AcmeDbContext

image


Между делом я сделал базу данных в локальном инстансе MS SQL Express. Это первое что попалось мне под руку — окружение уже было настроено на моей домашней машине. EF.Core более-менее поддерживает и остальные базы — вроде MySQL и PostgreSQL. Однако, на уровне аспекта, где работает Tecture, становится совершенно пофигу. Просто мне так удобнее проводить демонстрацию. Никаких скрытых зависимостей от базы данных тут нет.


Короче, пора втащить в логику первую зависимость. Докинем в неё Reinforced.Tecture и Reinforced.Tecture.Aspects.Orm.


image


Я рассказал про каналы в предыдущей статье и вот они мне пригодились. У меня будет простой канал для базы данных с единственным аспектом, отвечающим за O/RM:


image


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


image


Интеграция


На этом этапе проект уже вполне рабочий. Осталось только присоеденить Tecture к желаемому end-user решению. В нашем случае это web-проект. Я открываю его, иду в Startup.cs, и ищу там метод ConfigureServices. Это внутренний DI-контейнер, который идёт в комплекте с ASP.NET MVC. Для демонстрационных целей он вполне норм, поэтому я заталкиваю в него AcmeDbContext:


image


Теперь самое время поставить рантайм Tecture для взаимодействия с базой через EF. Прямо в web-проект. В моём рантайме реализованы 2 аспекта: O/RM и DirectSQL. DirectSQL я не буду здесь демонстрировать, но он есть. Как я уже сказал, в сборке с бизнес-логикой рантайм не нужен. Он должен подключаться только в ту часть системы, где бизнес-логика непосредственно вызывается, что сильно облегчает dll-ку с логикой на предмет зависимостей. Сам же рантайм вполне может реализовывать несколько аспектов за раз. Это не запрещено и — опять же — экономит зависимости:


image


Теперь надо засунуть Tecture в контейнер. Это делается через фэктори метод. Для наглядности я вынес его отдельно и снабдил комментариями. Тут я просто извлекаю из контейнера AcmeDbContext, заворачиваем его в LazyDisposable (это гибрид Lazy и Disposable, как следует из названия) и скармливаем его рантайму. Далее, для каждого канала мы указываем что вот именно этот контекст EF и надо использовать. Делается это с помощью входящих в комплект поставки рантайма fluent-методов:


image


По задумке такой интеграционный код пишется только один раз и не меняется примерно никогда. Принцип "настроил и забыл" в действии. Для сложных многоканальных систем, конечно, можно нагородить всякие обёртки над построителем Tecture, чтобы настраивать его на работу с разными комбинациями внешних систем, но в нашем приложении такое пока не нужно. Тут мы видим separation of concerns: пусть лучше действительно сложный и чувствительный кусок системы будет краток, прост, читаем и — главное — написан один раз. Далее, на ваше усмотрение — можете вынести его в отдельный репозиторий, единожды собрать и просто использовать.


Вообще я стараюсь возложить максимум ответственности на рантаймы и аспекты. По сути я выношу в них всю реальную проектировочную работу, которую необходимо сделать в приложении. По задумке, обслуживать это дело должен системный архитектор. Я полагаю, в любой компании найдётся бородатый разработчик, который сможет один раз в этом разобраться, настроить и больше никогда не трогать. Но если такого нет — можно воспользоваться моими аспектами и моим рантаймом. Таким образом, пресловутые separation of concerns я поднимаю на организационный уровень.


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


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


Как бы то ни было, теперь можно использовать ITecture из контроллеров. Давайте поиграем с ним.


Запросы


Я хочу написать совершенно тупой и очевидный веб-метод, который будет отдавать нам продукт по Id. Держу пари, у всех такой есть. И у всех на такой случай есть DTOшка. Вот, например, моя:


image


Теперь мы идём в контроллер, прокидываем ITecture в конструктор, выдёргиваем его в отдельное поле, через которое получаем долгожданный доступ к методу From<>. Используем его для того, чтобы вытянуть продукт по Id и смапить на DTO-шку:


image


И на этом, в общем, всё. Таким способом можно писать все методы для возврата всех сущностей по Id с DTO-шками без единого репозитория. Можно дать волю фантазии и не стесняться использовать компилятор C# на полную. Скажем, возврат множества DTO-шек можно обыграть таким способом:


image


image


Или таким:


image


image


Можно даже фигачить расширения непосредственно для IQueryable, дёргая их из читального конца канала через метод All<>, предоставляемый аспектом. Тут действительно можно дать фантазии развернуться. Хочешь — делай развесистый построитель запроса вокруг конца канала, со своими промежуточными абстракциями. Хочешь — строй фасады к AutoMapper, используя его копирующие expression-ы. Всё это в любом случае — статика без контекста. Она легко покрывается тестами при желании, не требует базы данных и сборки контейнера. Можно даже сделать свой аспект и выкинуть туда всё, что касается запросов. Короче, бескрайнее поле для творчества и самореализации. Я же не буду в нём зависать, потому что впереди ещё много интересного.


Операции


К сожалению, продуктов у нас в базе нет и что-то запрашивать из неё бесполезно. Нужен способ заслать туда данные. Как я уже говорил, добавление в Tecture делается через сервисы. Вот я и сделаю сервис для работы с продуктами. За полсекунды, я набросал вот такую заготовку вот в этом месте:


image


Я уже говорил, что в сервисах есть тулинги и они очень удобны. Я хочу остановиться и прям показать как они работают используя анимацию. Конкретно эти тулинги взяты из ORM-аспекта — зацените механику:


image


image


Кстати, нам понадобится Id свежедобавленного продукта. Когда я делал аспект ORM — долго думал как это обыгрыть. Ведь создание сущности — это команда, а получение Id — запрос. Как быть? Я выкрутился: команда Add экстендит интерфейс IAddition<>. После логической операции, саму команды можно смело вернуть из метода логики как IAddition<Product>. А уже после сохранения скормить аддишен методу Key читального конца канала. Это даст нам вожделенный Id. Но надо ещё указать где именно у сущности первичный ключ. Я реализовал эту механику через интерфейс IPrimaryKey<>. Вот:


image


Готово. Теперь можно вернуться в контроллер и наконец-то призвать наш сервис:


image


Postman сказал мне что это работает и вернул Id нового продукта.


Теперь у нас есть маленькое бизнес-приложение, сделанное на Tecture. Мы уже получили чёткую грануляцию проекта, удобные примитивы и минимум кода, но это только начало. Сейчас я перейду к самому интересному — к конкретным преимуществам, которыми меня щедро осыпает такой подход.




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


Я стараюсь быть практичным и всегда считал что архитектура решения сильнее всего влияет на цену поддержки и развития. Ещё в университете на парах по управлению проектами мне показывали статистику запоротых разработок в американской авиаиндустрии. Я чётко помню что в 90% случаев провал определялся ошибками на стадии раннего проектирования и управления требованиями. Это наталкивает на мысль, что если сначала думать, а потом делать — то можно не только предотвратить наступание на грабли, но и драматически снизить стоимость поддержки.


Чем ниже цена, которую платит бизнес за поддержку софта — тем круче. Чем меньше накладные расходы — тем больше денег остаётся и можно потратить их на развитие. А ещё лучше — положить мне в карман.


Я убеждён что только очень меркантильный человек мог придумать следующие фичи.


Трейсы


Вернёмся к коду контроллера. В Tecture есть неприметные методы BeginTrace и EndTrace. Я окружаю ими содержимое экшона по периметру. Вот так:


image


Помимо них тут есть вызов Explain. Так я прошу Tecture объяснить мне что происходит между началом и концом трассировки. Втыкаю точку останова на return и запускаю:


image


Бам! Человеческим языком мне рассказали что конкретно произошло в логике. Само собой, тут и логика-то из трёх с половиной действий. Но когда проект обрастёт сервисами, тонкой нюансировкой, зависимостями и прочими сложностями, которые порождает бизнес — трейс всё так же будет объяснять что прочитано и записано в базы, очереди, кэши, файлы. И если мы не будем лениться, а приложим совсем немного усилий добавляя командам и запросам аннотации, то текст трейса станет ещё более осмысленным:


image


Запросы аннотируются методом .Describe.


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


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


Tecture предлагает пойти другим путём: прибить описание происходящего к командам и запросам. Дать маленькое пояснение к каждому конкретному действию. Это сверх-дёшево, занимает считанные секунды и устойчиво к перестановке кусков кода с места на место. Потом фреймворк сам сложит эти описания вместе и красиво, последовательно и предельно понятно расскажет что же всё-таки случилось, когда вы нажали ту мелкую кнопку в углу экрана. К тому же, это композабельно. То есть если один метод документирован через аннотации, то его вызов будет выдавать эту документацию где бы вы его ни использовали, позволяя оценить происходящее в динамике и поделиться этим знанием с товарищами. Knowledge management!


Ещё есть интерфейс IDescriptive, который можно реализовывать, скажем, у сущностей. Он нужен чтобы вместо "User entity" у вас выводилось "User Vasiliy Pupkin". Это сделает трейс совсем похожим на человеческий текст и если звёзды сложатся, то он будет пригоден для обсуждения с заказчиком. Гораздо удобнее планировать изменения в системе, когда все понимают что она делает.


В прошлой статье я говорил, что по сути бизнес-логика отдаёт команды внешним системам. Explain трейса — это отчёт с авторскими комментариями о том, какие внешние системы были затронуты, что у них запрошено и что им отдано. Это ли не киберпанк, который мы заслужили: приложение, которое само является источником информации о себе независимо от изменений требований.


Но это ещё не всё.


Захват данных


Трейс включает в себя глубокие копии всех ответов на все запросы. Эту информацию можно извлечь в любую структуру данных и я распоряжусь ей элегантно. Докинем в web-проект ещё один пакет: Reinforced.Tecture.Testing. Он тяжеловат — по зависимостям тянет за собой Roslyn. Не надо так делать в живых приложениях, но я это сделаю исключительно в демонстрационных целях. И вот для чего:


image


Этот пакет добавил трейсу 2 эктеншона. GenerateData и GenerateValidation. Нас интересует первый метод, который проще всего вызвать:


image


Смотрите: все ответы на запросы, которые произошли в этом куске логики сконвертились в C#-класс. Я просто нажал пару кнопок, а Tecture уже подготовил мне fake-данные для тестирования. Мне не надо вбивать их руками, не надо подбирать, ставить, и настраивать фейк-генератор, не надо использовать сервисы в духе Mockaroo. У меня уже есть девелоперская база с какими-то данными — я на ней проект дебажу. Для тестов мне придётся эти данные хардкодить, так почему бы не автоматизировать этот процесс?


Но это полдела. Есть ещё один метод из Reinforced.Tecture.Testing. Его вызвать гораздо сложнее, на целых 4 строчки:


image


На пальцах: есть бизнес-логика, она генерирует цепочку каких-то команд при определённых вводных (данных из внешних систем + пользовательский ввод). Данные из внешних систем мы уже дампнули в файл с кодом. Значит и команды тоже можем!


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


А всё для того, чтобы...


Unit-тесты


Положим, по нашему куску логики достигнут консенсус относительно корректности. QA и заказчик в один голос кричат: "во, оставь, сейчас работает как надо!". После чего мы берём этот кусок логики, запускаем в тестовом окружении, выдираем данные и валидацию, а потом запечатываем это добро в регрессионный unit-тест.


Настройка CI/CD пайплайна под подобные тесты — дело пары минут. Не надо понимать окружение, базы, кэши, очереди. Не надо ждать пока они запустятся и прогреются. И подчищать их после запуска тоже не надо. Тесты, построенные на захвате данных Tecture и его валидации самодостаточны и запускаются прямо из коробки без всего. И с блеском решаю задачу контроля регрессии: если кто-то меняет логику, эти тесты послушно падают. При том падают тоже композабельно — по всей цепочке связанной функциональности. Цепью красных шариков можно проследить какие части системы похерены.


Вдовесок тесты расскажут что именно пошло не так — запрос изменился, появились лишние действия в логике, какие-то действия исчезли и так далее. Так или иначе, к куску системы, который внезапно стал работать не так, как договаривались будет привлечено внимание. А если всё в порядке — можно будет со спокойной душой снести старые тестовые данные и сгенерить новые — писать руками ведь ничего не надо, достаточно просто перезапустить.


И моки здесь совершенно не нужны. А значит не надо прятать сервисы за интерфейсами. И городить репозитории для запросов тоже не надо.


Дело за малым — подобрать тестовую инфраструктуру, чтобы удобно обыграть вызовы GenerateData и GenerateValidation. Тут я ничего конкретного не предлагаю и в NuGet не публикую. В тестовом проекте я сделал вот так, просто для примера.


В частности, с этой инфраструктурой можно писать тесты таким вот изящным способом:


image


image


А вот что я делаю, когда логика под тестами меняется:


image


Таким образом, я трачу на написание осмысленных unit-тестов от силы по 5 минут своего рабочего времени. Это чистый, концентрированный профит.


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


Такие вот дела.


Пост-скриптум


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


Сейчас мне на работе выделили небольшой кусок системы, чтобы обкатать Tecture. Не хочу говорить ничего раньше времени, но по результатам будет обзорная статья. Само собой, в ходе живого тестирования я калибрую своё решение под нужды реалий — проектирую новые аспекты и нахожу узкие места, которые нужно подкрутить. В целом пока что этот вариант выглядит вполне себе рабочим.


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


Пакеты опубликованы, исходники есть, я на связи в твиттере, телеграме и на github. Если вам вдруг хочется пополнить ряды early adopters и взять Tecture для своего пет-проекта — напишите мне, я постараюсь помочь.


Отдельная благодарность fillpackart, arttom и их сообществу "Мы обречены" за информационную поддержку и редактуру. Смотрите их подкаст, он крутой. Есть даже выпуск со мной.


Успехов!

Теги:
Хабы:
Всего голосов 32: ↑26 и ↓6 +20
Просмотры 12K
Комментарии Комментарии 26