В зависимости от того, какой рантайм подключается. В настоящий момент меня хватило на то, чтобы сделать один рантайм — для EF.Core. Но можно написать другой, где конструировать запросы будет что-то другое. Я планирую ещё сделать рантйм на EF6, но это будет разработка чисто в демонстрационных целях, что вот, мол, можно из одной точки приложения переключиться между EF6 и EF.Core и результат не изменится. Tecture как раз об этом.
Да, вы совершенно правы. Подход, который я предлагаю — явно вынести все кастомные продукты за абстракции каналов и работать с ними в правильно подобранных аспектах. Это как раз концепция, нацеленная на принятие зоопарка как неизбежности. А аспекты — это принудительная стандартизация.
Что для обычных проектов "ааа! мы подключаем интеграцию ещё с одной системой спасите!", то для проектов на Tecture — "о, архитект завёл новый канал".
Тут, кмк, надо разделять возникающие ошибки не две категории: логическая (ваш код не маякнул SMTP-серверу отправить письмо) и инфраструктурная (у SMTP-сервера кончились почтовые голуби и он ваше письмо не отправил).
В Tecture я позволяю юниттестить логические ошибки: получать тест-прувен гарантии что бизнес-логика точно затриггерила отправку емейла. Остальное — инфраструктурные ошибки. И чинятся они не в коде (а например в конфиге SMTP-сервера).
Я ставил себе целью как раз отделить логическое от инфраструктурного. И получилось.
Я сам сталкивался с фактом, когда твоё действительно дельное предложение тонет в пяти других, суть которых сводится к "да пох ваще, сделаем как можно тупее". А потом наступает фаза "ну я же говорил".
В Tecture тестируется тот факт, что вы отправили команду для отправки email-а. А как на это отреагировал email-сервер — не тестируется (ну или может захватиться один раз, при изначальном прогоне с живыми системами).
То есть вы послали команду, проверили что она реально работает — и забетонировали факт отправки команды именно с такими параметрами в тест. Остальное — не вашей бизнес-логики дело.
Design by comitee — это само по себе хреново. Но чтобы предлагать изменения архитектуры — надо обладать квалификацией. И если квалификация команды ниже чем тимлида/CTO, но при этом все активно предлагают и "каждый должен быть услышан" — то с проектом случается некоторого рода эта… жопа.
Угу. В Tecture прелесть в том, что BulkCopy можно легализовать в рамках отдельно взятого проекта и покрыть тестами все места с ним. Ну и моя разработка не отменяет linq2db ни коим образом. Можно его втащить спокойно. Займусь этим. Когда-нибудь.
Спасибо, коллега, за фидбек. Я даже как-то не ожидал.
Я с вами и не спорю: архитектура должна быть разная под каждое приложение. Иначе бы мамкины стартаперы не выпендривались и писали свои поделки, используя готовые CRM и CMS.
Просто по какой-то причине я регулярно вижу как архитектуры систем сравнительно простых сущностно (положить-достать из базы, отправить почту, сгенерить PDF), но больших линейно (много сущностей, много функциональности) сваливаются в ужасный и отвратительный бардак, который мне приходится в той или иной мере разгребать одинаковыми средствами в каждом проекте. Я просто устал от этой фигни, понимаете? И хочу в отпуск, а не писать в очередной раз очевидные строчки кода.
Кстати, бардак этот почему-то получается похожий даже на международном уровне. У эстонцев, у американцев, у европейцев, у белорусов и даже у евреев одно и то же, одно и то же.
А ещё вы подсветили важную фигню, которую надо учитывать при проектировании систем, которые могут из маленького хеллоуворлда внезапно бомбануть в кастом-билт ERP: управление сложностью. И это не сводится к тупому "делать меньше классов". Это так же затрагивает управление информационной сложностью проекта — требования, грануляция, время сборки и тестирования.
Вот к этой байде я и пытаюсь адресоваться в своём фреймворке в частности. Типа а как нам писать проект 15 лет и не сойти с ума? Нужны какие-то императивы, хорошо если формальные. Ну хотя бы оговоренные устно (конвеншоны). Я над этим и пытаюсь работать, но сами понимаете — в двух словах о таком не напишешь. А даже если напишешь, то читать никто не будет.
Захватывать мир со своей архитектурой я не собираюсь. Первую привычку, которую отбивают 10 лет опыта за плечами — веру в серебряные пули. Я ставлю целью сделать удобный инструмент для себя лично, чтобы жопа не полыхала от каждого проекта. Ну и люблю C# и его систему типов до кровавых соплей, поэтому хочу показать как с помощью его фич можно городить что-то интереснее фасадов-бриджей-репозиториев. Разумеется это не "инструмент-который-всех-нас-спасёт", но если хотя бы пяток систем он убережёт от типичной индустриальной безблагодатности — я буду считать себя победителем.
Теперь по техническим моментам:
Про IoC
В IoC-ах все наступают на проблему грануляции, про которую вы пишете как "ну просто не делать слишком маленькие сервисы и слишком большие сервисы". Вот то, о чём я говорю — нет никакого понимания. Маленький — это сколько? И чего? Методов, сущностей, операций там… я не знаю. В итоге никто ничего не понимает и система сама скатывается в снежный ком. Предлагаешь архитектурные услуги — слышишь "ой нам не нужен архитектор, у нас ажайл". Тьфу. В Tecture я пытаюсь ФОРМАЛЬНО (на компайл-тайм проверках) работать с этим аспектом сложности. Хорошо ли, плохо ли — вот расскажу как именно это происходит в следующей статье — там и подискутируем с превеликим удовольствием. А пока что — ну то понятно, что если грамотно использовать IoC, то с ним нет проблем. Но кто его умеет грамотно использовать? Вы, я, ещё десяток-другой человек? А надо чтобы масштабировалось, понимаете...
зачем прямой доступ к БД
Нужен. И дело не только в батч апдейтах/инсертах. Я как-то даже на митапе дотнетчиков в Нске рассказывал что ORM по сути абстрагирует базу данных, простите, списком. Ну не убожество ли? А где удаление через CTE, а где денормализация, а где индекс-хэлперы? А ORM выставляет наружу интерфейс листа. Та ну тьфу! Короче, нужен SQL в базу, поэтому в Tecture он легализован соответствующей фичей, делается просто, безопасно и тестируется так же. Да да, я люблю подход "не можешь победить — возглавь".
А что не так с тестами на БД?
Долго и дорого. Очень рад если в вашей компании могут позволить поднимать виртуалку на тесты и ждать сутки пока они прогонятся. Я работал в тех местах, где это людям просто не по карману, а формальной валидации продакшен-левела хочется. В Tecture проблема решена через перехват ответов на запросы от внешних систем, что с лихвой хватает для кучи разных систем. Если у вас свой кейс, который решает тестирование через БД и это легально — я могу вам только позавидовать. Я хочу сделать оптимальнее на тех кейсах, что я видел. Вроде даже получается.
Разные проекты были. От 10 до 500 моделей. Где-то на 150 UoW с репозиториями уже начинает загибаться. Я пытаюсь построить архитектуру, которая не будет ломаться на больших проектах с огромными доменами. Странно что я явно не упомянул это в статье.
Ну тут уже вам решать — или делать отдельный канал или расширять существующий, или запилить целый свой аспект. Вариантов море, у вас в руках вся мощь системы типов C#, я уверен при желании вы сможете обыграть это как-нибудь изящно.
Окей, понял, мы видимо плохо начали. Вот развёрнутый ответ:
3. Я ориентируюсь на два критерия: количество фич в условную единицу времени — в неделю там, в месяц или сколько там длится спринт… И на стабильность/верифицируемость получающегося решения в долгосрочной перспективе — предотвращение багов и сокращение регрессии.
Повышение количества фич достигается базово путём сокращения количества кода, необходимого для реализации требуемой функциональности и раннего устранения багов посредством некоторого числа compile-time проверок благодаря активному использованию системы типов C#. Конкретно же это достигается путём упрощения структуры приложения для разработчика, сведения к минимуму количества нерелевантных абстракций при сохранении необходимого уровня контроля, гибкости и создания удобных точек расширения во всех местах, в которых расширение может потребоваться. Таким образом, задача создания инфраструктурного кода с разработчика снимается настолько, насколько это возможно, позволяя сконцентрироваться непосредственно на бизнес-логике.
Простота структуры обуславливается разделением всего приложения на команды и запросы: эту концепцию я позаимствовал из CQRS, только адаптировал под внутреннее использование в приложении. Команды складываются в очередь исполнения, которая начинает раскручиваться при вызове `Save`, а запросы производятся непосредственно. За счёт правильного абстрагирования тулинга запросов у источников данных — отпадает необходимость строить леса абстракций для запросов (e.g. репозитории) в пользу написания оных в форм-факторе метода расширения для источника данных (своеобразная условно-чистая функция). Методы запросов могут быть generic (даже с констрейнтами): по задумке они должны цепляться к «запросному» концу канала благодаря — опять же — системе типов C#. Вот так я убираю примерно половину абстракций и IoC-записей у системы — путём вынесения запросной функциональности в бесконтекстное пространство. Идея диктуется самим LINQ в C# (методы .Where, .Any и т.п.), которую я немного модифицировал, обыграв абстракцию для запроса (хорошо наблюдается на примере ORM-фичи).
При том Tecture не пытается инкапсулировать в себе весь окружающий мир, реализовать свой собственный ORM, или там очередь сообщений, whatever. Он — просто удобная абстрактная прокладка между бизнес-логикой и внешним миром, которая ни коим образом не запрещает вам использовать фичи любого существующего фреймворка, если вдруг у вас возникнет такая необходимость. Точек расширения оставлено достаточно, чтобы вы без разрыва головы могли сами впаять поддержку того, что считаете нужным. Внутри Tecture этот оборот называется Feature, но я подумываю переименовать его в «Aspect», потому как звучит понятнее. O/CP во всей красе, как его вижу я.
Вот я сделал поддержку ORM и реализацию к ней на EF.Core. Не стану скрывать что основные абстракции и внешний дизайн EF.Core мне импонируют, поэтому при разработке ORM-фичи я смотрел именно на него, плюс немного на Dapper и маленький ORM от ServiceStack. SqlStroke — моя авторская задумка, так же хорошо зарекомендовавшая себя в ряде проектов. Этим не ограничивается, можно запилить что-нибудь ещё — очереди, емейлы, файлы… На любой вкус и цвет.
Повышение стабильности решения достигается посредством генерации data-driven регрессионных unit-тестов с автоматическим захватом тестовых данных. Фактически, стоимость создания и обслуживания подобных тестов при использовании фрейморка — нулевая. По задумке это должно побудить разработчиков писать больше meaningful-юнит тестов, которые прекрасно будут встраиваться в code coverege и live coverage-тулы вроде NCrunch, что позволяет в удобной форме сохранять метаинформацию о работоспособности бизнес-логики на конкретных кейсах, а портянки тестов при необходимости можно перегенерировать.
О конкретных цифрах говорить пока рано: некий прототип этого решения уже использовался на живом проекте для одного заказчика и показал себя весьма эффективно. Из нашего кода исчезло 90% репозиториев, но появились длинные бесконтекстные extension-методы, с которыми, тем не менее, было достаточно легко работать просто потому что они располагались целиком в отдельном файле и были названы правильно. Это всё, что я могу сказать на эту тему на текущий момент. Именно с этой целью в репозитории есть Playground-проект, дабы все желающие могли попробовать фреймворк «в деле». Это общая песочница, в которой нет никаких ограничений на чистоту кода и корректность используемых подходов, ибо как целью ставится — поставить фреймворк в неудобное положение. Сейчас на этот проект по мере наличия свободного времени и желания смотрят мои коллеги — я добавил их как контрибьюторов, но в виду добровольности всего происходящего время выделяется по остаточному принципу, поэтому быстрых результатов ждать не стоит. Глупо на этой страдии требовать полного отчёта с цифрами, сравнения с другими фреймворками, детальной документации и упаси б-же записей выступления с конференций, например — просто потому что бюджета на проект нет никакого, разрабатывается он моими силами.
Вот чтобы осветить эти подробности я и хотел сделать следующую статью, иначе размеры комментариев становятся совершенно неприличными.
4. Нет нет и ещё раз нет. К web-у проект не имеет СОВЕРШЕННО никакого отношения. Ни к web, ни к микросервисам, ни к чему. Он может использоваться и в web-приложениях, и в консольных, и в десктопных (хотя в основном задумывался для backend-части приложений), но в дизайне решения не предусмотрено эксплицитно никаких специальных для этого инструментов. Зависимостей у ядра проекта — 0. Он не требует от вас установки yarn, node, использования ASP.NET MVC и вообще ничего за собой не тянет, потому как не пытается быть комбайном-для-всего, коим является, скажем, Spring. Целью ставилось создать минимально-необходимый набор абстракций, от которого будет удобно плясать и попытаться поплясать в каком-то конкретном направлении — например в направлении работы с БД и оценить возможность использования разработки, скажем, в web-е.
Playground-проект, на который вы смотрите — это попытка понять насколько дорого выходит прикрутить Tecture к шаблонному ASP.NET MVC-приложению. Насколько удобно использовать его оттуда, составить общее понимание о том, как меняется внешний вид кода и его логика, да и просто чтобы попробовать сделать что-либо законченное-энд-юзерское а не ковыряться в абстракциях до посинения. Playground-проект, само собой не является частью поставки решения и создан в исследовательских и калибровочных целях. Сторонний же наблюдатель может пощупать его руками, дабы оценить на практике преимущества и недостатки. У меня стали просить ссылку на «пощупать» — я её и дал. Однако сейчас я понимаю что без контекста она не то чтобы сильно полезна.
Само собой! Однако же это не повод не делать просто SELECT EXISTS... в коде. Всяко информативнее и быстрее, нежели ждать пока прилетит Exception от базы. А база подстрахует констрейнтом в крайнем случае.
Второй вариант состоит в том, чтобы добавить к Db возможности редиса и переопределить (где угодно) Get и ByIdRequired как Get<T>(this Db channel, ...) и уже внутри него различать что — в базу, что — в редис.
Можно сделать IStoredInRedis и сделать Get<T>(this Read<Db> channel,...) where T : IStoredInRedis, где задействовать Redis. Аналогично завести IStoredInDb и переформулировать Get<T> в том числе и для where T : IStoredInRedis, IStoredInDb. Это так, первое что в голову пришло.
Запись осуществляется через команды. Аналогичная система и с Write<Db> — его возвращает From<Db>. Можно порешать на уровне экстеншонов, можно порешать на уровне аспекта (у меня аспекты называются "фичи"). Но если вы умеете проектировать и реализовывать аспекты для Tecture, то вы вообще всё что угодно можете сделать вплоть до DSL-я пресонально для своей бизнес-логики (другой вопрос стоит ли оно того).
В остальном — типы. Очень много на типах и очень много работы переложено на компилятор.
И да, я на всякий случай явно говорю: если вам хочется, то вы можете повторить repository pattern на Tecture. Но вот Tecture на repository pattern-ах повторить уже невозможно.
И на моей практике надо было делать даже не полную миграцию с Монги на Постгрес а частичную — часть мы пишем в Постгрес, а часть в Редис.
А здесь это просто не нужно. Повторяю: ByIdRequired — это статический extension-метод. Он посасывается в требуемым инстансам по типу, пользуясь соответствующей языковой фичей C#.
Если часть приложения нужна на постгре, другая — на редисе, то вы просто добавляете ещё один канал для редиса, настраиваете ему необходимые плюшки и один раз реализуете Get<> и ByIdRequired для него одним Extension-методом, подпихивая туда свою абстракцию — и вуаля, у вас уже часть приложения на редисе, часть на постгре. Никаких проблем здесь нет, все изменения займут, я полагаю, строчек 20 кода.
Тем самым вы приколачиваете свои сервис классы к конкретной имплементации сторадж для конкретного ентити
Нет, сторадж — Generic-класс.
как только у вас возникнет нужда один (или все) перевести с <Db> на <Mongo>
Ладно, мне не сложно рассказать.
Db — это канал. Если вы посмотрите что это — то увидите что канал — это интерфейс без реализаций. Он экстендит generic-интерфейс CommandQueryChannel<,>, который определяет используемые при работе с базой примочки (мне сегодня сказали, что слово "аспект" поставит многое на свои места в этом объяснении).
В Playground-проекте определён пока единственный канал — канал работы с базой данных. В подключённых примочках у него — ORM и SqlStroke. Это значит что при вызове From<Db> в списке доступных методов у вас окажутся ORM-овские Add/Update/Delete и SqlStroke (они подсасываются к промежуточному ковариантному интерфейсу как extension-ы). Так же список доступной функциональности канала регулируется типа-параметрами сервиса, где по задумке вы должны эксплицитно указать изменения каких сущностей производит сервис, в котором расположен данный метод: уберите оттуда MeasurementUnit — и код перестанет собираться.
Я так понимаю, что ваш вопрос состоял в том, что случится при переходе к Mongodb. Отвечаю: скорее всего у вас поменяется набор плюшек у канала, потому что Mongo не является реляционной БД и работать с ней надо другими средствами. Однако же, если вам удастся придумать удачную абстракцию, за которой вы спрячете и объектную и реляционную БД, то мои поздравления (у меня не получилось) — вам останется просто переключить рантайм с EF на Mongo.
К слову — Db это просто название канала. Оно может быть любым, хоть Barrel. Поведение канала определяется только набором поддерживаемых фич, что технически выражается в эстенде CommandQueryChannel<,> с разными тип-параметрами. Так же есть возможность заэкстендить только CommandChannel<> или только QueryChannel<>, оставив от аспекта только возможность записи или чтения соответственно.
Если вы видите принципиальную разницу, то я вам его с радостью сделаю. Сути происходящего это не изменит.
как сделать оптимистик локинг?
Рантайм для этого кода — EF.Core. Оптимистик локинг — стандартная фича EF, настраивается в DbContext-е с указанием ConcurrencyToken для сущности, что полностью абстрагировано от бизнес-логики и код Tecture не может и не должен влиять на это обстоятельство. Я полагаю, документация по EF расскажет об этом лучше и подробнее чем я.
почему вы считаете что это From<Db>().Get<MeasurementUnit>().ByIdRequired(id) горазда лучше и проще чем measurementUnitRepository.getById(id)
Потому что в первом варианте используется generic extension метод, а не метод инстанса. ByIdRequired определён один раз для всего приложения и всех сущностей. Для того, чтобы сделать такой запрос мне не надо создавать экземпляр measurementUnitRepository и регистрировать его в контейнере — ровно то, о чём я писал в статье когда говорил про длинные портянки IoC. Конечной целью этого приёма является сокращение количества записей в IoC-контейнере и упрощение инстанцирования служебных штук необходимых для запроса в БД. Tecture обеспечивает таким вызовам должные возможности для мокинга на этапе тестирования, а функциональная природа происходящего позволяет нам сократить количество абстракций до одной (собственно, функция).
В зависимости от того, какой рантайм подключается. В настоящий момент меня хватило на то, чтобы сделать один рантайм — для EF.Core. Но можно написать другой, где конструировать запросы будет что-то другое. Я планирую ещё сделать рантйм на EF6, но это будет разработка чисто в демонстрационных целях, что вот, мол, можно из одной точки приложения переключиться между EF6 и EF.Core и результат не изменится. Tecture как раз об этом.
Да, вы совершенно правы. Подход, который я предлагаю — явно вынести все кастомные продукты за абстракции каналов и работать с ними в правильно подобранных аспектах. Это как раз концепция, нацеленная на принятие зоопарка как неизбежности. А аспекты — это принудительная стандартизация.
Что для обычных проектов "ааа! мы подключаем интеграцию ещё с одной системой спасите!", то для проектов на Tecture — "о, архитект завёл новый канал".
Вкачусь в дискуссию.
Тут, кмк, надо разделять возникающие ошибки не две категории: логическая (ваш код не маякнул SMTP-серверу отправить письмо) и инфраструктурная (у SMTP-сервера кончились почтовые голуби и он ваше письмо не отправил).
В Tecture я позволяю юниттестить логические ошибки: получать тест-прувен гарантии что бизнес-логика точно затриггерила отправку емейла. Остальное — инфраструктурные ошибки. И чинятся они не в коде (а например в конфиге SMTP-сервера).
Я ставил себе целью как раз отделить логическое от инфраструктурного. И получилось.
А вы начните с провокационного описания какое именно говно царит в индустрии вокруг :)
Я сам сталкивался с фактом, когда твоё действительно дельное предложение тонет в пяти других, суть которых сводится к "да пох ваще, сделаем как можно тупее". А потом наступает фаза "ну я же говорил".
Бесит. Но я держусь огурцом.
Чего это вы так сразу. Для меня очевиден :)
В Tecture тестируется тот факт, что вы отправили команду для отправки email-а. А как на это отреагировал email-сервер — не тестируется (ну или может захватиться один раз, при изначальном прогоне с живыми системами).
То есть вы послали команду, проверили что она реально работает — и забетонировали факт отправки команды именно с такими параметрами в тест. Остальное — не вашей бизнес-логики дело.
Design by comitee — это само по себе хреново. Но чтобы предлагать изменения архитектуры — надо обладать квалификацией. И если квалификация команды ниже чем тимлида/CTO, но при этом все активно предлагают и "каждый должен быть услышан" — то с проектом случается некоторого рода эта… жопа.
Угу. В Tecture прелесть в том, что BulkCopy можно легализовать в рамках отдельно взятого проекта и покрыть тестами все места с ним. Ну и моя разработка не отменяет linq2db ни коим образом. Можно его втащить спокойно. Займусь этим. Когда-нибудь.
Спасибо, коллега, за фидбек. Я даже как-то не ожидал.
Я с вами и не спорю: архитектура должна быть разная под каждое приложение. Иначе бы мамкины стартаперы не выпендривались и писали свои поделки, используя готовые CRM и CMS.
Просто по какой-то причине я регулярно вижу как архитектуры систем сравнительно простых сущностно (положить-достать из базы, отправить почту, сгенерить PDF), но больших линейно (много сущностей, много функциональности) сваливаются в ужасный и отвратительный бардак, который мне приходится в той или иной мере разгребать одинаковыми средствами в каждом проекте. Я просто устал от этой фигни, понимаете? И хочу в отпуск, а не писать в очередной раз очевидные строчки кода.
Кстати, бардак этот почему-то получается похожий даже на международном уровне. У эстонцев, у американцев, у европейцев, у белорусов и даже у евреев одно и то же, одно и то же.
А ещё вы подсветили важную фигню, которую надо учитывать при проектировании систем, которые могут из маленького хеллоуворлда внезапно бомбануть в кастом-билт ERP: управление сложностью. И это не сводится к тупому "делать меньше классов". Это так же затрагивает управление информационной сложностью проекта — требования, грануляция, время сборки и тестирования.
Вот к этой байде я и пытаюсь адресоваться в своём фреймворке в частности. Типа а как нам писать проект 15 лет и не сойти с ума? Нужны какие-то императивы, хорошо если формальные. Ну хотя бы оговоренные устно (конвеншоны). Я над этим и пытаюсь работать, но сами понимаете — в двух словах о таком не напишешь. А даже если напишешь, то читать никто не будет.
Захватывать мир со своей архитектурой я не собираюсь. Первую привычку, которую отбивают 10 лет опыта за плечами — веру в серебряные пули. Я ставлю целью сделать удобный инструмент для себя лично, чтобы жопа не полыхала от каждого проекта. Ну и люблю C# и его систему типов до кровавых соплей, поэтому хочу показать как с помощью его фич можно городить что-то интереснее фасадов-бриджей-репозиториев. Разумеется это не "инструмент-который-всех-нас-спасёт", но если хотя бы пяток систем он убережёт от типичной индустриальной безблагодатности — я буду считать себя победителем.
Теперь по техническим моментам:
В IoC-ах все наступают на проблему грануляции, про которую вы пишете как "ну просто не делать слишком маленькие сервисы и слишком большие сервисы". Вот то, о чём я говорю — нет никакого понимания. Маленький — это сколько? И чего? Методов, сущностей, операций там… я не знаю. В итоге никто ничего не понимает и система сама скатывается в снежный ком. Предлагаешь архитектурные услуги — слышишь "ой нам не нужен архитектор, у нас ажайл". Тьфу. В Tecture я пытаюсь ФОРМАЛЬНО (на компайл-тайм проверках) работать с этим аспектом сложности. Хорошо ли, плохо ли — вот расскажу как именно это происходит в следующей статье — там и подискутируем с превеликим удовольствием. А пока что — ну то понятно, что если грамотно использовать IoC, то с ним нет проблем. Но кто его умеет грамотно использовать? Вы, я, ещё десяток-другой человек? А надо чтобы масштабировалось, понимаете...
Нужен. И дело не только в батч апдейтах/инсертах. Я как-то даже на митапе дотнетчиков в Нске рассказывал что ORM по сути абстрагирует базу данных, простите, списком. Ну не убожество ли? А где удаление через CTE, а где денормализация, а где индекс-хэлперы? А ORM выставляет наружу интерфейс листа. Та ну тьфу! Короче, нужен SQL в базу, поэтому в Tecture он легализован соответствующей фичей, делается просто, безопасно и тестируется так же. Да да, я люблю подход "не можешь победить — возглавь".
Долго и дорого. Очень рад если в вашей компании могут позволить поднимать виртуалку на тесты и ждать сутки пока они прогонятся. Я работал в тех местах, где это людям просто не по карману, а формальной валидации продакшен-левела хочется. В Tecture проблема решена через перехват ответов на запросы от внешних систем, что с лихвой хватает для кучи разных систем. Если у вас свой кейс, который решает тестирование через БД и это легально — я могу вам только позавидовать. Я хочу сделать оптимальнее на тех кейсах, что я видел. Вроде даже получается.
За сим откланяюсь. Ещё раз спасибо за фидбек.
Я обязательно ознакомлюсь, спасибо за интерес
А потом наступает клиринг...
Разные проекты были. От 10 до 500 моделей. Где-то на 150 UoW с репозиториями уже начинает загибаться. Я пытаюсь построить архитектуру, которая не будет ломаться на больших проектах с огромными доменами. Странно что я явно не упомянул это в статье.
3. Я ориентируюсь на два критерия: количество фич в условную единицу времени — в неделю там, в месяц или сколько там длится спринт… И на стабильность/верифицируемость получающегося решения в долгосрочной перспективе — предотвращение багов и сокращение регрессии.
Повышение количества фич достигается базово путём сокращения количества кода, необходимого для реализации требуемой функциональности и раннего устранения багов посредством некоторого числа compile-time проверок благодаря активному использованию системы типов C#. Конкретно же это достигается путём упрощения структуры приложения для разработчика, сведения к минимуму количества нерелевантных абстракций при сохранении необходимого уровня контроля, гибкости и создания удобных точек расширения во всех местах, в которых расширение может потребоваться. Таким образом, задача создания инфраструктурного кода с разработчика снимается настолько, насколько это возможно, позволяя сконцентрироваться непосредственно на бизнес-логике.
Простота структуры обуславливается разделением всего приложения на команды и запросы: эту концепцию я позаимствовал из CQRS, только адаптировал под внутреннее использование в приложении. Команды складываются в очередь исполнения, которая начинает раскручиваться при вызове `Save`, а запросы производятся непосредственно. За счёт правильного абстрагирования тулинга запросов у источников данных — отпадает необходимость строить леса абстракций для запросов (e.g. репозитории) в пользу написания оных в форм-факторе метода расширения для источника данных (своеобразная условно-чистая функция). Методы запросов могут быть generic (даже с констрейнтами): по задумке они должны цепляться к «запросному» концу канала благодаря — опять же — системе типов C#. Вот так я убираю примерно половину абстракций и IoC-записей у системы — путём вынесения запросной функциональности в бесконтекстное пространство. Идея диктуется самим LINQ в C# (методы .Where, .Any и т.п.), которую я немного модифицировал, обыграв абстракцию для запроса (хорошо наблюдается на примере ORM-фичи).
При том Tecture не пытается инкапсулировать в себе весь окружающий мир, реализовать свой собственный ORM, или там очередь сообщений, whatever. Он — просто удобная абстрактная прокладка между бизнес-логикой и внешним миром, которая ни коим образом не запрещает вам использовать фичи любого существующего фреймворка, если вдруг у вас возникнет такая необходимость. Точек расширения оставлено достаточно, чтобы вы без разрыва головы могли сами впаять поддержку того, что считаете нужным. Внутри Tecture этот оборот называется Feature, но я подумываю переименовать его в «Aspect», потому как звучит понятнее. O/CP во всей красе, как его вижу я.
Вот я сделал поддержку ORM и реализацию к ней на EF.Core. Не стану скрывать что основные абстракции и внешний дизайн EF.Core мне импонируют, поэтому при разработке ORM-фичи я смотрел именно на него, плюс немного на Dapper и маленький ORM от ServiceStack. SqlStroke — моя авторская задумка, так же хорошо зарекомендовавшая себя в ряде проектов. Этим не ограничивается, можно запилить что-нибудь ещё — очереди, емейлы, файлы… На любой вкус и цвет.
Повышение стабильности решения достигается посредством генерации data-driven регрессионных unit-тестов с автоматическим захватом тестовых данных. Фактически, стоимость создания и обслуживания подобных тестов при использовании фрейморка — нулевая. По задумке это должно побудить разработчиков писать больше meaningful-юнит тестов, которые прекрасно будут встраиваться в code coverege и live coverage-тулы вроде NCrunch, что позволяет в удобной форме сохранять метаинформацию о работоспособности бизнес-логики на конкретных кейсах, а портянки тестов при необходимости можно перегенерировать.
О конкретных цифрах говорить пока рано: некий прототип этого решения уже использовался на живом проекте для одного заказчика и показал себя весьма эффективно. Из нашего кода исчезло 90% репозиториев, но появились длинные бесконтекстные extension-методы, с которыми, тем не менее, было достаточно легко работать просто потому что они располагались целиком в отдельном файле и были названы правильно. Это всё, что я могу сказать на эту тему на текущий момент. Именно с этой целью в репозитории есть Playground-проект, дабы все желающие могли попробовать фреймворк «в деле». Это общая песочница, в которой нет никаких ограничений на чистоту кода и корректность используемых подходов, ибо как целью ставится — поставить фреймворк в неудобное положение. Сейчас на этот проект по мере наличия свободного времени и желания смотрят мои коллеги — я добавил их как контрибьюторов, но в виду добровольности всего происходящего время выделяется по остаточному принципу, поэтому быстрых результатов ждать не стоит. Глупо на этой страдии требовать полного отчёта с цифрами, сравнения с другими фреймворками, детальной документации и упаси б-же записей выступления с конференций, например — просто потому что бюджета на проект нет никакого, разрабатывается он моими силами.
Вот чтобы осветить эти подробности я и хотел сделать следующую статью, иначе размеры комментариев становятся совершенно неприличными.
4. Нет нет и ещё раз нет. К web-у проект не имеет СОВЕРШЕННО никакого отношения. Ни к web, ни к микросервисам, ни к чему. Он может использоваться и в web-приложениях, и в консольных, и в десктопных (хотя в основном задумывался для backend-части приложений), но в дизайне решения не предусмотрено эксплицитно никаких специальных для этого инструментов. Зависимостей у ядра проекта — 0. Он не требует от вас установки yarn, node, использования ASP.NET MVC и вообще ничего за собой не тянет, потому как не пытается быть комбайном-для-всего, коим является, скажем, Spring. Целью ставилось создать минимально-необходимый набор абстракций, от которого будет удобно плясать и попытаться поплясать в каком-то конкретном направлении — например в направлении работы с БД и оценить возможность использования разработки, скажем, в web-е.
Playground-проект, на который вы смотрите — это попытка понять насколько дорого выходит прикрутить Tecture к шаблонному ASP.NET MVC-приложению. Насколько удобно использовать его оттуда, составить общее понимание о том, как меняется внешний вид кода и его логика, да и просто чтобы попробовать сделать что-либо законченное-энд-юзерское а не ковыряться в абстракциях до посинения. Playground-проект, само собой не является частью поставки решения и создан в исследовательских и калибровочных целях. Сторонний же наблюдатель может пощупать его руками, дабы оценить на практике преимущества и недостатки. У меня стали просить ссылку на «пощупать» — я её и дал. Однако сейчас я понимаю что без контекста она не то чтобы сильно полезна.
Само собой! Однако же это не повод не делать просто
SELECT EXISTS...
в коде. Всяко информативнее и быстрее, нежели ждать пока прилетит Exception от базы. А база подстрахует констрейнтом в крайнем случае.Верно, это один из вариантов решения.
Второй вариант состоит в том, чтобы добавить к
Db
возможности редиса и переопределить (где угодно)Get
иByIdRequired
какGet<T>(this Db channel, ...)
и уже внутри него различать что — в базу, что — в редис.Можно сделать
IStoredInRedis
и сделатьGet<T>(this Read<Db> channel,...) where T : IStoredInRedis
, где задействовать Redis. Аналогично завестиIStoredInDb
и переформулироватьGet<T>
в том числе и дляwhere T : IStoredInRedis, IStoredInDb
. Это так, первое что в голову пришло.Запись осуществляется через команды. Аналогичная система и с
Write<Db>
— его возвращаетFrom<Db>
. Можно порешать на уровне экстеншонов, можно порешать на уровне аспекта (у меня аспекты называются "фичи"). Но если вы умеете проектировать и реализовывать аспекты для Tecture, то вы вообще всё что угодно можете сделать вплоть до DSL-я пресонально для своей бизнес-логики (другой вопрос стоит ли оно того).В остальном — типы. Очень много на типах и очень много работы переложено на компилятор.
И да, я на всякий случай явно говорю: если вам хочется, то вы можете повторить repository pattern на Tecture. Но вот Tecture на repository pattern-ах повторить уже невозможно.
А здесь это просто не нужно. Повторяю:
ByIdRequired
— это статический extension-метод. Он посасывается в требуемым инстансам по типу, пользуясь соответствующей языковой фичей C#.Если часть приложения нужна на постгре, другая — на редисе, то вы просто добавляете ещё один канал для редиса, настраиваете ему необходимые плюшки и один раз реализуете
Get<>
иByIdRequired
для него одним Extension-методом, подпихивая туда свою абстракцию — и вуаля, у вас уже часть приложения на редисе, часть на постгре. Никаких проблем здесь нет, все изменения займут, я полагаю, строчек 20 кода.Нет, сторадж — Generic-класс.
Ладно, мне не сложно рассказать.
Db
— это канал. Если вы посмотрите что это — то увидите что канал — это интерфейс без реализаций. Он экстендит generic-интерфейсCommandQueryChannel<,>
, который определяет используемые при работе с базой примочки (мне сегодня сказали, что слово "аспект" поставит многое на свои места в этом объяснении).В Playground-проекте определён пока единственный канал — канал работы с базой данных. В подключённых примочках у него — ORM и SqlStroke. Это значит что при вызове
From<Db>
в списке доступных методов у вас окажутся ORM-овские Add/Update/Delete и SqlStroke (они подсасываются к промежуточному ковариантному интерфейсу как extension-ы). Так же список доступной функциональности канала регулируется типа-параметрами сервиса, где по задумке вы должны эксплицитно указать изменения каких сущностей производит сервис, в котором расположен данный метод: уберите оттуда MeasurementUnit — и код перестанет собираться.Я так понимаю, что ваш вопрос состоял в том, что случится при переходе к Mongodb. Отвечаю: скорее всего у вас поменяется набор плюшек у канала, потому что Mongo не является реляционной БД и работать с ней надо другими средствами. Однако же, если вам удастся придумать удачную абстракцию, за которой вы спрячете и объектную и реляционную БД, то мои поздравления (у меня не получилось) — вам останется просто переключить рантайм с EF на Mongo.
К слову —
Db
это просто название канала. Оно может быть любым, хотьBarrel
. Поведение канала определяется только набором поддерживаемых фич, что технически выражается в эстендеCommandQueryChannel<,>
с разными тип-параметрами. Так же есть возможность заэкстендить толькоCommandChannel<>
или толькоQueryChannel<>
, оставив от аспекта только возможность записи или чтения соответственно.Как-то так, если кратко.
Если вы видите принципиальную разницу, то я вам его с радостью сделаю. Сути происходящего это не изменит.
Рантайм для этого кода — EF.Core. Оптимистик локинг — стандартная фича EF, настраивается в DbContext-е с указанием ConcurrencyToken для сущности, что полностью абстрагировано от бизнес-логики и код Tecture не может и не должен влиять на это обстоятельство. Я полагаю, документация по EF расскажет об этом лучше и подробнее чем я.
Потому что в первом варианте используется generic extension метод, а не метод инстанса. ByIdRequired определён один раз для всего приложения и всех сущностей. Для того, чтобы сделать такой запрос мне не надо создавать экземпляр measurementUnitRepository и регистрировать его в контейнере — ровно то, о чём я писал в статье когда говорил про длинные портянки IoC. Конечной целью этого приёма является сокращение количества записей в IoC-контейнере и упрощение инстанцирования служебных штук необходимых для запроса в БД. Tecture обеспечивает таким вызовам должные возможности для мокинга на этапе тестирования, а функциональная природа происходящего позволяет нам сократить количество абстракций до одной (собственно, функция).