Comments 56
Проблемы продуктов в enterprise чуток шире.
Далеко не эксперт, но вы верно заметили что в любой enterprise (напр. крупные банки, типа JPMC, WellsFargo) внутри хаос из продуктов 3х фирм и внутренних продуктов.
Как следствие, enterprise архитектура продуктов должна решать задачи enterprise уровня.
Желательна концепция архитектуры, принимающая во внимание и зоопарк, как неизбежность, и задачи бизнеса и проблемы реализации (неимоверно затянутые сроки, низкий уровень понимания big picture) и снижение порога вхождения для эффективного добавления продуктов и принудительная стандартизация для исключения нового хаоса и масса других мелочей, для описания которых комментарий неуместен.
Ребята из Jet иногда делятся интересными решениями.
Да, вы совершенно правы. Подход, который я предлагаю — явно вынести все кастомные продукты за абстракции каналов и работать с ними в правильно подобранных аспектах. Это как раз концепция, нацеленная на принятие зоопарка как неизбежности. А аспекты — это принудительная стандартизация.
Что для обычных проектов "ааа! мы подключаем интеграцию ещё с одной системой спасите!", то для проектов на Tecture — "о, архитект завёл новый канал".
Сложность-то лежит как раз в разработке каналов-адаптеров для всего корпоративного зоопарка.
Проблема-то достаточно старая, решение такое лежит на поверхности, и тоже уже давно есть реализации
А можно ссылку? А то что-то мне не по глазам.
Так же подчеркну: я не делаю что-то принципиально новое и уникальное: просто раскладываю по полочкам старое и существующее.
To().Add(new Order()) отвалится
Он не может отвалиться потому что, простите, ничего не делает кроме создания команды и заталкивания её в очередь.
В ходе процедуры накатки этой очереди, возможностей отменить изменения представляется куда больше. Над этой архитектурной возможностью и политиками отката изменений из очереди я сейчас как раз думаю. Но если у вас есть по крайней мере список изменений (если угодно — лог кросс-системной транзакции), то откатить её становится значительно проще чем при любой другой реализации. Поправьте меня если я ошибаюсь.
Более того — уже в этой версии фреймворка можно реализовать простенький адаптер транзакций и если все системы, с которыми вы работаете — транзакционные, то при ошибке записи в любую из них произойдёт откат транзакций для всех. Это то, что уже работает. Как раз на случай если этот механизм не годится — я думаю над добавлением возможности покомандного откатывания, но пока сомневаюсь что это нужно.
Сложность-то лежит как раз в разработке каналов-адаптеров
Я и не отрицаю. Задача состояла в том, чтобы чётко разделить реализацию каналов-адаптеров и реализацию бизнес-логики. Не вижу ни одной причины, по которой можно сказать что эта задача не решена.
Если вы декомпозируете работу с внешними каналами по моей схеме — у вас сразу появляется чёткое понимание что надо сделать: реализовать раннеры для таких-то команд, реализовать методы таких-то аспектов для запросов. Хоть в джиру готовые задачи закатывай.
То есть можно работать по крайней мере в понятных примитивах, а не нырять в неизвестность при встрече с каждой внешней системой.
public interface IQueryFor<T>
IQueryable<T> All { get; }
IQueryable<U> Joined<U>();
Так не понятно, внутри взаимодействие идет через EF?
В зависимости от того, какой рантайм подключается. В настоящий момент меня хватило на то, чтобы сделать один рантайм — для EF.Core. Но можно написать другой, где конструировать запросы будет что-то другое. Я планирую ещё сделать рантйм на EF6, но это будет разработка чисто в демонстрационных целях, что вот, мол, можно из одной точки приложения переключиться между EF6 и EF.Core и результат не изменится. Tecture как раз об этом.
Меня раздражает традиционная архитектура бизнес-приложений
+1
// Этот ById приклеится ко всем IQueryFor, где T содержит int-овый IdКрасиво и здраво всё выглядит. Даже немного жаль что там где сейчасас на C# пишу ничего не решаю, а то бы попробовал.
Читал по диагонали и потом с трудом нашёл ссылку на репозиторий. Было бы здорово как то выделить и поместить или в самом начале или в самом конце. Можно даже отдельным заголовоком. Тут не стоит скромничать имхо.
В Tecture есть свой мини-IoC на типах.
Любопытства ради, а что вы понимаете под "IoC" в данном контексте?
Я подразумеваю любой примитив, позволяющий сопоставить в run-time тип некого компонента с его экземпляром, абстрагирующий пользователя от управления временем жизни этого экземпляра. Как-то так.
Штука, которой можно сказать "дай мне печеньку", не заботясь о том, откуда печенька берётся, кто её создал и на чём её мне привезут.
То есть только управление временем жизни?
Да. Видимо мне стоило использовать термин DI вместо IoC, но как-то так повелось что под IoC-контейнерами я (и не только я) понимаю вполне конкретную методологию.
Я вообще не силён в терминах и думаю что из статьи это довольно очевидно :)
И время жизни у вас только одно? Какое, кстати?
Я исхожу из
Лайфтаймы <...> всё равно во всех проектах одинаковые и прибиты гвоздями к лайфтайму подключений к базе (и остальным внешним системам).
Сервис создаётся в тот момент, когда он впервые понадобился и умирает (ну… у него есть диспоз, но умирать там шибко нечему) вместе со смертью корневого инстанса ITecture
(он Disposable) и задействованным подключения ко всем каналам.
Если брать на примере web-проекта, то смерть всего там в основном происходит после завершения обслуживания запроса.
Если брать на примере web-проекта, то смерть всего там в основном происходит после завершения обслуживания запроса.
Ну вот у меня тут под боком веб-проект (вполне себе enterprise), в котором лайфтаймов как минимум два: на все время жизни приложения и на время жизни запроса.
Вопрос в том, какие каналы у вас живут и почему так долго. Вангую что где-то у вас сидит SignalR или что-то подобное.
Без проблем — берёте TectureBuilder и делаете два разных инстанса ITecture
— один на долгую память, другой на запрос.
То есть инстанс, создаваемый TectureBuilder-ом можно воспринимать как модуль — пакет сервисов с одинаковым временем жизни. Просто вы не каждому ProductsService
прописываете явно .InSingletonScope
/.InstancePerRequest
, а делаете это один раз, скопом для всего ITecture.
Вангую что где-то у вас сидит SignalR или что-то подобное.
Нет.
Без проблем — берёте TectureBuilder и делаете два разных инстанса ITecture — один на долгую память, другой на запрос.
… и как они друг с другом взаимодействовать будут? Потому что сейчас я могу из per-request-сервиса обратиться к singleton, и у меня все хорошо.
В данном конкретном случае сходу мне в голову приходят два костыля: или выкинуть один из инстансов в статическую переменную, но это создаст проблемы использования других фишек Tecture вроде тестирования.
Либо запилить отдельный аспект для общения с инстансами из других лайфтаймов, что будет правильнее, но чуть больше заморочек.
Либо вообще не засовывать в Tecture ту часть, которая статическая (полагаю она меньше) и общаться с ней вне сервисов Tecture.
Но кейс интересный, учту.
Это основной кейс в asp.net. Конфигурация — singleton, а контроллеры и db context чаще всего per request. Синглетон-сервисов целая куча, не меньшая куча per-request сервисов.
Да. В конфигурации таких приложений, как мне показала практика — лежат по сути параметры инстанцирования каналов. Конекшн стринг к базе, адрес очереди, кэша и всякое такое прочее. Эти настройки по сути нужны один раз при инстанцировании канала в терминологии Tecture и держать их всё время в контейнере нет необходимости.
Но если хочется — можно например сделать канал/аспект с настройками. Вай нот? В дизайн всё ещё вписывается :)
Currently there are 2 aspects available for Tecture: ORM and DirectSql.Вместо Belgrade ORM лучше добавь поддержку REST API вызовов. Это покроет очень значительную часть потребностей потенциальной базы пользователей проекта.
Сделать будет легче чем ORM, а людей которые её захотят — сильно больше.
Ещё не увидел интеграцию с авторизацией, интерпрайзы это сильно любят. Например возьмём разделение на обычного пользователя и админов. Если на уровне «тулинга», можно было бы декларировать что вот это только для админов, поэтому сломайся когда попробуешь засунуть в сервис для обычных пользователей. Думаю что на уровне generic constraints, можно что-то такое придумать. Опять же, даже если это будет какое-то простейшее решение, которое работает в runtime вместо compile-time, то всё равно будет плюс в глазах пользователей. Ещё для подобных вещей можно притащить в проект .NET Compiler Platform Analyzers, там сильно гибко можно обработать правила и при компиляции отвалиться если не удовлетворяет критериям.
Ну и конечно, было бы совсем хорошо, если бы существовал некий roadmap по поэтапной миграции существующих приложений.
Например с чистого EF на вашу надстройку.
Интерпрайз архитекты думать не желают, хотят чтобы им продукт помог и проложил колею.
При создании корневого инстанса есть возможность подпихнуть свой Transaction Manager, создающий транзакции на разных этапах процесса и на выбранных вами каналах. Не очень удобно — придётся обернуть канальные транзакции в обёртки Tecture и отнаследоваться от Transaction Manager-а. Но сама возможность присутствует.
Higher-Kinded Types?
Higher-kinded type, тип высшего рода/порядка. Своего рода функция на уровне типов.
Примером HKT является дженерик: IEnumerable<int>
— это обычный тип (тип рода *
), а IEnumerable<>
— это уже HKT (тип рода * -> *
).
Однако, под присутствием HKT в языке понимается не возможность создать простой дженерик, а возможность определить на уровне типов произвольную функцию, и даже передать её куда-нибудь. Пример из С++:
template <typename T> struct foo_traits;
template <typename T> using foo = typename foo_traits<T>::foo;
template <> struct foo_traits<void> {
using foo = bar;
}
template <> struct foo_traits<int> {
using foo = baz;
}
Higher Kinded Types. Когда тип-параметры считаются полноценной частью языка, их можно собирать в массивы, фильтровать, группировать. В C# есть where-constraint-ы, но их мощности маловато. Дискуссию о том, что ожидают от них в C# можно почитать, например тут. Там же по ссылке приводят меткий термин: generics on generics.
Аналогию можно провести с higher kinded functions. В C# есть их поддержка через делегаты. С их помощью методы можно запихивать в переменные, собирать их в список, итерироваться по ним, передавать аргументами. Без них методы были бы просто методами. Вот хочется такой же гибкости, но на тип-параметрах.
Подскажите, что такое НКТ?
Спасибо!
UPD: повторила предыдущий вопрос. Ответ уже есть)
Чем-то похоже на чистую архитектуру, по кайней мере, судя по статье, есть попытка следовать некоторым его постулатам.
А мне показалось наоборот. В чистой архитектуре приложение не знает про то, что под ним, здесь оно имеет некоторое знание. Я бы даже сказал, что основная мысль автора, что для развесистой энтерпрайз аппликухи чистая архитектура недостижима и он предлагает некоторый практичный компромисс.
Например, бизнес код у автора работает с linq напрямую или посредством слоя "запросов" которые находятся в extension методах. В отличии от репозитория слой запросов нельзя подменить на другой (он в статических методах) в результате прикладной код зависит от linq.
Т.е. чтобы реализовать какой-то новый механизм хранения без изменения бизнес кода фактически надо реализовать некий эмулятор реляционки поверх (вместо того, чтобы реализовать только слой запросов к какому-то, возможно, более простому API).
Всё так, но не совсем.
Если вы используете O/RM как аспект, как соглашение, как подход — вы не можете отменить сам подход. Но переключить его реализацию — вполне можете. Идея в том, чтобы чётко разделять подход, внешнюю систему, которая этот подход приемлет и адаптер, который этот подход реализует для внешней системы.
LINQ — часть стандартной поставки .NET и это просто способ строить запросы. А вот способ ИСПОЛНЯТЬ эти запросы явно задаётся отдельно и его можно переключать.
Способ хранения при этом как раз не принципиален и его можно легко изменить. Ближайшая аналогия — это как интерфейс, у которого фиксированный набор методов, но может быть разная реализация.
Идея как раз в том, чтобы логика опиралась на подходы, а не на конкретную реализацию. Но в то же время подход — это не один интерфейс, а штука чуть сложнее включающая в себя, скажем, логирование, политику работы с тестовыми данными, логические ограничения на использование подхода и т.п. Выносить сложные внешние системы за один-единственный интерфейс — гораздо менее гибко чем определять подход к работе с ней.
Вот как-то так, только подход я называю аспект.
Ну я про это и говорю. Условно, если у вас порт (в терминах паттерна "hexagonal architecture") на уровне linq — то вы должны реализовать весь linq если надо присоединиться к чему-то другому. Если у вас репозиторий то там вы собираете только те запросы, которые реально используются вашим приложением — соответственно другая реализация должна реализовать только их.
В Tecture с одной стороны вы собираете используемое подмножество запросов, но в неудобной для подмены формы — вроде статических методов. Кстати, вы не задумывались сделать их instance методами? Типа не extension метод который вызывает запрос, а extension-метод, который инстанциирует репозиторий, который вызывает запрос или как-то еще.
Методические рекомендации к использованию вашей архитектуры требует вынесения всех запросов в слой query или допустимо фигачить мимо этого слоя?
linq
Послушайте, LINQ в Tecture — это сугубо opt-in ORM-аспекта. Вам не обязательно его использовать, но если используете то рантайм должен поддерживать его целиком. В данном случае я использую рантайм, основанный на EF для этого. Если бы у меня его не было — то я бы не смог запустить приложение.
В остальном — я ещё раз подчеркну что LINQ (а точнее его expression) прежде всего — это про конструирование запроса и хранение информации о нём. Не о выполнении. Выполнение LINQ-запроса легко делегируется другой части системы, что в ORM-аспекте и сделано.
Кстати, вы не задумывались сделать их instance методами?
А в чём профит? Я вижу в такой декомпозиции два возможных профита:
- моки. В Tecture не нужны (см. следующую статью);
- возможность изменить реализацию ОДНОГО КОНКРЕТНОГО запроса. Такая необходимость происходит крайне редко, но и в Tecture она возможна — LINQ-запросу можно задать маркер (скажем через тот же метод
.Describe
) и уже на уровне рантайма вставить хук — мол — если пытаются выполнить такой-то запрос — сделать вот так-то.
Просто если следовать этой логике, то и методы в духе Where
, Select
, Join
в LINQ так же стоило бы сделать instance-методами, но MS так не сделали по одной простой причине: смысл такой декомпозиции стремится к нулю при гигантских накладных расходах.
Методические рекомендации к использованию вашей архитектуры требует вынесения всех запросов в слой query или допустимо фигачить мимо этого слоя?
Нет "слоя" query. Слоёв по сути три — приложение, аспекты и рантайм. Запросы и команды — это разделение как бы "по другой оси координат". Т.е. есть часть системы, где пишут, а есть — где читают. By design у вас просто нет возможности нафигачить "мимо слоя". То есть можно, но это довольно сложно и вряд ли пользователь будет нарочно извращаться чтобы это сделать.
Выполнение LINQ-запроса легко делегируется другой части системы, что в ORM-аспекте и сделано.
Тут я имею ввиду уровень абстракции на котором определен интерфейс канала (КМК это то же самое что и "порт" в рамках hexagonal architecture)
А в чём профит?
Я встречал два варианта:
Допустим у вас есть какой-то модуль, который может хранить свои данные и в SQL и в плоских файлах. Если сделать "слой query" легко подменяемым (фактически — репозиторий) то можно легко заменять его реализации другими полностью — там где был сложный LINQ с джоинами будет, например вызитывание какого-нибудь смещения в файле. Чтобы по подменить реализации не придется поддерживать linq поверх этих файлов.
Допустим вы разрабатывали какой-то модуль на одной субд, и портировали на другую. В другой субд оказался такой оптимизатор, что некоторые запросы пришлось разбивать на несколько. Например в одной быстрее джоин 5 таблиц, а во второй вычитка данных из первых двух, а потом запрос к остальным трем.
Как будет выглядеть трюк с хуками в данном случае? Будет ли он типобезопасным?
Нет "слоя" query.
Наверное, я использовал неправильный термин. Имелся ввиду "Query extensions"
Query extensions can be placed everywhere. Their intention is to replace read abstractions (Repositories) in software design.
Логически он есть так как у вас есть типа "статический репозиторий" где собраны типовые запросы. Я не вижу механизма который воспрепятствовал бы обращению из бизнес кода к linq напрямую. Вопрос заключается в том, считается ли хорошей практикой из бизнес кода ходить в linq мимо этих репозиториев?
Там нужно выделить интерфейсы в отдельную сборку, на сервере — своя реализация, на клиенте — прокси-реализация через WCF/SOAP
В клиентском приложении всё так же — у вас есть один канал (сервер) + по вкусу локальный кэш — второй канал. Не вижу противоречий. WCF/SOAP — это уже вкусовщина на уровне рантайма в моей терминологии.
Вроде натягивается и даже не как сова на глобус.
Будет что-то в духе
var b = new TectureBuilder();
b.WithChannel<Server>(x=> { x.UseServer("127.0.0.1", 8080); });
var tecture = b.Build();
tecture.Let<OrdersService>().AddLine();
Нет. В Server
и его аспекты вы оборачиваете всю коммуникацию с сервером поверх WCF или что там у вас.
tecture.Let<OrdersService>().AddLine();
, значит и сборка, содержащая класс OrdersService, подключена в клиента.Да, логику с клиентской (а не серверной) логикой, содержащий набор сервисов, работающих с сервером как с каналом через WCF — придётся подключать к клиентскому приложению.
В классическом 3-звенном «интерпрайзе» есть интерфейс IOrderService с методом AddLine, и 3 его реализации: 1) OrderService, который выполнет всю работу, на сервере, в транзакции; 2) OrderService_WcfServer, который упомянут в *.svc-файле и потому вызывается IIS-ом на входящем запросе, и делегирующий всю работу первому сервису, и 3) OrderService_WcfClient, который делает wcf-вызовы к серверу. Классы OrderService_WcfServer и OrderService_WcfClient генерируются по интерфейсу автоматически, например, tt-файлами (как альтернативный вариант — какой-нибудь Castle DynamicProxy).
Клиентское приложение так сконфигурировано, что по IOrderService получает OrderService_WcfClient. Какая-нибудь консольная патчилка или тесты будут по IOrderService получать настоящий OrderService.
Что такое в вашей терминологии «клиентская логика»? Это ещё один класс OrderService, отличный от серверного OrderService? Его будет писать «архитектор»? Но ведь методы и параметры 100500 раз меняются по мере развития приложения. Типичное ТЗ: «добавьте в AddLine ещё один параметр Notes». Переписывать серверный, затем клиентский OrderService?
В этой ситуации делается так:
- на сервере: несколько каналов (база данных там, или что у вас), несколько сервисов для работы с ними.
- на клиенте: один канал (Server), один аспект для WCF, но я бы сделал RPC-аспект (думаю над ним), одним из рантаймов которого был бы WCF (который уже мало кто использует).
На сервере пишем OrderService как показано в статье. Для клиента можем генерить обвязки к ним тем же самым tt, которые пускают запросы к серверу через аспект (чтобы работал перехват данных — см. следующую статью). Вероятно вместо tt можно использовать типы (надо смотреть на конкретику).
Клиентское приложение будет получать свой собственный OrderService, который сгенерен для него по образу и подобию серверного.
Что такое в вашей терминологии «клиентская логика»?
Вот это вот.
И, кстати, зависимость на сборку с сервером (или же промежуточную сборку с интерфейсами, необходимую и клиенту и серверу) нужна как раз в вашей схеме.
И, кстати, зависимость на сборку с сервером (или же промежуточную сборку с интерфейсами, необходимую и клиенту и серверу) нужна как раз в вашей схеме.Верно, это один из принципов (IoC) в SOLID: зависимости не от реализаций, а от абстракций. Абстракции выносятся в отдельную сборку.
Клиентское приложение будет получать свой собственный OrderService, который сгенерен для него по образу и подобию серверного.Теперь понятно. Но тут я вижу тот минус, что код должен знать, какой ему нужен OrderService: серверный или клиентский, они разные. Если вдруг какой-то сервис нужно перенести с клиента на сервер, нужно будет править сам сервис, а не конфигурацию DI.
Согласен, должен. Однако в вашей декомпозиции есть минус куда более фатальный: протекающая абстракция.
Вы хотите обставить всё так что мол на сервере OrdersService и на клиенте OrdersService с теми же методами и работают они одинаково, но эта абстракция течёт по той причине что сериализовывать stateful-объекты вы не можете и если таковой возвращает серверный OrdersService, то при попытке вызвать этот метод на клиенте произойдёт ошибка. Уже не говоря о том, что надо помечать все модели DataContract-атрибутами и писать портянку WCF-конфигурации в конфиге, иначе чуда не случится. Плюс разный контракт ошибок: если нет связи с сервером то выкинется соответствующий эксепшон в то время как на сервере такового не может произойти по определению.
Вы как хотите, я (как уже было сказано) не претендую на создание единственно-верного-пути, но выступаю решительно против протекающих абстракций, полагая их несоизмеримо большим злом нежели неудобства кодогенерации.
1) Модели помечать атрибутами, портянки конфигурации (кстати, в app.config писать не обязательно — можно кодом нагенерить по списку классов/интерфейсов и зарегистрировать). Ну да, если требование — WCF, это нужно, так работает технология.
2) Сервисы не могут возвращать stateful-объекты, в частности, entities. Это решается вынесением абстракций. Если в сборке-интерфейсе не упоминаются entities, и только она подключена к клиенту, то на клиент entities никак не попадут.
Но вы же предложили использовать автогенерацию клиентского OrdersService (и все dto к нему тоже дублировать авто-генерацией?). Авто-генерация тупо сделает копии, не разбираясь, что за объект — stateful/stateless.
Интересный подход. У нас более "развязанная" архитектура (а логирование и прочие штуки можно приделывать к брокеру, через который общаются все модули), а модули, в принципе, могут быть на разных ЯП… У вас больше ориентированно на то, чтобы дать разработчикам рамки, за которые им не рекомендуется выходить, а у нас на то, чтобы разработчик конкретного модуля мог, при желании, сделать в нём почти всё, что угодно (лишь бы "контракт по API" не нарушался).
Но у вашего подхода, несомненно, тоже есть плюсы (в нашем немного больше шансов при стрельбе задеть ноги). Пока сходу не придумал, что можно позаимствовать, но буду иметь в виду.
Откройте свой рабочий проект, найдите то, что у вас называется «бизнес-логика»: это просто код, который сам ничего не делает, а только говорит что делать внешним по отношению к вашей системам.
А кто же все тогда делает? Редакторы баз данных не в счёт :)
Их не надо регистрировать ни в каком IoC-е
Тогда все будет прибито гвоздями к базовым классам, к статике, к созданию всего чту нужно прямо в коде или вам все-таки придётся передать что-то через конструктор.
И сидеть потом в ожидании runtime exception.
Это вероятно, если использовать IoC как Service Locator как любят в ASP.NET: IServiceProvider или ISupportRequiredService. Попробуйте использовать подход «composition root».
Во-вторых интерфейс для такого сервиса не нужон. Обычно сервисы прячутся за интерфейсы чтобы писать моки, но Tecture построен так, что потребность в моках отпадает.
Я думаю, что «сервисы прячутся за интерфейсы» еще и для того, чтобы детали (реализация) зависели только от абстракций. А «моки» не нужны если вы не пишите Unit тесты или используете что-то типа Microsoft Fakes.
В-третьих с такими сервисами, например, можно резать систему по семантическим швам и закатывать в отдельные сборки вместе с используемыми сущностями …
Для этого можно (и нужно) использовать интерфейсы.
В-четвёртых: тулинги. Я не нашёл подходящего названия для этого механизма, поэтому называю его тулинг.
Можно назвать «зависимости» :)
Тулинги явно описывают что в этом сервисе делается, а чего в нём точно не делается. Степень детализации этой информации зависит от аспекта. Вот про сервис из примера выше точно можно сказать что Order-ы он не удаляет, а OrderLine-ы не меняет (только создаёт).
Так же и зависимости в конструкторе (если это конечно не Service Locator) скажут вам что в этом сервисе делается, тут еще “interface-segregation principle” поможет.
Если попробовать написать в этом сервисе, скажем To_Db_().Delete(order) — компиляция упадёт с ошибкой, как бы говоря нам: «хэй, чувак, это наш двор и ордеры тут не удаляют
В случае с абстрактными зависимостями в конструкторе у вас не получится написать «.Delete(order)» если нет абстракции для этого, тут и до компилятора дело не дойдет.
PS: Вопросы есть почти по каждому тезису
Архитектура интерпрайз-приложений может быть другой