Архитектура интерпрайз-приложений может быть другой

    image


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


    Мне нравится перебирать архитектурные концепции. Всю жизнь я пытаюсь найти в области архитектуры и дизайна ПО что-то работающее и в то же время простое. Не требующее разрыва мозга для понимания и кардинальной смены парадигмы. Идей накопилось порядочно и я решил объединить лучшие из них в своём фреймворке — Reinforced.Tecture. Разработка таких штук даёт гигантское количество пищи для размышлений, я хочу ими поделиться.


    Тексты про такие технические вещи обычно до ужаса нудные. Я честно постарался не нудить, поэтому мой текст получился слегка агрессивным. Если вам с этим норм и интересно почитать про архитектуры .NET-приложений — заходите.


    Дисклеймер

    Это продолжение моей предыдущей статьи. Думаю, стоит сказать несколько слов перед тем, как перейти к сути дела.


    Момент первый: я не считаю что я тут самый умный, не пытаюсь никого научить, оскорбить или что-то продать. Я уважаю многолетний опыт индустрии (и опыт читателей в том числе), но то, что я наблюдаю в кишках каждого проекта всю свою карьеру — мне совсем не нравится. Мне за это стыдно. Я делал Tecture для себя, чтобы сэкономить своё время и успокоить свои нервы. Я презираю DevRel и коммерцию в open source за лицемерие, о чём уже писал в своей старой статье. Мне никто не платит, я не выступаю от лица никакой компании и делаю свои проекты в свободное время на свой страх и риск, без всяких гарантий (о чём сказано в MIT-лицензии). Я делюсь своими наработками на языке программирования, который не высасывает мне мозг и если они будут кому-то полезны — хорошо.


    Момент второй: я рассказываю про код для бизнеса. Не для игр, не для библиотек, не для фронтенда, а для бизнеса. Про кровавый интерпрайз, другими словами. Все знают что такое кровавый интерпрайз: опердни, автоматизации документооборота, складов, отчётности, делопроизводства, биллинг и прочая нудятина с префиксом "бизнес-" и привкусом офисной затхлости. Обычно такой код пишется двумя способами:


    • дико обмазывается классическими ОО-паттернами родом из Java (да, даже на C#) и лихих 80х-90х. Например — MS-овский eShopOnContainers, авторы которого использовали вообще всё, что когда-либо слышали про ОО-дизайн. Меня от такого кода просто разрывает, потому что авторы таких монстров на peer review регулярно не могут объяснить нахрена они сделали именно так;
    • говнокодится на коленке не самыми квалифицированными сотрудниками в тщетной попытке уложиться в дедлайны и удовлетворить бизнес на предмет требований. Как следствие — генерит больше мемов, чем полезной работы, а мне наливает фрустрации вместо утреннего кофе.

    В народе первое считается "правильно", а второе вроде как "быстро". А вот чтобы и быстро и правильно — никто вроде не сделал. Поэтому я попробовал сам, со своими представления о прекрасном. Получилось крафтово, оригинально и местами кринжово для мира .NET.


    Момент третий: я говорю про большие проекты для бизнеса. Не про хэллоуворлды, которых по 100 рублей пучок в базарный день можно купить на UpWork-е и не про стартапные кривожопые MVP. Я говорю про огромные, как слоновий хер проекты для бизнеса, которые делаются командами по 30 и более человек и длятся по 15 лет. Которые уже распластались на несколько баз данных, десятки подпроектов и просто вопят о том, что нужно уменьшить сложность. К таким проектам нет и никогда не было чёткой документации, но они работают в кровавом production-е, ежедневно обслуживая чёртову прорву разных бизнес-процессов. Прототипы списка покупок на node/react меня не интересуют. В них архитектура не нужна, потому что они всё равно сдохнут быстрее, чем их разработчики окончат университет. Мне с исследовательской позиции интересно управление сложностью в long term. У больших проектов остро встаёт вопрос "если переписывать, то как" и тут я попробую подкинуть пару идей.


    Внешние системы


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


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


    За примером далеко ходить не надо: скорее всего вы работаете с реляционной базой данных. Такие базы удобны бизнесу, который пользуется вашей системой, но не удобны вам. Поэтому вы используете O/RM, который по сути здоровенный конструктор SQL-я. Но в своё время вокруг O/RM-ов раздули дикий хайп и преподносили их чуть ли не как серебряную пулю от всех бед. А бизнесу тем временем совершенно пофигу — напишете вы INSERT-ы руками, или за вас их слепит библиотека. Я, кстати, не доверяю O/RM-ам уже давно и вот почему: они старательно пытаются внушить мне что "ложки нет". Есть, мол объекты, ты их меняешь. Есть коллекции, ты в них объекты добавляешь и удаляешь. А базы данных не существует. Ну подумаешь — надо вызывать SaveChanges время от времени.


    И это полная чушь. Как и все подобные абстракции, O/RM безбожно течёт. Он рвётся от натуги, когда пытается полностью заменить собой базу. Регулярно приходится выбирать — сделать "как по ООП" или выразиться в терминах SQL, чтобы работало быстрее. Не я первый натолкнулся на эту проблему, она довольно известна и называется "object-relational impedance mismatch". При попытке понять причины и придумать решение можно легко потонуть в высокопарных рассуждениях о несоответствии контрактов. Поэтому, я предлагаю проще: дело в том, что я не работаю с объектами. Моя конечная цель — изменения в базе данных, пусть и сделанные через объекты. Нельзя долго делать вид, что базы нет, а то она обидится и даст по роже в самый неподходящий момент самым непредсказуемым образом. Но и писать интерпрайз целиком в примитивах базы данных — тоже хреновая затея. Я ж не DBA какой-нибудь и не хочу перевести всю логику на stored-процедуры.


    Нужен разумный баланс. Я нашёл его в концепции каналов и аспектов. Моё авторское мнение: лучше не спорить с объективной реальностью и признать что у нас есть внешние системы. До них мы прокидываем каналы, с которыми работаем в тех или иных определённые аспектах.


    Именно такое положение дел перетекло в Tecture дословно. И вот первая абстракция, которую я добавляю. Даже две абстракции, которые ходят парой.


    Каналы (описаны в документации)


    Канал олицетворяет любую внешнюю систему, с которой мы будем взаимодействовать. База данных, очередь, кэш или что у вас там. В Tecture канал — это интерфейс, не требующий реализации. Чтобы его задекларировать — достаточно написать:


    public interface Db { }

    Букву I перед именем канала писать не нужно.


    Если бы я хотел поумничать и ляпнуть со сцены условного CodeFest-а что-то солидное от лица компании, то я бы наверняка сказал, что канал — "это типовой разделитель логики, который мы используем для извлечения метаинформации посредством HKT", но нет. Мне за умные слова никто не платит, поэтому я объясню проще.


    В C# нет ключевого слова type как в F# или TypeScript, а его самый близкий аналог — interface без реализации. Каналы будут использоваться именно как type — подставляться в методы и классы тип-аргументами, предоставляя метаинформацию и шевеля шарповый type inference под нужными мне углами. Плюс, в C# нет HKT, поэтому с реальными системами каналы будут сопоставляться через позднее связывание на reflection-е.


    Аспекты (в документации тут)


    Канал есть. Теперь надо привязать к нему аспекты. Аспект определяет как мы работаем с системой. Но он не определяет как система работает на самом деле. У нас есть канал базы данных и в куске кода ниже по тексту мы хотим сказать что мы будем работать с ним через O/RM, а ещё будем пулять в неё голым SQL-ем. И если по-честному, то прятаться за этим каналом может всё, что угодно, и ему не обязательно поддерживающее смапленные на типы множества или SQL нативно. Достаточно исполнять обязательства по аспектам. Это как интерфейс, только обыгранный чуть по-другому.


    Сначала я называл это "фича", но мне сказали что если переименовать в "аспект" — будет круче звучать. Я не очень хочу начинать со слов, что я сделал аспектно-ориентированный фреймворк. Вся теория вокруг AOP сложна и содержит кучу не очень удачных терминов. В Tecture можно разглядеть и аспекты, и советы, и срезы и точки соединения, но зачем? Я хочу уменьшить сложность, а не увеличить.


    Ещё канал может подсасывать несколько аспектов сразу. Я снасильничал над компилятором C# и обыграл это через множественный экстенд интерфейсов. Получается приятно и лаконично:


    PM> Install-Package Reinforced.Tecture.Aspects.Orm
    PM> Install-Package Reinforced.Tecture.Aspects.DirectSql

    public interface Db :
            CommandQueryChannel <
                    Reinforced.Tecture.Aspects.Orm.Command, 
                    Reinforced.Tecture.Aspects.Orm.Query>,
            CommandQueryChannel <
                    Reinforced.Tecture.Aspects.DirectSql.Command,
                    Reinforced.Tecture.Aspects.DirectSql.Query>
        { }

    Аспекты подтягиваются из отдельных пакетов, в ядре же самого Tecture ничего нет кроме поддержки корневых концепций (сервисы, каналы, команды и запросы). Вся конкретика by design должна лежать отдельно. Это сознательное решение: ядро и аспекты не требуют ничего сверх netstandard2.0. Конкретного кода там довольно мало — считай одни абстракции. А сборки на нетстандарте превосходно подключаются и к .NET Core и к полноразмерному фреймворку.


    Более того, ядро и аспекты (по задумке) являются необходимыми и достаточными зависимостями для реализации бизнес-логики. А это значит, что target framework для неё так же будет не выше netstandard2.0. В воздухе отчётливо запахло переездом на неткор. Таким образом, Tecture не мешает отвязке от полноразмерного .NET (читай: от windows), а очень даже потворствует.


    Но на практике всё зависит от того, какие ещё зависимости подтягиваются в логику. Если там есть что-то хитрое, требующее полноразмерный .NET Framework, то чуда не случится.


    А ещё умные мужики, придумавшие SOLID, называют это O/CP и Separation of Concerns. И благословляют.


    Также аспекты — это крутая тема с организационной точки зрения. Башковитый системный архитект может сам нахерачить аспект и связать его с каналом, таким образом закрепив для линейных разрабов формальные правила работы с внешними системами, специфичные для проекта. И эти правила ни один джун не сможет перепрыгнуть в силу строгой типизации — компилятор тупо не даст. Если так строить систему, то в ней будет соблюдаться порядочек, которым архитект рулит формально и может контролировать.


    И каналов, в общем-то, можно сделать сколько угодно. Тут есть свои бенефиты: можно организовать separated contexts в дань традиции DDD, а можно не мудрствуя лукаво использовать несколько баз данных из одного приложения без разрыва жопы.


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


    Сервисы


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


    // это сервис
    public class Orders : TectureService
        <                       
            Updates<Order>,     // он обновляет ордеры 
            Adds<OrderLine>     // и создаёт OrderLine-ы
        >
    {
        private Orders() { }    // это форсирование правил инстанцирования. 
                                // шучу. это приватный конструктор. так надо.
    
        // это бизнес-метод бизнес-логики бизнес-бизнеса
        public void AddLine(int orderId, int productId, int quantity)   
        {   
            // вот так мы читаем из канала
            var order = From<Db>().Get<Order>().ById(orderId);          
    
            // а вот так пишем
            To<Db>().Update(order)                                      
                .Set(x => x.TotalQuantity, order.TotalQuantity + quantity);
    
            // потому что можем
            To<Db>().Add(new OrderLine {                                
                OrderId = orderId, 
                ProductId = productId, 
                Quantity = quantity});
    
            // а так зовём другой сервис, если не можем
            Let<Products>().Reserve(productId, quantity);                
    
        }                       // всё.
    }

    Такие сервисы уделывают обычные, сделанные руками по всем пунктам:


    Во-первых их не надо регистрировать ни в каком IoC-е. Вызвать один сервис из другого можно написав Let<Products>().СделатьЧтото(...) в любом месте сервиса (кроме конструктора). Одна только эта примочка начисто сносит 90% однотипных записей в регистрации IoC-а (вместе с модулями да) и выпиливает из проекта километровые портянки бессмысленного и тупого интеграционного кода, в котором легко потерять строчку при рефакторинге. И сидеть потом в ожидании runtime exception. Да и вообще, когда те же проблемы решаются за меньшее количества кода — это хорошо. Всё по ТРИЗ, как деды завещали.


    В Tecture есть свой мини-IoC на типах. Тупой ровно настолько же, насколько и эффективный. Лайфтаймы в нём прописывать не надо — они всё равно во всех проектах одинаковые и прибиты гвоздями к лайфтайму подключений к базе (и остальным внешним системам). Инстансы сервисов Tecture создаёт лениво, поэтому можно не бояться циклических зависимостей. И за потребление памяти тоже можно не бояться. В довершении всего: никакого дискаверинга сервисов при старте не происходит, что позволяет Tecture не добавлять приложению перфоманс-оверхеда без необходимости.


    Во-вторых интерфейс для такого сервиса не нужон. Обычно сервисы прячутся за интерфейсы чтобы писать моки, но Tecture построен так, что потребность в моках отпадает. Как и почему это происходит я объясню потом. Пока важно вот что: выносить сервисы за интерфейсы не надо и точка. Да, можно выкинуть из кодовой базы кучу бесполезных файлов с ISomethingService. Это тоже хорошо. Меньше типов, меньше абстракций — проще проект. Я терпеть не могу интерфейсы, у которых ровна одна реализация и постоянно заменяю их на классы (расставляя virtual если потребуется). Они не нужны примерно ни за чем, кроме как чтобы добавить мне ещё один клик мышкой при попытке увидеть код метода.


    В-третьих с такими сервисами, например, можно резать систему по семантическим швам и закатывать в отдельные сборки вместе с используемыми сущностями. Вот есть у нас отдел обработки заказов на фирме — сделаем под него отдельную dll-ку, декларирующую все, используемые отделом заказов сущности. Отведём на обслуживание этой части системы отдельную команду. А наружу будут торчать сервисы для работы с заказами. Сущности для полного счастья можно закрыть на изменение модификатором internal так, чтобы все изменения проходили только через сервисы. Готово, вы великолепны: система гранулирована на мелкие кусочки, всё разложено по полочкам — хоть инкрементальные билды делай. Зависимости между такими сборками будут иерархическими, а иерархия всегда проще для понимания чем линейная структура. Ну и назвать такие сборки можно "domains", типа бизнес-домен.


    В-четвёртых: тулинги. Я не нашёл подходящего названия для этого механизма, поэтому называю его тулинг. Это вот там, где у сервиса указаны тип-параметры:


    public class Orders : TectureService < Updates<Order>,  Adds<OrderLine> >
    {

    Тулинги явно описывают что в этом сервисе делается, а чего в нём точно не делается. Степень детализации этой информации зависит от аспекта. Вот про сервис из примера выше точно можно сказать что Order-ы он не удаляет, а OrderLine-ы не меняет (только создаёт). И это мы глянули только на шапку сервиса, а уже сколько информации. Я могу так сделать, потому что строгая типизация в C# решительно даёт прикурить остальным языкам. Если попробовать написать в этом сервисе, скажем To<Db>().Delete(order) — компиляция упадёт с ошибкой, как бы говоря нам: "хэй, чувак, это наш двор и ордеры тут не удаляют".


    Тулинги гибкие. Они тоже подсасываются из аспекта. Вот интерфейсы Updates<> и Adds<> определены в аспекте ORM до восьми сущностей включительно. Само собой, это автогенерированный код, я не писал это всё руками. Жаль что в шарпе пока нет квазицитирования и HKT — приходится собирать подобные конструкции из говна и палок.


    Но с другой стороны — оно и к лучшему. Отсутствие HKT не позволяет писать типы, параметризуемые потенциально бесконечными числом аргументов и вынуждает меня ограничивать как сервисы, так и их тулинги по числу тип-параметров. Это можно использовать чтобы предотвратить появление в системе god object-ов. Я считаю так, что если вы добавляете девятую по счёту обязанность сервису, то ему уже хватит и надо его декомпозировать, а не накидывать. Компилятор просто помогает мне как архитекту доносить эту мысль до линейных разработчиков наиболее эффективно. По задумке — сделать заготовку для TectureService с девятью параметрами будет сложнее, чем потратить 10 минут и разбить сервис на два. Так я ситуативно использую лень разработчика, чтобы направить его по пути декомпозиции.


    В-пятых: я убрал дебильный суффикс "Service". И так понятно что это — сервис. В жопу суффикс.


    Чтобы вызвать один сервис из другого есть Let<>. Но как вызвать сервис извне? Подробно я расскажу в следующей статье, но для полноты в двух словах: сам Tecture может регистрироваться в любом IoC-е как интерфейс ITecture (через фэктори метод). Получить инстанс ITecture можно пнув TectureBuilder и забайндив каналы. Именно так, через построитель каналы и аспекты связываются с живыми внешними системами. Штука, которая непосредственно обеспечивает коммуникации и реализует аспекты называется рантайм. И пока что достаточно о них.


    Так вот, у ITecture есть метод Let<>(), такой же как и внутри сервиса. Через него можно позвать позвать любой понравившийся сервис просто подстановкой типа: tecture.Let<Orders>().CreateOne(...), как только инстанс ITecture окажется у вас в руках.


    Что ещё доступно внутри сервиса? Не считая всякие службные штуки, можно сказать что в основном там обитают три интересных метода (закрыты модификатором protected):


    • Let<TService>(), про который я уже сказал. Он лениво резольвит инстанс другого сервиса и позволяет вызвать методы из него. У него есть брат-близнец: метод Do<>. Делает ровно то же самое, просто позволяет писать более идиоматично и человеко-читаемо.
    • From<TChannel>(): отдаёт конец канала, через который можно читать данные. Такой же, кстати, есть у инстанса ITecture;
    • To<TChannel>(): отдаёт конец канала, через который можно писать данные. Тулинг сервиса может влиять на то, что и как можно писать;

    На From<> и To<> стоит остановиться подробнее.


    Команды и запросы


    Что меня зацепило в архитектуре EntityFramework: запросы к базе данных делаются вот прямо вот на месте. Пишешь LINQ, транслятор его запинывает в SQL, используя метаданные, скармливает базе и выплёвывает коллекцию объектов в момент, когда разворачивается получившийся IQueryable. Но! Если хочешь что-то записать в базу, то всё происходит по-другому. Ты меняешь объекты, зовёшь .Add, .Remove — создаёшь такой… чертёж изменений. Потом хлоп — SaveChanges, всё собирается в SQL батч и летит в базу. Я зацепился за эту мысль и долго крутил её в голове. Ненавижу EF-ный ChangesTracker, который сравнивает начальное и конечное состояние объектов и выводит diff, но вот сам подход "запросы сейчас, а изменения — потом" — звучит дельно.


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


    Вот та же база данных. Какие грабли подстерегают нас при записи? Индексы тормозят из-за перебалансировки б-дерева (особенно кстати на вставке гуидов заметно), транзакции надо разруливать чтобы не перетереть чужие изменения, консистентность данных там блюсти, денормализацию ещё затриггерить. Ну что-то в этом духе.


    Когда пытаешься читать — всплывает совершенно другое. Типа а как читать по-быстрее, как составить запрос, какие данные клиенту нужны, а какие не очень, что делать если требуют 10 тысяч записей одной пачкой, в какие индексы смотреть чтобы не облажаться, может вообще читать из кэша? Ну и самое очевидное — редко когда удаётся что-то записать не прочитав.


    Инструментарий для чтения и записи должен быть разным. Я долго думал как это обыграть. Пошёл посмотреть что в интернете предлагают, открыл для себя дивный мир CQRS, вскоре разочаровался в нём, посмотрел на MediatR, изучил тему DDD, пролистал книгу "Entity Framework Core in Action" за авторством какого-то умного чувака и поглядел репозиторий к ней. Выпал в осадок и понял, что так делать точно не надо. Потом пошевелил мозгами и решил делать как Microsoft — тупо, нагло, прямо. Если Microsoft берётся делать фреймворк для MVC, то жди классов Model, View и Controller. Если для web-а, то будет HttpRequest и HttpResponse. Прямо, эффективно, без лишней зауми и оверинжиниринга. Местами даже тупо. Ну будем подражать великим.


    В Tecture разделение чтения и записи обыгрывается в лоб: чтение выполняется через запросы, а запись — через отложенные команды.


    Учимся читать (про запросы)


    Чтобы сделать запрос — надо достать входной конец канала через From<>.


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


    В этом есть глубокий теоретический смысл: запросы в основном не меняют состояние внешней системы, если отбросить concern-ы производительности. SELECT данных в базу не добавляет. В идеальном мире его можно выполнить сколько угодно раз и получить один и тот же ответ. А это уже толстый намёк на идемпотентность чтения, что в случае с базой данных реально так, если юзать транзакции — см. Repeatable Read. Тут любители ФП кричат нам с дивана: функция, которая возвращает результат, не модифицирует глобальный контекст, да ещё и собственные параметры не изменяет, что-то подозрительно похожа на чистую. А чистые функции в C# принято выражать экстеншонами.


    Если отдельно проработать механизмы перехвата запросов и возможность подстановки fake-ответов, то быстро выясняется что Repository Pattern НЕ НУЖЕН. По этому пути я и пошёл. В итоге запросы в Tecture писать непривычно, но просто: берёшь "читальный конец" канала, заворачиваешь в отдельную абстракцию и просто фигачишь к ней статические методы расширения. Это хорошо тем, что становится пофигу где именно написан метод запроса — компилятор, женерик-констрейнты и маркировочные интерфейсы прицепят их куда надо. А решарпер ещё и подскажет. Вот мой любимый пример: как сделать метод GetSomethingById, выкинув из системы добрую половину репозиториев:


    // Общий интерфейс сущности с Id-шником
    public interface IEntity { int Id {get;} }
    
    // Промежуточная абстракция над IQueryable (схематично)
    public interface IQueryFor<T> 
    { 
        IQueryable<T> All { get; } 
        IQueryable<U> Joined<U>(); 
    }
    
    public static class Extensions
    {
    
        // Достаём нужный нам IQueryFor из читального конца канала
        public static IQueryFor<T> Get<T>(this Read<QueryChannel<Orm.Query>> qr) where T : class
        {
            // но вообще этот код написан в аспекте
            var pr = qr.Aspect();
            // тут он просто для наглядности
            return new QueryForImpl<T>(pr);
        }
    
        // Этот ById приклеится ко всем IQueryFor<T>, где T содержит int-овый Id
        public static T ById<T>(this IQueryFor<T> q, int id) where T : IEntity
        {
            return q.All.FirstOrDefault(x => x.Id == id);
        }
    }

    Всё, репозитории не нужны:


    // Читаемо, идиоматично, метафорично
    var user = From<Db>().Get<User>().ById(10);

    Не то чтобы я тут изобретал что-то совсем новое — аналогично устроен весь LINQ, да и в целом весь fluent-стиль, но чёрт побери, как же это удобно.


    Учимся писать (про команды)


    Для чтения у каналов есть читальный конец — From<>(). Значит для операций записи/изменения есть… я не знаю, ПИСАЛЬНЫЙ КОНЕЦ? В общем та штука, которую возвращает To<>() внутри сервиса. Получить её за пределами сервиса невозможно — так сделано, чтобы не было соблазна разбрасывать изменение данных по всему коду. Хочешь изменений — вступай в сервисы.


    To<>() возвращает тип Write<...> с кучей женерик-параметров, к нему подтягиваются экстеншоны из аспекта. С учётом тулингов, конечно же. Там вот выше в примере таким способом был вызван .Add. Вот его исходники. Если аспект или тулинг не позволяет подобрать .Add с нужными аргументами — ошибка компиляции. Если позволяет — успех.


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


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


    Ну и по аналогии с EntityFramework, можно сделать Save у корневого инстанса ITecture. Я называю этот этап сохранение.


    В чём профит? Больше контроля из одной точки. С такой очередью удобно работать. Можно как угодно издеваться над ней программно. Например, отдельные этапы записи можно обложить логами, можно гибко сделать отлов exception-ов один раз на всё приложение, можно сериализовывать очередь. Можно вообще её не выполнять. Я подумываю над программным способом отката изменений, но пока такое сложновато.


    Но самое крутое в том, что появляется чёткое разделение ошибок. Все exception-ы, пойманные в ходе выполнения логики можно смело трактовать как логические ошибки приложения. То есть смысловые. Например: не выполняется какое-то бизнес-правило, нарушаются ограничения выданные по ТЗ, недостаточно товара на складе, нет подходящей детали и иже с ними. А вот технические ошибки по причине, скажем, недоступности базы данных, отвалившейся транзакции, упавшего веб-сервиса, неушедшего e-mail-а проявляются только в моменты сохранения (ну и запросов). Становится проще концептуально отделить мух от котлет и понять — это вы лажаете, или сторонняя система гонит. Ну и чинить по обстоятельствам.


    Последний узкий момент, который надо упомянуть: начать сохранение изнутри сервиса невозможно технически. Но как быть, если надо что-то сделать с только что добавленным заказом? А вот как: можно тоже положить это действие в специальную очередь, которая будет разобрана после того, как Tecture разберёт основную очередь. И это изящно обыграно синтаксически (код приводится на примере ORM-аспекта, ни один Order не пострадал):


    public async Task ILikeToMoveIt()
    {
        var newOrder = To<Db>().Add(new Order());
    
        await Save;
    
        var id = From<Db>().Key(newOrder);
        To<Db>().Delete<Order>().ByPk(id);
    }

    Это Save-оцентрический await. Ну весело же, ну!


    На самом деле это только выглядит красиво на простых примерах, но вообще использовать его надо с осторожностью — конфликтует с асинхронными запросами. В крайнем случае можно откатиться к явной записи через Save.ContinueWith(...).




    Это были основные примитивы Tecture и я намеренно старался держать их количество под контролем, чтобы снизить порог вхождения. По той же причине я не пишу про его внутреннее устройство. Там местами свой локальный адок (связанный с обходом отдельных языковых ограничений C#), но в общем ничего криминального. Исходники открыты — вот ссылка на репозиторий.


    Однако, сухой текст и теоретические рассуждения не позволяют прочувствовать как Tecture ведёт себя на практике. Тот самый Development Experience надо показывать на примерах.


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


    image


    Там и встретимся.

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 55

      +6
      Извините, ни в коем случае не негатив, но под заголовком предполагал описание именно enterprise архитектуры, а нашел вариант реализации продукта.
      Проблемы продуктов в enterprise чуток шире.
      Далеко не эксперт, но вы верно заметили что в любой enterprise (напр. крупные банки, типа JPMC, WellsFargo) внутри хаос из продуктов 3х фирм и внутренних продуктов.
      Как следствие, enterprise архитектура продуктов должна решать задачи enterprise уровня.
      Желательна концепция архитектуры, принимающая во внимание и зоопарк, как неизбежность, и задачи бизнеса и проблемы реализации (неимоверно затянутые сроки, низкий уровень понимания big picture) и снижение порога вхождения для эффективного добавления продуктов и принудительная стандартизация для исключения нового хаоса и масса других мелочей, для описания которых комментарий неуместен.
      Ребята из Jet иногда делятся интересными решениями.
        +3

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


        Что для обычных проектов "ааа! мы подключаем интеграцию ещё с одной системой спасите!", то для проектов на Tecture — "о, архитект завёл новый канал".

          +7
          Как по мне, вы предлагаете велосипед. Проблема-то достаточно старая, решение такое лежит на поверхности, и тоже уже давно есть реализации (правда, как и принято в приличном энтерпрайзе, либо с конскими ценами, либо на Java, либо с конскими ценами на Java), работают в принципе похожим образом, и кроме того, включают дополнительные необходимые обработчики грабель, на которые вы пока ещё не наступили, но обязательно наступите. Например, поддержку транзакций, когда ваш To().Add(new Order()) отвалится где-то в недрах Db, и надо синхронно откатить всё то, что наделали перед этой операцией на другом конце этого вашего «канала» и всех ему предшествующих.
          Сложность-то лежит как раз в разработке каналов-адаптеров для всего корпоративного зоопарка.
            0
            Проблема-то достаточно старая, решение такое лежит на поверхности, и тоже уже давно есть реализации

            А можно ссылку? А то что-то мне не по глазам.
            Так же подчеркну: я не делаю что-то принципиально новое и уникальное: просто раскладываю по полочкам старое и существующее.


            To().Add(new Order()) отвалится

            Он не может отвалиться потому что, простите, ничего не делает кроме создания команды и заталкивания её в очередь.


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


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


            Сложность-то лежит как раз в разработке каналов-адаптеров

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


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


            То есть можно работать по крайней мере в понятных примитивах, а не нырять в неизвестность при встрече с каждой внешней системой.

        0
        public interface IQueryFor<T>  
            IQueryable<T> All { get; } 
            IQueryable<U> Joined<U>(); 
        


        Так не понятно, внутри взаимодействие идет через EF?
          +3

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

            +2
            EF6 и EF.Core


            Тогда уже лучше EF 5 или Dapper.
            EF 6 скоро будет legacy
              +1

              Я тут надысь узнал про существование Belgrade ORM. Вот можно его попробовать воткнуть :) Но конструктор запросов — одна из самых сложных частей EF и если делать его с нуля, то я даже не знаю с какого боку кусать эту задачу.

                +1
                А в общем — проект интересный, но думаю, в одного такое нереально поддерживать и развивать.
                  +3

                  Мы рождены чтоб сказку сделать былью :)


                  Но да, бывает грустненько.

          +4
          Меня раздражает традиционная архитектура бизнес-приложений

          +1


          // Этот ById приклеится ко всем IQueryFor, где T содержит int-овый Id

          Красиво и здраво всё выглядит. Даже немного жаль что там где сейчасас на C# пишу ничего не решаю, а то бы попробовал.


          Читал по диагонали и потом с трудом нашёл ссылку на репозиторий. Было бы здорово как то выделить и поместить или в самом начале или в самом конце. Можно даже отдельным заголовоком. Тут не стоит скромничать имхо.

            +2
            В Tecture есть свой мини-IoC на типах.

            Любопытства ради, а что вы понимаете под "IoC" в данном контексте?

              0

              Я подразумеваю любой примитив, позволяющий сопоставить в run-time тип некого компонента с его экземпляром, абстрагирующий пользователя от управления временем жизни этого экземпляра. Как-то так.


              Штука, которой можно сказать "дай мне печеньку", не заботясь о том, откуда печенька берётся, кто её создал и на чём её мне привезут.

                0

                То есть только управление временем жизни?

                  +2

                  Да. Видимо мне стоило использовать термин DI вместо IoC, но как-то так повелось что под IoC-контейнерами я (и не только я) понимаю вполне конкретную методологию.


                  Я вообще не силён в терминах и думаю что из статьи это довольно очевидно :)

                    +1

                    И время жизни у вас только одно? Какое, кстати?

                      +1

                      Я исхожу из


                      Лайфтаймы <...> всё равно во всех проектах одинаковые и прибиты гвоздями к лайфтайму подключений к базе (и остальным внешним системам).

                      Сервис создаётся в тот момент, когда он впервые понадобился и умирает (ну… у него есть диспоз, но умирать там шибко нечему) вместе со смертью корневого инстанса ITecture (он Disposable) и задействованным подключения ко всем каналам.


                      Если брать на примере web-проекта, то смерть всего там в основном происходит после завершения обслуживания запроса.

                        +2
                        Если брать на примере web-проекта, то смерть всего там в основном происходит после завершения обслуживания запроса.

                        Ну вот у меня тут под боком веб-проект (вполне себе enterprise), в котором лайфтаймов как минимум два: на все время жизни приложения и на время жизни запроса.

                          +1

                          Вопрос в том, какие каналы у вас живут и почему так долго. Вангую что где-то у вас сидит SignalR или что-то подобное.


                          Без проблем — берёте TectureBuilder и делаете два разных инстанса ITecture — один на долгую память, другой на запрос.


                          То есть инстанс, создаваемый TectureBuilder-ом можно воспринимать как модуль — пакет сервисов с одинаковым временем жизни. Просто вы не каждому ProductsService прописываете явно .InSingletonScope/.InstancePerRequest, а делаете это один раз, скопом для всего ITecture.

                            0
                            Вангую что где-то у вас сидит SignalR или что-то подобное.

                            Нет.


                            Без проблем — берёте TectureBuilder и делаете два разных инстанса ITecture — один на долгую память, другой на запрос.

                            … и как они друг с другом взаимодействовать будут? Потому что сейчас я могу из per-request-сервиса обратиться к singleton, и у меня все хорошо.

                              +2

                              В данном конкретном случае сходу мне в голову приходят два костыля: или выкинуть один из инстансов в статическую переменную, но это создаст проблемы использования других фишек Tecture вроде тестирования.


                              Либо запилить отдельный аспект для общения с инстансами из других лайфтаймов, что будет правильнее, но чуть больше заморочек.


                              Либо вообще не засовывать в Tecture ту часть, которая статическая (полагаю она меньше) и общаться с ней вне сервисов Tecture.


                              Но кейс интересный, учту.

                                +2

                                Это основной кейс в asp.net. Конфигурация — singleton, а контроллеры и db context чаще всего per request. Синглетон-сервисов целая куча, не меньшая куча per-request сервисов.

                                  0

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


                                  Но если хочется — можно например сделать канал/аспект с настройками. Вай нот? В дизайн всё ещё вписывается :)

                                    0
                                    Кстати из того же asp.net mvc можно взять концепцию processing pipeline.

                                    Туда органично впишется authorization или data validation.
                                      0

                                      Чтобы был processing pipeline — надо чтобы было что процессить. А у нас команды и запросы :)

              +4
              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 на вашу надстройку.

              Интерпрайз архитекты думать не желают, хотят чтобы им продукт помог и проложил колею.
                +2

                Согласен с вами, работы именно столько. Не уверен что стану её делать.

                –3
                Каждый программист мечтает создать свой велосипед ASP.NET Boilerplate.
                  +1
                  А как работать с транзакционными каналами? У меня несколько каналов и они должны отработать в одной транзакции.
                    0

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

                    +1
                    а что такое НКТ?
                      0

                      Higher-Kinded Types?

                        +1

                        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;
                        }
                          +2

                          Higher Kinded Types. Когда тип-параметры считаются полноценной частью языка, их можно собирать в массивы, фильтровать, группировать. В C# есть where-constraint-ы, но их мощности маловато. Дискуссию о том, что ожидают от них в C# можно почитать, например тут. Там же по ссылке приводят меткий термин: generics on generics.


                          Аналогию можно провести с higher kinded functions. В C# есть их поддержка через делегаты. С их помощью методы можно запихивать в переменные, собирать их в список, итерироваться по ним, передавать аргументами. Без них методы были бы просто методами. Вот хочется такой же гибкости, но на тип-параметрах.

                          0
                          Добрый день!
                          Подскажите, что такое НКТ?
                          Спасибо!

                          UPD: повторила предыдущий вопрос. Ответ уже есть)
                            0

                            Чем-то похоже на чистую архитектуру, по кайней мере, судя по статье, есть попытка следовать некоторым его постулатам.

                              +1

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


                              Например, бизнес код у автора работает с linq напрямую или посредством слоя "запросов" которые находятся в extension методах. В отличии от репозитория слой запросов нельзя подменить на другой (он в статических методах) в результате прикладной код зависит от linq.


                              Т.е. чтобы реализовать какой-то новый механизм хранения без изменения бизнес кода фактически надо реализовать некий эмулятор реляционки поверх (вместо того, чтобы реализовать только слой запросов к какому-то, возможно, более простому API).

                                0

                                Всё так, но не совсем.


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


                                LINQ — часть стандартной поставки .NET и это просто способ строить запросы. А вот способ ИСПОЛНЯТЬ эти запросы явно задаётся отдельно и его можно переключать.


                                Способ хранения при этом как раз не принципиален и его можно легко изменить. Ближайшая аналогия — это как интерфейс, у которого фиксированный набор методов, но может быть разная реализация.


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


                                Вот как-то так, только подход я называю аспект.

                                  +1

                                  Ну я про это и говорю. Условно, если у вас порт (в терминах паттерна "hexagonal architecture") на уровне linq — то вы должны реализовать весь linq если надо присоединиться к чему-то другому. Если у вас репозиторий то там вы собираете только те запросы, которые реально используются вашим приложением — соответственно другая реализация должна реализовать только их.


                                  В Tecture с одной стороны вы собираете используемое подмножество запросов, но в неудобной для подмены формы — вроде статических методов. Кстати, вы не задумывались сделать их instance методами? Типа не extension метод который вызывает запрос, а extension-метод, который инстанциирует репозиторий, который вызывает запрос или как-то еще.


                                  Методические рекомендации к использованию вашей архитектуры требует вынесения всех запросов в слой query или допустимо фигачить мимо этого слоя?

                                    0
                                    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 у вас просто нет возможности нафигачить "мимо слоя". То есть можно, но это довольно сложно и вряд ли пользователь будет нарочно извращаться чтобы это сделать.

                                      0
                                      Выполнение LINQ-запроса легко делегируется другой части системы, что в ORM-аспекте и сделано.

                                      Тут я имею ввиду уровень абстракции на котором определен интерфейс канала (КМК это то же самое что и "порт" в рамках hexagonal architecture)


                                      А в чём профит?

                                      Я встречал два варианта:


                                      1. Допустим у вас есть какой-то модуль, который может хранить свои данные и в SQL и в плоских файлах. Если сделать "слой query" легко подменяемым (фактически — репозиторий) то можно легко заменять его реализации другими полностью — там где был сложный LINQ с джоинами будет, например вызитывание какого-нибудь смещения в файле. Чтобы по подменить реализации не придется поддерживать linq поверх этих файлов.


                                      2. Допустим вы разрабатывали какой-то модуль на одной субд, и портировали на другую. В другой субд оказался такой оптимизатор, что некоторые запросы пришлось разбивать на несколько. Например в одной быстрее джоин 5 таблиц, а во второй вычитка данных из первых двух, а потом запрос к остальным трем.



                                      Как будет выглядеть трюк с хуками в данном случае? Будет ли он типобезопасным?


                                      Нет "слоя" query.

                                      Наверное, я использовал неправильный термин. Имелся ввиду "Query extensions"


                                      Query extensions can be placed everywhere. Their intention is to replace read abstractions (Repositories) in software design.

                                      Логически он есть так как у вас есть типа "статический репозиторий" где собраны типовые запросы. Я не вижу механизма который воспрепятствовал бы обращению из бизнес кода к linq напрямую. Вопрос заключается в том, считается ли хорошей практикой из бизнес кода ходить в linq мимо этих репозиториев?

                              0
                              Для трёхзвенных десктопных приложений не очень подходит.
                              Там нужно выделить интерфейсы в отдельную сборку, на сервере — своя реализация, на клиенте — прокси-реализация через WCF/SOAP
                                0

                                В клиентском приложении всё так же — у вас есть один канал (сервер) + по вкусу локальный кэш — второй канал. Не вижу противоречий. WCF/SOAP — это уже вкусовщина на уровне рантайма в моей терминологии.


                                Вроде натягивается и даже не как сова на глобус.

                                  0
                                  Так как с клиента вызывать сервисы, не абстрактные Add/Update, а в терминах бизнес-логики (OrdersService.AddLine)? Если сигнатура Orders.AddLine существует только в сборке-реализации сервиса.
                                    0

                                    Будет что-то в духе


                                    var b = new TectureBuilder();
                                    b.WithChannel<Server>(x=> { x.UseServer("127.0.0.1", 8080); });
                                    var tecture = b.Build();
                                    tecture.Let<OrdersService>().AddLine();
                                      –2
                                      Получается, что мы на клиента тащим реализацию OrdersService (пользователям доступен код, которого у них быть не должно), а также все зависимости — EF, Hibernate (если есть), драйверы БД (mysql/postgress), которые фактически не будут запускаться, а нужны просто потому, что подключены к DLL с OrdersService?
                                        0

                                        Нет. В Server и его аспекты вы оборачиваете всю коммуникацию с сервером поверх WCF или что там у вас.

                                          –2
                                          Если в коде клиента есть строчка
                                          tecture.Let<OrdersService>().AddLine();
                                          , значит и сборка, содержащая класс OrdersService, подключена в клиента.
                                            0

                                            Да, логику с клиентской (а не серверной) логикой, содержащий набор сервисов, работающих с сервером как с каналом через WCF — придётся подключать к клиентскому приложению.

                                              0
                                              Я не понимаю ваш ответ.

                                              В классическом 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?
                                                0

                                                В этой ситуации делается так:


                                                • на сервере: несколько каналов (база данных там, или что у вас), несколько сервисов для работы с ними.
                                                • на клиенте: один канал (Server), один аспект для WCF, но я бы сделал RPC-аспект (думаю над ним), одним из рантаймов которого был бы WCF (который уже мало кто использует).

                                                На сервере пишем OrderService как показано в статье. Для клиента можем генерить обвязки к ним тем же самым tt, которые пускают запросы к серверу через аспект (чтобы работал перехват данных — см. следующую статью). Вероятно вместо tt можно использовать типы (надо смотреть на конкретику).


                                                Клиентское приложение будет получать свой собственный OrderService, который сгенерен для него по образу и подобию серверного.


                                                Что такое в вашей терминологии «клиентская логика»?

                                                Вот это вот.


                                                И, кстати, зависимость на сборку с сервером (или же промежуточную сборку с интерфейсами, необходимую и клиенту и серверу) нужна как раз в вашей схеме.

                                                  0
                                                  И, кстати, зависимость на сборку с сервером (или же промежуточную сборку с интерфейсами, необходимую и клиенту и серверу) нужна как раз в вашей схеме.
                                                  Верно, это один из принципов (IoC) в SOLID: зависимости не от реализаций, а от абстракций. Абстракции выносятся в отдельную сборку.

                                                  Клиентское приложение будет получать свой собственный OrderService, который сгенерен для него по образу и подобию серверного.
                                                  Теперь понятно. Но тут я вижу тот минус, что код должен знать, какой ему нужен OrderService: серверный или клиентский, они разные. Если вдруг какой-то сервис нужно перенести с клиента на сервер, нужно будет править сам сервис, а не конфигурацию DI.
                                                    0

                                                    Согласен, должен. Однако в вашей декомпозиции есть минус куда более фатальный: протекающая абстракция.


                                                    Вы хотите обставить всё так что мол на сервере OrdersService и на клиенте OrdersService с теми же методами и работают они одинаково, но эта абстракция течёт по той причине что сериализовывать stateful-объекты вы не можете и если таковой возвращает серверный OrdersService, то при попытке вызвать этот метод на клиенте произойдёт ошибка. Уже не говоря о том, что надо помечать все модели DataContract-атрибутами и писать портянку WCF-конфигурации в конфиге, иначе чуда не случится. Плюс разный контракт ошибок: если нет связи с сервером то выкинется соответствующий эксепшон в то время как на сервере такового не может произойти по определению.


                                                    Вы как хотите, я (как уже было сказано) не претендую на создание единственно-верного-пути, но выступаю решительно против протекающих абстракций, полагая их несоизмеримо большим злом нежели неудобства кодогенерации.

                                                      +1
                                                      Вы указали на проблемы так, как будто в вашем подходе тех же самых проблем не будет. Это проблемы в принципе трёхзвенки, и Tecture их не решает.

                                                      1) Модели помечать атрибутами, портянки конфигурации (кстати, в app.config писать не обязательно — можно кодом нагенерить по списку классов/интерфейсов и зарегистрировать). Ну да, если требование — WCF, это нужно, так работает технология.

                                                      2) Сервисы не могут возвращать stateful-объекты, в частности, entities. Это решается вынесением абстракций. Если в сборке-интерфейсе не упоминаются entities, и только она подключена к клиенту, то на клиент entities никак не попадут.
                                                      Но вы же предложили использовать автогенерацию клиентского OrdersService (и все dto к нему тоже дублировать авто-генерацией?). Авто-генерация тупо сделает копии, не разбираясь, что за объект — stateful/stateless.
                                +2

                                Интересный подход. У нас более "развязанная" архитектура (а логирование и прочие штуки можно приделывать к брокеру, через который общаются все модули), а модули, в принципе, могут быть на разных ЯП… У вас больше ориентированно на то, чтобы дать разработчикам рамки, за которые им не рекомендуется выходить, а у нас на то, чтобы разработчик конкретного модуля мог, при желании, сделать в нём почти всё, что угодно (лишь бы "контракт по API" не нарушался).


                                Но у вашего подхода, несомненно, тоже есть плюсы (в нашем немного больше шансов при стрельбе задеть ноги). Пока сходу не придумал, что можно позаимствовать, но буду иметь в виду.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое