Привет, Хабр! Таки да, недавно посчастливилось мне работать в одной фирме в качестве ведущего программиста и после того, что я там видел, я хочу поговорить о том, как желательно организовывать код и файлы и зачем. Ну таки да, прописная истина про то что не желательно папку для одного файла создавать и библиотеку, которую будет использовать только одно приложение и так всем известна. Ну почти всем. В общем, в той фирме, куда я недавно попал, было все — и папки с одним файлом и куча библиотек, которые использует одна единственная программа и код кое-где прямо в контроллерах в огромных методах на кучу строк. На самом деле мне больше всего там не понравилось то, что курили IQOS прямо в офисе. У меня на эту хрень аллергия. Я поговорил с руководством. Мне сказали, что эту проблему решат. Ничего за две недели не поменялось. Фиг с ним с кодом, код бы я бы со временем отрефакторил бы, а вот культуру такую как-то терпеть не хотелось и я свалил.

Так вот, чтобы показать зачем и когда создавать отдельную библиотеку, я создал примеры проектов. За одно и про Hexagonal расскажу и зачем она вообще нужна. Начнем с того что никакая архитектура не сделает ваш код хорошим. Это просто правила про то где что создавать. И да, это все архитектура кода, а не системы. Архитектурой сложной системы обычно заниматься архитектор и там все про то какой сервис за что будет отвечать, какой будет протокол передачи данных, какая база будет использоваться. Архитектурой кода обычно занимается Сеньор или Лид. Ну в простом проекте они еще и работу архитектора берут на себя. Что уж там, в совсем простом еще и админа и аналитика и вообще всех остальных, но речь сейчас не об этом. Так вот не архитектура кода сделает его хорошим, а соблюдение принципов SOLID, YAGNI (который создавая еще одну библиотеку на каждый чих вы не соблюдаете), KISS, CQS, DRY и т.п. Только вот принципы это не шаблоны. С принципами желательно постоянно думать в отличие от шаблонов. Тот же MVC например, усвоил что где создать и дальше все делаешь на автомате не думая. Так же с Hexagonal — Усвоил где создавать Порты, Домены, Адаптеры и дальше делаешь все на автомате, не думая. Это можно сравнить с вещами в комнате. NTier, Message Based, Hexagonal это про то что обувь оставляем в прихожей, а пальто в шкафу на вешалке, а принципы это про то что какие туфли желательно подобрать к платью и то что туфли желательно модные т. е. соблюсти принцип одеваться модно. Архитектура приложения это про то что есть дверь 2 окна и 4 стены. Архитектура кода это про то как замешивать раствор, как кирпичи класть и как дверь делать вообще. Да и должность Архитектора так-то сильно ближе к Аналитику. И так, поговорим про архитектуру кода.

Шаблон CQRS, принцип CQS и архитектура на основе сообщений


Тут такое дело, некоторое путают шаблон CQRS и архитектуру на основе сообщений которую можно реализовать с помощью библиотеки MediatR или с нуля сделать самому, например вот тут я пробовал это сделать CqrsTodo. Так вот, принцип CQS говорит что метод может или изменять состояние вашего приложения (БД, Кеш, Файл или просто поле класса) (Команда) или читать его (Запрос). Архитектура на основе сообщений или на основе событий это про то что у вас есть какой-то посредник или шина который передает между вашими классами сообщениям или события и на их основе они выполняют какие-то действия и генерируют новые сообщения или события.Суть такой архитектуры у вас есть обработки (Handllers) посредники/шина (Mediator/Dispatcher) и DTO которые определяют что нужно сделать (Command/Query/Message/Event). Блягодоря такой архитектуре вы избегаете прямых зависимостей между вашими классами. Все знают только о шине/посреднике/диспетчере и о DTO и больше ни чем с друг другом не связаны и это позволяет сделать архитектуру чистой и расширяемой. Без лишних зависимостей. Шаблон CQRS говорит что в вашем приложении должно быть два потока. Один только на чтение и один за запись. Это приводит к тому что методы на чтение и на запись должны находиться еще и в разных классах т. е. у вас должен быть ReadRepository и WriteRepository. Совсем не обязательно чтобы они вызывались с помощью событий или сообщений. Ну и должно соблюдаться еще и одно важно правило — классы и методы отвечающие за чтение могут вызывать внутри себя только другие классы и методы для чтения.

Например: вот класс который
  1. Является обычным классом Hexagonal архитектуры (Entity)
  2. Реализует принцип разделения ответственности команд и запросов CQS

    class Calculatror
    {
        private double _value;

        public Calculatror(double value)
        {
            _value = value;
        }

        public double Result => _value;

        public void Add(double value)
        {
            _value += value;
        }

        public void Sub(double value)
        {
            _value -= value;
        }
    }
//..............
  var calculator = new Calculatror(0);

  Console.WriteLine(calculator.Result);

  calculator.Add(3);
  calculator.Sub(1);

  Console.WriteLine(calculator. Result);


Пример CQRS классов без хендлеров и диспетчеров. Их можно в Домене вашего приложения использовать.
        class UserByIdQuery
        {
          //...
            public User Get()
            {
                //.....
            }
        }

        class UserDeleteCommand
        {
          //...
            public void Execute()
            {
                //.....
            }
        }

Да, весь ваш домен может состоять только из команд и запросов ну и использовать шаблон CQRS. Как собственно и ваш Порт (UI слой в многослойной архитектуре) может быть MVC, MVP, MVVM и т.д. Ваши Адаптеры (Слой доступа к данным, DAL в многослойной архитектуре) могут реализовать паттерн Repository, UnitOfWork или Active Record.

NTier aka Data Driven aka Многослойная Архитектура


Если в Hexagonal ядро это Домен то в Многослойной Архитектуре ядро это Адаптеры (DAL или Слой Доступа к Данным). В Многослойной у вам может быть намного больше чем 3 слоя. Может быть слой профилирования, слой логирования, слой валидации, слой агрегации, слой авторизации и т.д. Вообще это самая древняя архитектура и ИМХО для примитивного CRUD приложения она подходит лучше всех.


Hexagonal


Домен


По сути это и есть наше приложение. Тут вся наша логика, включая логику валидации т.е. если мы делаем сайт знакомств и нам понадобилось проверить что возраст пользователя больше 18 лет то мы это делаем в домене например с помощью нашего класса Validator. Домен еще называют слоем бизнес логики. Он не знает о реализации его пользователей (Портов) и о реализации того что он использует (Адаптеры). Если сравнить с луковицей, то этот слой это ядро. Он не зависит ни от кого. Он не ссылается ни на кого за исключением стандартной библиотеки. Тут просто голая логика. Тут хранятся объекты нашей бизнес логики (User например). Тут не хотелось бы никакого маппинга. Остальные слои либо используют объекты объявленные в домене либо маппят своим объекты в объекты домена. Тут же хранятся контракты (интерфейсы) для наших адаптеров. Думайте о домене всегда как будто это отдельная библиотека и как будто вы ее можете отдельно опубликовать в Nuget и раздавать людям. Я серьезно, если к домену так относиться то намного проще будет понять что в нем может быть, а что нет. Например вы пишите простенькое приложение калькулятор. Тут ваш домен это класс Calculator ваши объекты домена aka Entity это double (ну или int если у вас только целочисленные операции) и вы еще хотите добавить логирование выполненных операций. Тут вы добавляете в ваш домен интерфейс ILogger и вызываете его в вашем Calculator когда хотите что-то залогировать. Теперь ваш домен можно спокойно опубликовать как библиотеку и каждый сможет реализовать по своему ILogger. Кто-то будет писать в файл, а кто-то в консоль. Ну а вы дальше реализуете свой ILogger в слое Адаптеров. Обычно доменные классы называют менеджер или сервис. Например: UserService, OrderManager ну или просто Calculator, Postman и т.д.

Адаптеры


Слой доступа к данным и состоянию. Зависит от Домена (Ссылается на него) и от любых внешних библиотек. Кеш, БД, сеть, файл, внешняя библиотека, логгер. Все здесь. Зачем? Затем чтобы была возможность это все без крови поменять и для того чтобы наша логика не зависела от внешних реализаций, чтобы было проще расширять, дополнять и изменять. Обычно классы тут называют репозиторий. Например: UserRepository. Если мы в этом слое объявляем какие-то свои сущности, то домену не хотелось бы знать об их существовании. Например если у нас есть UserDto то мы его можем маппить и в домен передавать только User. Весь маппинг происходит тут. В домен передаются только готовые сущности. Да, если у вас и в слое портов тоже есть маппинг то ему желательно быть отдельным и не зависит от этого т.е. у портов желательно чтобы был свой собственный маппер. Для AutoMapper это решается отдельными конфигурациями. У Портов свои у Адаптеров свои, а в Startap вы их объединяете. Если для маппинга нужны какие-то дополнительные данные, то их можно не вычислять, а передавать в качестве параметров. Маппер желательно чтобы занимался только тупым копированием. Если Маппер делает что-то кроме тупого копирования данных, то это уже сразу нарушения S из SOLID. Пример Адаптера это Entity FrameWork он внутри себя маппит Reader в наши классы и изолирует нас от используемой БД.

Порты


Слой представления. Зависит (Ссылается) от Домена и любых других внешних библиотек. Порт это Адаптер наоборот. Если с помощью Адаптеров наш Домен пользуется чем-то внешним, то с помощью Портов кто-то пользуется нашим Доменом. В MVC — Контроллер это наш Порт. В MVP — Презентер это наш Порт и т.д. Как и для Адаптеров если мы в этом слое объявляем какие-то свои сущности, то домену не рекомендуется знать об их существовании. Например если у нас есть UserModel то мы его можем маппить и в домен передавать только User.

Валидация


Есть только одно важное правило: условия валидности должен задавать домен т. е. можно в слое портов и адаптеров создавать приватные классы которые используют эти правила и там же, в этих слоях, выполнять валидацию. Например вы можете в Домене объявить классы которые реализуют паттерн спецификация. В слое Портов (Представления) создать класс валидатор который использует эти спецификации и инициализируют ваш FluentValidator. Можно объявить в Домене IValidationService, а в слое портов обвить атрибут для валидации. В этом атрибуте вызывать ваш IValidationService. Ну а дальше повесить этот атрибут на методы вашего контроллера. Есть еще подход с ValueObject и валидацией в его конструкторе. При таком подходе у вас не валидный Entity не может существовать в принципе.

Главное помните:

  1. Что валидное, а что нет решает Домен
  2. Начать сам процесс валидации может любой слой. Если это не сам домен то порту или адаптеру нужно запросить список правил у домена (например коллекцию спецификаций) или использовать сервис валидации из Домена (IValidationService/Manager) или просто использовать ValueObject из Домена. Тут есть множество способов как это реализовать


ValueObject aka VO


VO это примитивные кирпичики которые хранят в себе наши значения. Пример VO из стандартной библиотеки это Guid и DateTime. Например вот VO для Email в нашем Домене:
        struct Email
        {
            public string Value { get;}
            
            public Email(string email)
            {
                if (email == null)
                    throw new EmailIsNullException();

                if (string.IsNullOrWhiteSpace(email))
                    throw new EmailIsEmptyException();

                var pattern = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$";
                
                if (!Regex.IsMatch(email,pattern))
                    throw new EmailFormatException(email);
                
                Value = email;
            }
        }

        //использование
       class User
       {
           public Email Email {get; set;}
        //....
       }       

Да, пример максимально простой. Лучше TryParse методы иметь а не через конструктор с эксепшенами все делать. Главное — наш VO должен гарантировать что внутри него именно Email а не что-то другое поэтому мы проверяем его режексом.

Организация структуры проекта


Одно приложение




Тут я навал папки Adapters и Posts хотя на самом деле их можно было назвать Repositories и Controllers. Так просто нагляднее. Тут у нас одно единственное приложения поэтому создавать другие проекты и библиотеки не имеет никакого смысла потому что они только раздуют размер проекта и замедлят время сборки. Все что вам нужно можно просто в отдельные папочки уложить. Тут тоже лучше не увлекаться. Папка с одним единственным файлом это не гуд. Эти три папки корневые. Все остальные папки создаются внутри них и на этом уровне лучше больше вообще папок не создавать. Например в папке Adapters можно создать паки Web, DB, Memory и т.д. В папке Domens можно создать по папке для каждого вашего Под — Домена. Например Ordering, Authorization и т.д.

Тут у нас есть три объекта — Item, ItemEntity, ItemModel.

ItemEntity это объекты из нашего Домена. Его желательно чтобы ничего не связывало с используемым фреймворком уровня представления и с используемой БД из уровня доступа к данным поэтому мы в каждом слое создали его близнецов.

    public class ItemEntity
    {
        public string Text { get; set; }
    }

БТВ, классы лучше по умолчанию делать internal sealed этим вы таки соблюдаете YAGNI и повышаете безопасность, скорость работы вашей программы.

Item это объект из слоя Адаптеров. Он нужен потому что нам нужно установить атрибуты [BsonId], [BsonRepresentation(BsonType.ObjectId)] у поля Id при этом домен желательно чтобы не знал о том что мы используем MongoDB поэтому мы создали отдельный класс и маппим его. Так как наша Доменная логика не требует уникального идентификатора для своей работы то в доменном объекте нам поле Id и не требуется. На самом деле много случаев когда все ваше приложения использует только доменные объекты и всем норм потому что на них нет никаких таких специфичных атрибутов характерных для одной БД или еще чего такого. Тут я специально имитировал ситуацию когда кроме доменного Entity нужны еще вспомогательные DTO.

Если вы видите что, для того чтобы изолировать Домен от какой-то внешней зависимости, нужно создать еще одну DTO, то вы ее создаете. Если такой необходимости нет, то не рекомендую плодить лишних сущностей — YAGNI.

    public class Item
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string Id { get; set; }
        
        public string Text { get; set; }

        public Item(ItemEntity entity)
        {
            this.Text = entity.Text;
        }

        // Use AutoMapper for this in big project.
        public ItemEntity ToEntity()
        {
            return new ItemEntity
            {
                Text = this.Text
            };
        }
    }

ItemModel это просто Model для нашего MVC RPC приложения. Тут мы добавили характерный для ASP.NET атрибут [Display(Name = «Текст»)] поэтому нам нужен отдельный объект которого нет в домене и мы его маппим туда и обратно. Домен и Адаптеры желательно чтобы не знали какой слой представления их использует.

    public class ItemModel
    {
        [Display(Name = "Текст")]
        public string Text { get; set; }

        public ItemModel(ItemEntity entity)
        {
            this.Text = entity.Text;
        }

        public ItemEntity ToEntity()
        {
            return new ItemEntity()
            {
                Text = this.Text
            };
        }
    }


Несколько приложений


И тут настал час X. Нам понадобилось для одной и той же доменной логикой создать три приложения — RPC веб сервис — ItemsService, Консольное — ItemsConsoleApp и MVC веб сайт c Razor шаблонами — ItemsWebApp. Причем веб сервис у нас использует MongoDB, а консольное приложения и веб сайт используют эмбедед БД LiteDB. Ну вот случился в нашей жизни такой внезапный поворот. Мы распилили наше предыдущее приложение на библиотеки и вот что получилось.



Domain.dll


Так как все 3 наших приложения используют одну, и ту же доменную логику то мы вынесли ее в отдельную библиотеку. Тут все наши доменные объекты, сервисы и интерфейсы (контракты) для адаптеров.

Adapters.LiteDB.dll


Так как у нас 2 приложения используют одинаковые адаптеры (для LiteDB) то мы вынесли их в отдельную библиотеку. Только одно наше приложение (ItemsService) использует Адаптеры для MongoDB поэтому они остались в папке Adapters этого приложения.

Ссылки


  1. Проект с одним приложением
  2. Проект с несколькими приложениями

Комментарии (69)


  1. Ascar
    25.04.2019 12:43

    Посмотрел проект там обычный репозиторий, cqrs не нашел.


    1. VanquisherWinbringer Автор
      25.04.2019 23:57

      Хм, так этот репозиторий соблюдает принцип CQRS (в личку мне написали что сам принцип называется CQS). Там всего два метода. Один только изменяет данные а другой только читает.


      1. Ascar
        26.04.2019 10:21
        -1

        По ваше логике любой репозиторий типа crud, где что то пишет, а что то читает — уже cqrs.


        1. VanquisherWinbringer Автор
          26.04.2019 10:26
          -2

          Если в нем одни методы только пишут а другие только читают данные то почему вы думаете что такой репозиторий нарушает принцып Разделения Ответсвенности Комманд (запись) и Запросов (чтение)? И так для справки — не надо путать архитектуру и принцып. Я про принцип говорю. В архитектуре да, там отдельный репозиторий для чтения и отдельный для записи.


          1. ghost404
            26.04.2019 10:47
            +1

            Зачем тогда вы используете термин CQRS подразумевая CQS?


            1. VanquisherWinbringer Автор
              26.04.2019 10:53
              -1

              Ну википедия говорит что это одно и тоже — ru.wikipedia.org/wiki/CQRS Я тоже считаю что по факту это одно и тоже. Хотите холивар на эту тему начать? Хотя таки да CQRS это шаблон CQS это принцып. Но суть у них одинакова и говорят они об одном и том же. CQS говорит что метод должен или читать или изменять. CQRS еще добавляет что методы на чтение и на запись должны быть в разных классах. На этом и все различие.


              1. ghost404
                26.04.2019 11:11

                Да, говорят, в целом, они об одном и том же, но они не реализуются одним классом как у вас. CQRS это архитектурный подход на макроуровне который говорит о том, что у нас есть как минимум 2 класса, а как максимум 2 хранилища.
                image


                1. Ascar
                  26.04.2019 11:19
                  -1

                  Я бы сказал как минимум 4 базовых класса, 2 команды и запросы, 2 под их обработчики. Хранилищ думаю тоже можно сделать больше, например бд на редактирование реплицирует в несколько на чтение.


                  1. ghost404
                    26.04.2019 11:46
                    +1

                    Я говорю о минимальных требования. В минимальных требованиях CQRS не требует от вас создавать CommandHandler. Да, безусловно без CommandHandler-ов и хотя бы дюжины команд и запросов вы не получите профита от CQRS. CQRS вообще выгоден только на больших проектах. На малых проектах он создает ненужное переусложнение и оверхед.
                    Но минимальные требования это 2 класса.


                    1. Ascar
                      26.04.2019 11:54
                      -1

                      Минимальное это приемлемое для реализации, 2 класса это не приемлемо для реализации, как вы сами говорите, на большом проекте.


                    1. VanquisherWinbringer Автор
                      26.04.2019 12:49
                      -1

                      Я правильно понимаю что для ASP.NET, два минимальных класса это CommandController.cs и QueryController.cs? Потому что контроллеры это тоже ваш код и тоже часть вашего приложения и как следствие часть вашей архитектуры. Иначе мне не понятно как соблюсти СQRS во всем приложении.


                      1. TimurNes
                        26.04.2019 14:10
                        -1

                        Вот репозиторий с правильно реализованным архитектурным паттерном CQRS в asp.net core проекте, в рамках курса CQRS in Practice


                        1. VanquisherWinbringer Автор
                          26.04.2019 14:38
                          -1

                          Блин, вот это вот заблуждение я хотел в статье развеять. CQRS это блин, как вы и пишите «архитектурный паттерн» такой же как  MVC, MVP, MVVM — это не архитектура приложения. Вот об этом я хотел сказать. Вот архитектура может быть Hexagonal, NTier, Event(Message) Based и т. д.


                          1. TimurNes
                            26.04.2019 15:20

                            Это был ответ на

                            Иначе мне не понятно как соблюсти СQRS во всем приложении.
                            Я предоставил вам пример, как можно соблюсть CQRS во всем приложении


                            1. VanquisherWinbringer Автор
                              26.04.2019 21:31

                              Ну я сам такое приложения писал года 2 назад github.com/VictoremWinbringer/CqrsTodo только и тут у меня и там в примере который вы мне скинули не соблюдается шаблон CQRS повсеметно. Начнем с того что и у меня и у него контроллер не CQRS. На этом можно собственно и закончить. Еще добавлю что это вообще очень простые и примитивные проекты. В большом и сложном у вас будет очень много месте где надо и читать и писать сразу. Да еще и в пределах одной транзакции.


                        1. VanquisherWinbringer Автор
                          26.04.2019 14:56

                          del. не туда ответил.


                      1. ghost404
                        26.04.2019 14:12

                        Нет. СQRS отдельно, а Controller отдельно. Я говорю о том, что для реализации CQRS нужно минимум 2 класса — DoSomethingCommand и GetSomethingQuery. Для удобной обработки запросов и команд логично сделать DoSomethingCommandHandler и GetSomethingQueryHandler, но это не обязательно. СQRS от вас этого не требует. Хендлить команды вы можете и в Controller и в Front Controller.
                        Я не сказал, что все приложение должно состоять только из 2 классов. Я сказал, что реализации СQRS в приложении может состоять из 2 классов.


                        1. VanquisherWinbringer Автор
                          26.04.2019 14:32
                          -2

                          Нет. СQRS отдельно, а Controller отдельно.

                          Только истинный Шотландец…
                          Если ваш контроллер не следует  шаблону CQRS или не соблюдает принцы CQS то нельзя сказать что все ваше приложение его соблюдает или ему следует потому что контроллер или презентер или еще что-то там это часть вашего приложения. Вы соблюдаете это только локально. В каких то отдельных участках кода. Да и вообще, Handler это не про CQRS, Handler это про артектуру на основе сообщений/событий и они могут быть не только командами/запросами а вообще чем угодно. Не надо путать шаблон, архитектуру и принцы. CQRS может быть и без всяких Handler или что-то в этом роде. Обработчиков реагирующих на сообщения или события. Да и Handler может быть отдельно от СQRS например реагировать на CreateUserAndReturnCreatedIdMessage что ни разу не CQRS зато вполне себе часть Архитектуры на основе сообщений/событий.
                          Просто запомните: Если у вас есть Хендлеры которые реагирую на приходящие в них команды или запросы (что по сути есть сообщения) То у вас архитектура на основе сообщения которая реализует паттерн CQRS и соблюдает принцып CQS


                          1. ghost404
                            26.04.2019 17:08

                            Не надо путать шаблон, архитектуру и принцы.

                            Это вы путаете архитектурный шаблон с принципом.


                            Да и вообще, Handler это не про CQRS, Handler это про артектуру на основе сообщений/событий и они могут быть не только командами/запросами а вообще чем угодно.

                            Вы путаете все. Я не говорил, что Handler == CQRS. Handler — это обработчик чего-то. Это что-то можем быть командой, запросом, событием, сообщением, да всем чем угодно.


                            А теперь о семантике. Мне странно, что вы говорите о CQRS и не знаете таких базовых вещей.


                            • Событие — возникает в результате какого-то изменения данных в приложении. Обработчик события может стригерить Команду на изменение других данных. У события может быть несколько слушателей. Событие не возвращает никаких данных.
                            • Команда — действие направленное на изменение данных в приложении. В результате изменения данных может быть стригерено Событие. У команды только один получатель. Команда не возвращает никаких данных.
                            • Запрос — направлено на получение/извлечение данных из приложения. Запрос не меняет ничего в приложении. У запроса только один получатель.
                            • Сообщение — направлено на выполнение чего-то в приложении. Часто реализуется как симбиоз Команды и Запроса. У сообщения только один получатель. Из-за неоднозначности сообщений, это, по большому счету, антипаттерн.

                            Есть еще События доменного слоя. Не стоит их путать с событиями приложения. Они часто реализуются теми же инструментами, но имеют совсем другое значение.


                            Итого: Event Based != Message Based != CQRS


                            Рекомендую почитать Фаулера или Янга.


                            1. VanquisherWinbringer Автор
                              26.04.2019 20:54
                              -2

                              Блаа блаа блаа. Слова слова слова. Да вы можете их хоть Class1, Class2 и т. д. назвать. Хотя делать так конечно не стоит. Суть такой архитектуры у вас есть обработки (Handllers) посредники/шина (Mediator/Dispatcher) и DTO которые определяют что нужно сделать (Command/Query/Message/Event) и все. Как это называется это дело десятое. Блягодоря такой архитектуре вы избегаете прямых зависимостей между вашими классами. Все знают только о шине и о DTO и больше ни чем с друг другом не связаны и это позволяет сделать архитектуру чистой и расширяемой. Без лишних зависимостей. Вот в чем суть такой архитектуры как ее не называй.


                            1. VanquisherWinbringer Автор
                              26.04.2019 21:02
                              -2

                              БТВ, Сам Фаулер называет CQRS паттерном а не ахтитектурой. Это «архитектурный паттерн» а не архитектура. Вы можете и в Hexagonal архитектуре использовать этот паттерн если хотите.


                            1. VanquisherWinbringer Автор
                              26.04.2019 22:18

                              БТВ, вот тут у меня github.com/VictoremWinbringer/CqrsTodo используются архитектурные паттерны MVC и CQRS. Используется архитектура на основе сообщений. Это все одновременно в одном проекте живет вместе.


          1. Ascar
            26.04.2019 11:04
            -1

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


            1. VanquisherWinbringer Автор
              26.04.2019 11:20
              -2

              Ой таки вей. Соблюсти принцип в рамках всего проекта это такое себе. Его хотяб локально то соблюсти. Что вы будете делать если вам надо будет посчитать сколько раз вызывали какой то запрос? Да тот же пример со стеком. CQS самый сложно реализуемый принцип а SQRS и архитектура на его основе это утопия.


              1. Ascar
                26.04.2019 16:13

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

                сложно реализуемый принцип а SQRS и архитектура на его основе это утопия


                1. VanquisherWinbringer Автор
                  26.04.2019 20:49

                  Приведите пример как вы будете решать проблему со стеком и проблему когда нам необходимо посчитать количество вызовов какого то запроса. Меньше слов, больше дела. Ну и совсем стандартный кейс — айди созданного объекта который сгенерировала БД.


                  1. Ascar
                    26.04.2019 21:26
                    -1

                    Вы опишите в чем ваша проблема. Какие стеки? Какие запросы?
                    У меня появилось ощущение что вы тут просто всех троллите, так что идите ка и разбирайтесь сами со своим проблемным стеком.

                    Меньше слов, больше дела.


                    1. VanquisherWinbringer Автор
                      26.04.2019 21:28

                      Понятно все с вами


                      1. Ascar
                        26.04.2019 21:42

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


                        1. VanquisherWinbringer Автор
                          26.04.2019 23:34

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

                          class A
                          {
                          //...
                          public int Execute()
                          {
                          //Тут мы читаем значение из БД
                          //Умножаем его на 2
                          // и возвращаем его из метода
                          }
                          }
                          


                          1. Ascar
                            27.04.2019 10:32

                            Да вам уже сто раз написали про команды и запросы, а то что вы привели это ни то и не другое, так они не строятся. Абсолютно никакого понимания о предметной области. Да команда возвращает тип, и да они схожи с запросами числу параметров, и что теперь? Вы прочитаете пару слов где то что то погуглив и начинаете что то доказывать. Так Выглядят интерфесы хендлеров команд и запросов:

                                public interface IQueryHandler<in TQuery, out TResponse>         
                                    where TQuery : IQuery<TResponse>
                                {
                                    TResponse Get(TQuery query);
                                }
                            
                                public interface ICommandHandler<in TCommand, out TResult>       
                                    where TCommand : ICommand<TResult>
                                {
                                    TResult Execute(TCommand command);
                                }
                            
                            public interface IQuery<out TResponse> { }
                            public interface ICommand<out TResult> { }
                            


                            1. VanquisherWinbringer Автор
                              27.04.2019 10:42
                              -1

                              Это у вас абсолют нет понимания предметной области. То что вы провели это лишь один из множества способов реализации а не единственно верный. Шире мыслить надо. Расширяйте свой кругозор, читайте книги, думайте в конце концов больше. Не надо так узко мыслить. Ладно, думайте что хотите. Мне плевать. Конкретных ответов на мои вопросы вы не смогли дать. Я только зря на вас время трачу да еще и тут все комменты зафлудили.


                              1. Ascar
                                27.04.2019 11:16

                                Это у вас абсолют нет понимания предметной области.

                                Перечислите мои абсолютные неточности в понимании?

                                То что вы провели это лишь один из множества способов реализации а не единственно верный.

                                Приведите свое, и что то получше этого:
                                class A
                                {
                                //…
                                public int Execute()


                                Шире мыслить надо. Расширяйте свой кругозор, читайте книги, думайте в конце концов больше. Не надо так узко мыслить.

                                Аргументы хоть какие то будут?
                                • Что я не учел?
                                • Что я не верно описал?


                                Мне плевать. Конкретных ответов на мои вопросы вы не смогли дать.

                                На эти вопросы то?:
                                это запрос который изменяет состояние или команда которая возвращает значение?

                                Сами указали несуразный класс и просите что то сказать по его поводу, ну не абсурдно ли? Я вам написал как должно быть.

                                И если команды могут возвращать значения то зачем вам вообще запросы?

                                Действительно! Зачем вообще нужно это все если можно в репозиторий захардкодить методами, назвать это cqrs и радоваться тому как он растет! Вам уже привели где то выше даже картинку, зачем надо разделять, даже написали про разные базы, что команда изменяет состояние, а запрос — нет.


                            1. VanquisherWinbringer Автор
                              27.04.2019 10:47
                              -1

                              Подсказка:
                              У вас есть три типажа
                              1) Те кто возвращают значение и не изменяют состояния (Запросы)
                              2) Те кто не возвращают значение и изменяют состояние (Команды)
                              3) Те кто делают и то и другое. И возвращают значение и изменяют сотояние (Не пойми кто, и вы вместо того чтобы этому третьему типажу дать отдельное имя называете его Коммандой хотя также можно было и Запросом или еще как назвать)
                              Это все равно что если бы у меня в Haskell были функции
                              1) *->*
                              2)*->*->*
                              3)*->*->*->
                              и я сказала бы что у 2 и 3 одинаковый тип. Вы даже сами то не понимаете что говорите. Просто прочитали где-то и теперь считаете это прописной истиной.


                              1. Ascar
                                27.04.2019 11:22

                                3) Те кто делают и то и другое. И возвращают значение и изменяют сотояние (Не пойми кто, и вы вместо того чтобы этому третьему типажу дать отдельное имя называете его Коммандой хотя также можно было и Запросом или еще как назвать)

                                Ну вот опять все в кучу. Тут вы выполняете команду, а потом уже запрос. s — означает сегрегация, разделение.

                                Это все равно что если бы у меня в Haskell были функции

                                Выучите хотя бы один язык, но хорошо.

                                прочитали где-то и теперь считаете это прописной истиной.

                                Я хотя бы прочитал


                1. VanquisherWinbringer Автор
                  26.04.2019 20:57

                  Так да — это вы совсем не понимаете «архитектурный паттерн» CQRS поэтому несете не пойми что. Так для справки — Hexagonal Архитектура тоже может быть CQRS. Делаете ReadAdapter, WriteAdapter, OrderCreator, OrderReader и т. д. Вот и все.


                1. VanquisherWinbringer Автор
                  26.04.2019 22:24

                  Я вам даже больше скажу. У вас в одном проекте может жить одновременно
                  1) Архитектурный паттерн Repository и UnitOfWork
                  2) Архитектурный паттерн CQRS
                  3) Архитектурный паттерн MVC
                  и при этом у вас может быть стандартная многослойная архитектура (NTier)


    1. VanquisherWinbringer Автор
      27.04.2019 11:04

      Вообще все непонимание началось с того что я случайно написал CQRS вместо CQS. И да, CQS то он соблюдает а CQRS нет. Таки да, все же я виноватом в начавшемся холиваре. Статья вообще то про Hexagonal и про то что лишние библиотеки и папочки создавать не надо. Я CQRS не люблю потому что гигантское количество мусора из — за него. Тонны мелких классов и файлов создаться. Я его просто в скользь упомянул, как и NTier и MessageBased чтобы читатель знал что кроме Hexagonal есть еще и такие вещи. А в результате набежали вы и ghost404 и теперь все комментарии к статье зафлужены CQRS хотя к самой статье он то имеет только косвенное отношение.


  1. NYMEZIDE
    25.04.2019 12:48

    Домен… Тут вся наша логика, включая логику валидации


    а если валидация сложная и требуется использовать библиотеку дял валидации? например FluentValidation?

    ее тащить в Домен нельзя, это зависимость.
    делать IValidator в домене, а реализацию FluentValidator: IValidator, помещать
    вместе с зависимостью в адаптерную часть или на уровень выше?


    1. TimurNes
      25.04.2019 15:36

      Я валидацию делаю в Presentation layer, а домен использует только Value Objects, которые нельзя создать «неправильными».
      Т.е. у VO есть статический метод, пусть будет IsValid(value), который позволяет проверить правильность пришедших «извне» данных (в dto например), а в конструкторе — эта же проверка, но с исключением, если что то не так.
      Все это довольно неплохо интегрируется во FluentValidation в presentation layer.
      Т.е. правила создания обьекта (например регулярка или ограничение по длине, любые правила) задаются в домене, но проверяются во внешних слоях приложения. Невалидные данные в домене в принципе не смогут появится


      1. NYMEZIDE
        25.04.2019 15:56

        Presentation layer делает свою валидацию, используя FluentValidation,
        затем вы передаете-мапите в VO, который имеет IsValid и который дергается в Домене? или там же в Presentation layer до того момента, как уйти в Домен?

        «Невалидные данные в домене в принципе не смогут появится» смотря как реализовано.
        Если Домен не дергает IsValid — то ничто не помешать забыть про проверку в Presentation layer и передать туда ему «кривую» VO


        1. TimurNes
          25.04.2019 16:07

          Presentation layer использует IsValid, иногда с некоторыми обвязками, к примеру — поле необязательное. FluentValidation позволяет это сделать более прозрачным и упрощает контроллеры.
          Домен работает только с VO, по этому в presentation создается VO и передается дальше в домен.
          А у самого VO в конструкторе, при создании, проверяется валидность через тот же IsValid и, если false, выбрасывается исключение

          Примерно так:

          public PhoneNumber(String value) {
          	if (!IsValid(value)) {
          		throw new ArgumentException("Phone number is not valid");
          	}
          
          	Value = value;
          }


          1. NYMEZIDE
            25.04.2019 18:02

            PhoneNumber — это VO? а как выглядит тогда сложный объект с 3-4 свойствами?

            и почему в IsValid передается value? что за бред?

            выглядит не очень уже, не говоря уже об реализации IsValid.


            1. TimurNes
              25.04.2019 19:06

              Ок, расширю пример.
              Можете пояснить, что не так с передачей value в IsValid, где тут бред и почему?

              PhoneNumber
              public class PhoneNumber {
                  public PhoneNumber(string value) {
                      if (!IsValid(value)) {
                          throw new ArgumentException("Phone number is not valid");
                      }
              
                      Value = value;
                  }
              
                  public string Value { get; }
              
                  public static bool IsValid(string value) {
                      // Validation logic like RegEx
                      return true/false;
                  }
              }

              FluentValidation:
              public class UserViewModelValidator : AbstractValidator<UserViewModel> {
                  public UserViewModelValidator() {
                      ...
                      RuleFor(m => m.PhoneNumber)
                          .Must(p => PhoneNumber.IsValid(p))
                          .WithMessage("Invalid phone number");
                      ...
                  }
              }


              Использование в сферическом сервисе:
              public class OTPService {
                  private readonly IExternalSmsSender _externalSmsSender;
              
                  public OTPService(IExternalSmsSender externalSmsSender) {
                      _externalSmsSender = externalSmsSender;
                  }
              
                  public void VerifyByOneTimePassword(PhoneNumber phoneNumber) {
                      ...
                      var generatedOneTimePassword = "qwerty";
                      _externalSmsSender.SendOneTimePassword(
                          phoneNumber.Value, 
                          generatedOneTimePassword);
                      ...
                  }
              }


              1. NYMEZIDE
                25.04.2019 19:23

                это оверхед.

                а создание

                new PersonalName(new Name("str1"), new Name("str2"))
                — это финиш!

                а если надо передать int, DateTime и т.д. вы будете оборачивать и их?

                т.е. в сумму у вас есть
                DTO — на входе в метод (валидируется стандартными средствами или нет?)
                -> VO + несколько классов на каждый параметр и тип.
                -> DomainEntry/Agreegate — чтобы сюда попасть.

                return Name.IsValid(firstName) && Name.IsValid(lastName);


                а если надо валидировать пару параметров? во взаимосвязи? если первый равен 1 — то второй валидируется по одному правилу, если 2 — то по другому. тогда как вы поступите?

                И почему вообще VO хранит логику??? VO — это просто контейнер для данных.
                Валидация FluentValidation стала зависеть внезапно от того, как внутри устроен IsValid — что он принимает и т.д. Это бред!

                у вас FluentValidation (целая зависимость, которая не маленькая по размеру) прикручена к проекту тупо чтобы она дергала метод IsValid — где находится реальная валидация. Вам не кажется это странным?

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


                1. TimurNes
                  25.04.2019 19:56

                  Покажите ваш вариант, с удовольствием ознакомлюсь.

                  а если надо передать int, DateTime и т.д. вы будете оборачивать и их?

                  VO вполне может состоять из нескольких других VO. Все зависит от контекста. Допустим, в контексте приложения есть Address как VO. В него могут входить Country, City, ZipCode, AddressLine1, AddressLine2, и я считаю это нормальным. Но этот самый Address в PersonalName не имеет никакого смысла, даже если условный пользователь в своем профиле имеет и имя, и адрес.

                  а если надо валидировать пару параметров? во взаимосвязи? если первый равен 1 — то второй валидируется по одному правилу, если 2 — то по другому. тогда как вы поступите?

                  Мой пример выше — кардинально упрощен, чтоб не раздувать ответ. Если нужна сложная логика, вот как вы написали — именно она и будет реализована в IsValid.

                  И почему вообще VO хранит логику??? VO — это просто контейнер для данных.

                  Абсолютно согласен, что VO — это просто контейнер для данных.
                  По поводу логики — дополню. Исключительно логика правил создания объекта. Лежит в одном месте — если нужно что то поменять (например, разрешенная длина строки увеличиласть или регулярка изменилась) — поменять легко и просто.
                  А теперь представьте, что условный телефон передается из десятка вьюх в составе десятка разных ViewModel, для каждой из которых — свой отдельный валидатор. Во первых, копипаста одинаковых правил (которые могут быть сложные, как из предыдущей цитаты). Во вторых, в случае изменения правил придется поменять весь десяток валидаторов. Где то забыли — привет, баг!

                  Вот неплохая статья: Validation and DDD. Перечислено несколько вариантов валидации, в том числе чистый FluentValidation и вариант, на который отдаленно походит мой IsValid.
                  Очень рекомендую ознакомится с ресурсом — много полезных и интересных статей


                1. ookami_kb
                  26.04.2019 09:51
                  +3

                  И почему вообще VO хранит логику??? VO — это просто контейнер для данных.

                  Вообще, хранить логику в VO – вполне себе нормально (более того, это даже может быть предпочтительно), если это именно доменный Value Object. Контейнер для данных – это DTO.


        1. oxidmod
          26.04.2019 11:24
          +2

          VO не может быть кривой. В этом то смысл заворачивания всех примитивов в VO.
          Тобишь age это не интеджер, а VO, которая не соберется, если передать использовать невалидное значение из презентейшена. Что такое валидное значение? А это уже зависит от вашей бизнес-логики (18+ для дейтинга к примеру).


          1. VanquisherWinbringer Автор
            26.04.2019 11:41
            -1

            ИМХО VO c валидацией в конструкторе это  лучший путь для такой валидации вроде проверки на null  или еще чего примитовного в таком роде но такие вещи вроде 18+ (это должно из кончиков браться а значит это должен поставлять адаптер конфига) или проверка уникальности email пользователя (а для этого надо обратиться к адаптеру БД или кеша) лучше делать в ValidationService потому что инжектить что-то в VO это такое себе. Ну или можно еще конечно в конструктор  VO передавать необходимые данные. Например.

                    struct AgeVO
                    {
                        public int Value { get;}
            
                        public AgeVO(int age,int minAge)
                        {
                            if (age < minAge)
                                throw new AgeToLowException(age, minAge);
                            Value = age;
                        }
                    }
            
                    struct UserEmailVO
                    {
                        public string Value { get;}
            
                        public UserEmail(string email, int countEmailsLikeThisInDb)
                        {
                            if (countEmailsLikeThisInDb > 0)
                                throw new NotUniqueEmailException(email, countEmailsLikeThisInDb);
                            Value = email;
                        }
                    }
            


            1. oxidmod
              26.04.2019 13:02

              Не согласен с вами.
              1. Валидировать ВО должно себя только от данных.
              UserEmail должен быть валидным email, но валидный email ничего не говорит о том, если такой email в системе или нет. Это валидация немного другого порядка.
              if (userRepository.containsUserWithEmail(email)) {
              throw new UserEmailNotUniqueException(email);
              }


              2. Передавать что-то дополнительно в конструктор нет смысла.
              new UserEmail('some@example.com', 0)
              Вот уже не работает ваш подход


              1. VanquisherWinbringer Автор
                26.04.2019 13:17
                -1

                1) Я вообще предпочитаю Сервис валидации в котором вызывается.

                if (userRepository.containsUserWithEmail(email)) {
                throw new UserEmailNotUniqueException(email);
                }
                а ВО не использую для сложной валидацию. Только проверка на null или Empty ну и в этом духе, не требующее дополнительных данных.
                2) Так не надо делать. Надо вот так —
                new UserEmail(email, userRepository.countUserWithEmail(email))

                ну или
                new UserAge(age,configRepo.MinUserAge());

                так или иначе — тогда откуда вы собрались брать данные для валидации? Хардкодить 18?
                Да и вообще, я сам против подхода со сложной валидацией в ВО. Лучше класса валидатор для этого создать а в ВО банальная проверка на null.


                1. oxidmod
                  26.04.2019 13:54

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


                  1. VanquisherWinbringer Автор
                    26.04.2019 14:49

                    Мне как то не нравиться идея зашивать в коде что-то кроме в принципе не изменяемых констант. Ну вроде PI или скорости света в вакууме или силы земного тяготения. Да и то сомнительно.


                  1. VanquisherWinbringer Автор
                    26.04.2019 14:59

                    Я вообще все конфиги предпочитаю хранить в LiteDB и делать простенькую админку для них. Чтобы если в друг закон или правила сайта поменяются Админ мог зайти и поменять 18 на 16 сам в ручную и все.


                  1. ghost404
                    26.04.2019 15:51

                    Есть и другой подход. В ВО передавать не значение MinUserAge, а передавать сервис который возвращает это значение:


                    new UserEmail(email, userRepository)

                    Это обезопасит нас от подобных проблем:


                    new UserEmail('some@example.com', 0)

                    Вообще валидация это очень холиврная тема и нет лучшего решения для нее.
                    Сущность и ВО всегда должны быть валидными и поэтому валидация должна быть в них. Но иногда валидация требует обращения к внешним сервсиам и в этом случае сервисы должны передаваться в Сущность/ВО в качестве зависимости.
                    Тут мы сталкиваемся с проблемой того, что таких зависимостей может быть много, и если у нас большой агрегат, может потребоваться пробрасывать зависимости для вложенных объектов.
                    В связи с тем, что сущности не могут быть не валидными, в случае если приходят невалидные данные, сущность должна бросить исключение. И тут мы плавно подходим ко второй проблеме.
                    Безусловно, мы можем перехватить исключение и отобразить пользователи сообщение об ошибке, но если невалидных полей несколько, то придется несколько раз повторять процесс отправки, получения и валидации запроса. Мы не можем вывести сразу все ошибки. Неплохим решением этой проблемы будет метод IsValid предложенный TimurNes.
                    Но это еще не все. Еще есть проблема с тем, что в некоторых случаях нам нужно знать о некоторых правилах за пределами домена. Например, в Web мы можем ограничить длину поля ввода на уровне представления (атрибут maxlength), формат данных (атрибут pattern) и т.д. Можем делать предварительную валидацию на JavaScript, например идентичность полей пароля и подтверждения пароля при его смене.
                    Отсюда мы приходим к тому, что держать всю валидацию только в домене не выгодно.
                    Я считаю более правильным (не лучший) подход с разделением валидации на слои.


                    • Простейшая валидацию в frontend для удобного UI.
                    • На основе пользовательских запросов составлять DTO и выполнять корректность введенных данных. Тип, формат, длинна и т.д.
                    • DTO передается в сервисы которые выполняют проверку бизнес требований требующих обращения к зависимостям. Например, есть ли пользователь с таким email.
                    • Сервисы собирают сущности из DTO и в сущностях выполняется валидация оставшейся бизнес логики.

                    В этом подходе Сущности и ВО проверяют только бизнес правила. Повторная проверка вводных данных усложняет код и негативно скажется на производительности. Этот подход требует составления регламента разработки кода и четкого следования ему. Есть утилиты которые позволяют следить за соблюдением регламента.
                    Недостаток этого подхода конечно в размазывании валидации, но как я сказал выше, это вынужденная мера.


                    1. oxidmod
                      26.04.2019 16:38
                      +1

                      Есть и другой подход. В ВО передавать не значение MinUserAge, а передавать сервис который возвращает это значение:

                      Что мне мешает передать туда свой фейковый сервис? Это та же проблема.
                      Не нужно всю валидацию вносить в ВО. Что вообще такое ВО в домене?
                      Это минимальный кирпичик домена. Он всегда валидный, и от агрегата отличается тем, что у него нет идентичности. Тот же UserEmail не может быть валидным или невалидным в зависимости от состояния бд.
                      Честно говоря, я бы даже не называл его UserEmail, это просто Email. Его цель — предотвратить попадание мусора в домен. Чтобы не приходилось везде, где мы хотим воспользоваться этой строкой, не нужно было проверять, а валидная ли это строка.
                      Дальше, в юзкейсе регистрации есть требование об уникальности мыла. Но это требование не к мылу, это требование к процессу регистрации, потому контролировать его должен отвественный за регистрацию. Мыло отвечает за регистрацию? А если потом появится рега по телефону или через социалочки?


                      1. ghost404
                        26.04.2019 17:11

                        Собственно и я о том же


                      1. VanquisherWinbringer Автор
                        26.04.2019 22:45

                        Спасибо за полезный комментарий. Хочу заметить что так-то Guid и DateTime из стандартной библиотеки это так-то готовые ВО. Блин, я статью писал про Hexagonal архитектуру и про то что не надо создавать библиотеки если у вас одно приложение и не надо создавать по папке на каждый чих. Решил вскользь упомянуть чертов CQRS и тут набежало его фанатов и зафлудило тут все в результате ваш действительно полезный комментарий находиться в самом низу и его будет сложно найти тем кто зайдет статью посмотреть а вверху тоны флуда про CQRS который к статье имеет толко косвенное отношение.


    1. VanquisherWinbringer Автор
      25.04.2019 21:23
      +1

      Значит так — есть только одно важное правило: условия валидности должен задавать домен т. е. можно в слое портов и адаптеров создавать приватные классы которые используют эти правила и там же, в этих слоях, выполнять валидацию. Например вы можете в Домене объявить классы которые реализуют паттерн спецификация а в слое Портов (Представления) создать класс валидатор который использует эти спецификации и инициализируют ваш  FluentValidator. Да и вообще, так то FluentValidation это библиотека для валидации на уровне представления т. е. чтобы валидировать Модель вашего Контроллера поэтому не Домен не Адаптеры не должны в принципе знать о их существования ну если вам очень хочется ее использовать то можете создать класса обертку ValidationAdapter который только вызывает методы из FluentValidation и сам ничего не делает. Дальше создаете ValidationService  в Домене и там уже в конструкторе вызываете

      public ValidationService(IValidationAdapter validationAdapter)
      {
      _validationAdapter = validationAdapter;
       ValidationAdapter.RuleFor(x=>x.y)//...

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

      1. VO лучше делать структурой потому что обычно он без наследования и его не надо передавать по ссылке потому что он не изменяем
      2. Это так себе:
        throw new ArgumentException("Phone number is not valid");

        Лучше делать так:
        throw new VoPhoneNamberIsNullException();
        //или
        throw new VoPhonewNuberLengthIsNotValidException(minLength,maxLength,currentLength)


      1. TimurNes
        25.04.2019 21:43
        +1

        VO лучше делать структурой потому что обычно он без наследования и его не надо передавать по ссылке потому что он не изменяем

        С одной стороны — да. Но может появится нюанс маппинга VO для ORM. Например, в Entity Framework Core для Owned Types нужен класс, по крайней мере в официальной документации указан именно он. Лично я структуру не пробовал маппить. С Value Conversions структура теоретически должна подойти. Второй нюанс — тестирование. Если VO — структура, а нам надо ее замокать, то сделать что то подобное не получится: var voMock = new Mock<SomeStruct>();

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


        1. VanquisherWinbringer Автор
          25.04.2019 22:03

          Можно для ORM создать DTO с простыми свойствами public string FirstName и т.д. и в него или из него маппить нашу Entity. Вот с тестированием это да. Я лично предпочитаю использовать VaidationService хотя согласен что VO хорош тем что с ним Домен в принципе не может быть в не валидном состоянии. Просто с ValidationService проще задавать сложные правила а с VO проще соблюдать любые правила. Оба подхода имеют право на жизнь.


  1. build_your_web
    26.04.2019 09:44

    ИМХО, терминология хексагональной архитектуры неудачная.
    Устоявшиеся «Domain — Services — Presentation» более интуитивно понятны.


    1. VanquisherWinbringer Автор
      26.04.2019 10:22

      Это да, такое название больше объясняет суть. Хотят тут есть одно но — так как я ASP.NET пользуюсь то у меня нет Presenters и скорее «Domain — Adapters — Controllers»


  1. ghost404
    26.04.2019 10:59

    Например вот класс который… обычным классом Гексагональной DDD… архитектуре на основе сообщения… разделения ответственности команд и запросов CQRS

    Это шедеврально. Так красиво намешать в одну кучу столько несовместимых понятий. Это потрясающе. Аплодирую стоя.


    1. VanquisherWinbringer Автор
      26.04.2019 11:03
      -2

      Почему? Этот класс Entity? Да Entity. Этот класс соблюдает CQS? Да соблюдает? Этот класс генерирует события/сообщения или обрабатывает события/сообщения? Нет.


      1. ghost404
        26.04.2019 11:38
        +1

        Тут целая мешанина из всего.


        • Domain-driven design (DDD) — это о бизнеслогике, Ubiquitous Language и Bounded Context. Не о программировании.
        • Гексагональной — это архитектурный шаблон. Это шаблон об организации совокупности классов. Один единственный класс не может характеризовать архитектуру приложения. Так, что формулировка обычным классом Гексагональной по меньшей мере не корректна.

        Сочетание этих двух понятий в Гексагональной DDD… удивляет.


        • Архитектуре на основе сообщений — тоже является архитектурным подходом и один единственный класс ни как не может характеризовать ее.
        • CQRS — так же архитектурный шаблон. Как писал выше, это о разделении приложения на 2 потока. Поток чтения и поток записи. Один класс не может и не должен (это нарушения CQRS) реализовывать одновременно запрос и команду.

        А самое главное это пример кода в котором прослеживается CQS (не CQRS) и я бы скорей сказал, что это обычная бизнес логика реализация которой не нарушает SRP.


        1. VanquisherWinbringer Автор
          26.04.2019 11:46

          Оу, теперь я понял что вы хотели сказать. :) Таки да, согласен. Я просто хотел показать пример класса с  CQS и хотел сказать что CQS это не обязательно CQRS или Архитектуре на основе сообщений. Что он может отдельно от них существовать. И что CQRS и рхитектуре на основе сообщений тоже могут существовать отдельно. Просто у меня кривовато получаеться объяснять что я хочу сказать. Да и других понять мне сложно.


        1. VanquisherWinbringer Автор
          26.04.2019 12:38

          Так то один класс может соблюдать SRP, CQS, DRY и YAGNI одновременно.


  1. VanquisherWinbringer Автор
    26.04.2019 12:09
    +1

    Внес правки в статью на основе замечаний. Всем спасибо за помощь! :)