Pull to refresh

Comments 19

повторение boiler-plate-кода (всевозможные using, try-catch, log и т.д.)

И как же вы это исправляете? В примерах ничего нет.

При такой организации кода вы упадете именно в том месте, где попытаетесь установить не верный email, а не при сохранении в БД, которое может быть очень далеко от момента простановки значения, особенно при массовых операциях.
        [Key, Index("IX_Email", 1, IsUnique = true)]
        public string Email
        {
            get { return _email; }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException("email");
                }

                _email = value;
            }
        }


А как же валидация на клиенте? Эксепшн-то вы получили, но как вам получить список невалидных полей? (и это мы еще не затрагиваем сохранение драфтов)

Моя реализация IEntity наиболее абстрактная – это метод, возвращающий Id в виде строки.

… и как же потом по нему искать? Ваш репозиторий принимает на вход целочисленный id. Поздравляю вас, вы только что нарушили целостность.

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

И при этом ваш следующий абзац называется Persistence Ignorance. Нет, серьезно? Собственно, именно поэтому как раз про ignorance у вас нет ни слова, только про persistence.

CQ[R]S – Command, Query [Responsibility] Segregation

Так где же реальные примеры, зачем это надо? Абстрактные «соцсети» — это не пример, там можно и на OLTP/OLAP сделать. И нет ни слова про то, как же, собственно достигается консистентность всего этого цирка.

(Собственно, про консистентность и ее границы у вас тоже нет ни слова)

И самое интересное: хотя вы и сторонник Rich Model, в примерах команд у вас только CRUD. Это означает, что либо у вас на самом деле доменная модель анемичная, либо вы выполняете бизнес-операции мимо CQRS. Так как же оно на самом деле? Опишите хотя бы тот же пример с лайками.

Связки UoW+Command+Query+Specification+Validator

А где валидатор-то в вашем примере?
А как же валидация на клиенте? Эксепшн-то вы получили, но как вам получить список невалидных полей? (и это мы еще не затрагиваем сохранение драфтов)
Про драфты не понял, а DataAnnotation отлично поддерживается jquery.unobtrusive. Вопрос не понял полностью:)

И при этом ваш следующий абзац называется Persistence Ignorance. Нет, серьезно? Собственно, именно поэтому как раз про ignorance у вас нет ни слова, только про persistence
Опять-же не понял. Уберите аттрибты, добавьте FluentValidation и классы паммингов. Сам объект не содержит никаких данных о том, как он хранится.

Так где же реальные примеры, зачем это надо? Абстрактные «соцсети» — это не пример, там можно и на OLTP/OLAP сделать. И нет ни слова про то, как же, собственно достигается консистентность всего этого цирка
Это не абстрактные соц.сети, а конкретные фиды в VK и Facebook

И самое интересное: хотя вы и сторонник Rich Model, в примерах команд у вас только CRUD. Это означает, что либо у вас на самом деле доменная модель анемичная, либо вы выполняете бизнес-операции мимо CQRS. Так как же оно на самом деле? Опишите хотя бы тот же пример с лайками
Здесь вы правы, примеры не привел, спасибо. Поищу и добавлю попозже.

Про драфты не понял

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

DataAnnotation отлично поддерживается jquery.unobtrusive.

Вот только у вас не атрибут Required стоит на свойстве Email, а проверка в коде (еще и не через Code Contracts). Я же специально пример вашего кода привел.

Опять-же не понял. Уберите аттрибты, добавьте FluentValidation и классы паммингов.

Вы бы хоть смотрели, на что именно я отвечал — а отвечал я на вашу риторику «почему атрибуты лучше маппинг-классов».

Это не абстрактные соц.сети, а конкретные фиды в VK и Facebook

И много людей, читающих эту статью, разрабатывает конкретные фиды в ВК и ФБ? Кстати, а у вас есть first-hand knowledge, что там именно CQRS? Так что нет, это очень отвлеченный пример.
Добрый день, отмечу объективные минусы Вашей ( наш вариант CQRS ) реализации:

UnitOfWorkScope.GetFromScope().Commit();

Нужно помнить, о том что надо вызывать метод. Проблема решается, если мы будем использовать Dispatcher, который скрывает все эти моменты.
dispatcher.Push(new AddSomeEntityCommand());
dispatcher.Push(s=>{
s.Quote(new Some1Command());
s.Quote(new Some2Command());
}); // share transaction


Тем более, что проблему дублирования linq-запросов в коде можно изящно решить вот так:
public class Account : IEntity
{
        [BusinessRule]
        public static Expression<Func<Lead, bool>> ActiveRule = x => x.IsDeleted && x.Ballance > 0;
}


Мне кажется, более универсально (а так же к тестированию пригодно) разбить Where на 2 части, а сами expression упаковывать в классы
new LeadOnlyDeletedWhere().And(new LeadGreaterBalance(0))

__queryFactory.GetQuery<Product>()
    .Where(Product.ActiveRule) // это статический экспрешн, как в примере с Account. Используется ExpressionSpecification
    .OrderBy(x => x.Id)
    .Paged(0, 10) // получаем 10 продуктов для первой страницы


Наш, пример с specifications.
Repository.Query(whereSpecification: new ProductByFullTextWher("test")
                                    .And(new ProductBySomethingElseWhere("some"),
                 orderSpecification:new ProductByNameOrder(),
                 paginatedSpecification:new PaginatedSpecification(current,size))

Отмечу, основные плюсы:
1. Можно делать StubQuery, в рамках теста и потом отдельно тестировать specification.
2. Specification, позволяют скрыть логику, к примеру ProductByRangeWhere(start,end)

ICommandFactory и IQueryFactory

Те же static helper, по этому я бы посоветовал двигаться в сторону использования Command/Query повсеместно, к примеру SendEmailCommand(), GetConnectionStringQuery и т.д.

Плюсы?
— Больше не надо, выдумывать ManagerHelper,CoreService,CommandHelper,FileBuilder and etc
— Command/Query помогают бороться с появление GOD object в проекте, потому что они атомарные и делают только одно действие ( иногда смежно AddOrEdit, но это скорее исключение, чем правило)
-Повторно можно использовать, в рамках других Command/Query (к примеру BulkAddProduct, который в foreach использует AddProduct, что бы не дублировать логику)
public class GroupEncounterRedFlagCommand : CommandBase
{
    public Guid EncounterId { get; set; }
    public Guid RedFlagGroupId { get; set; }
    public string Value { get; set; }
    public override void Execute()
    {
        foreach (var redFlag in Repository.GetById<RedFlagGroup>(RedFlagGroupId).RedFlags)
        {
            Dispatcher.Push(new ChangeEncounterRedFlagCommand()
                            {
                                    EncounterId = EncounterId, 
                                    RedFlagId = redFlag.Id, 
                                    Value = Value
                            });
        }
    }
}

-Проще навигация по проекту, ищем действие, что проще чем CommandFactory.Add, так что лучше AddGenericCommandT
-Мы даже, вызываем Command/Query по Ajax без controller (MVD)
@model AddAcoGroupCommand
<form action="@Url.Dispatcher().Push(new AddAcoGroupCommand())">
    @Html.HiddenFor(r=>r.Id)
    <input type="submit"/>
</form>


Проблемы:
— Command/Query должны создавать UnitOfWork, только в момент обращения к Repository, иначе cache внутри будет все равно тратить ресурсы на открытие подключения.
-Command/Query должны иметь разный тип транзакций, ReadUncommited для query и ReadCommited для Command

и ещё много других нюансов, которые я нашел в процессе написания Incoding Framework.
Command/Query должны иметь разный тип транзакций, ReadUncommited для query и ReadCommited для Command

Вам настолько плевать на консистентность данных?
Скорее всего vkopachinsky описывает ситуацию с нагруженными ресурсами и речь идет как раз о все-возможных ViewCount'ах, CommentCount'ах и Like'ах, которые потом «задним числом» в фоне приводятся к консистентному состоянию
Во-первых, я бы для таких значений использовал неблокирующее чтение и обновление.
Во-вторых, это же локальные правила, для конкретного Command и конкретного Query нужны такие настройки — но не для всех.
Да, для всех это как-то черезчур.
Во-вторых, это же локальные правила, для конкретного Command и конкретного Query нужны такие настройки — но не для всех.

Можно добавить возможность переопределять, но lock таблицу при чтение (без записи) это не лучший вариант в плане perfromance

Во-первых, я бы для таких значений использовал неблокирующее чтение и обновление.

Вот, для Query ReadUncommited и используется.

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

А читать данные, которые другая транзакция еще не объявила консистентными — лучший вариант? Не нравятся локи — используйте lock-free примитивы.

Вот, для Query ReadUncommited и используется.

Вы про SNAPSHOT и READ_COMMITTED_SNAPSHOT не слышали?

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

Неплохо отдавать себе отчет в том, к чему ведут ваши «однотипные» решения.
Вы про SNAPSHOT и READ_COMMITTED_SNAPSHOT не слышали?

Отвык я от Ваших умных комментариев, понятно откуда мне это знать )))

А читать данные, которые другая транзакция еще не объявила консистентными — лучший вариант?

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

Неплохо отдавать себе отчет в том, к чему ведут ваши «однотипные» решения.

ух, какая конкретика ))) Уже пошел код свой удалять, после такого аргументов…
Command/Query должны создавать UnitOfWork, только в момент обращения к Repository, иначе cache внутри будет все равно тратить ресурсы на открытие подключения.
Я вообще отказался от репозиториев. По остальным моментам, думаю что у нас примерно, разница в основном во вкусовщине.

Все-возможные диспетчеры меня пугают тем, что пролезают зависимостью по всему приложению. Мне это не нравится.
Я думаю, после того, как пару раз пропустите вызов метода Commit или появится необходимость одновременно расширить функционал Command/Query, Вы придете к варианту централизованного вызова.

На данную проблему делал акцент lair
повторение boiler-plate-кода (всевозможные using, try-catch, log и т.д.)


P.S. кстати, не заметил вызова Rollback ( сорри если пропустил )

повторение boiler-plate-кода (всевозможные using, try-catch, log и т.д.)

Кросскатинг конёрнс (AOP).
Я сторонник богатой доменной модели (Rich Domain Model) и не люблю анемичную, поэтому Lead обладает правильным конструктором и не дает перевести себя в несогласованное состояние (нарушить инвариант).


Замечу пару моментов, который сам усвоил при использование DDD:

1. CTOR — смотрится очень хорошо в рамках объекта с 2 — 5 полями, но крайне не читабельный при усложнение, когда у Вас 15, так что более лучшим решением будет new T() { Name = «value»,Name2=«value2»}.
примечание: что бы так же поддерживать обязательность заполнения всех полей, лучше писать Unit Test и Equal weak
2. Красивые названия методов HasRule, Verify и т.д, очень скоро делают из Entity обычный GOD object, по этому лучше реализовывать логику в рамках Query/Command.

Пример, такого Query
      public class GetTaskNameFromAppointmentQuery : QueryBase<string>
    {
        public Appointment Appointment { get; set; }

        protected override string ExecuteResult()
        {
            var user = Dispatcher.Query(new GetCurrentUserQuery());
            var sb = new List<string>();
            sb.Add(string.Format("Appointment Reminder - {0} Appointment:", Appointment.Status.Name));
            if (Appointment.Type != null)
                sb.Add(string.Format("{0}", Appointment.Type.Name));

            if (Appointment.Type == null && !string.IsNullOrWhiteSpace(Appointment.Professional))
                sb.Add(string.Format("With"));

            if (!string.IsNullOrWhiteSpace(Appointment.Professional))
            {
                string value = Appointment.Professional;
                if (Appointment.ProfessionalCareId.HasValue)
                {
                    var profCare = Dispatcher.Query(new GetDetailForAppointmentNameQuery() {
                                                         Id = Appointment.ProfessionalCareId
                                                                  });
                    value = profCare.Name;
                }
                sb.Add(value);
            }

            if (Appointment.Date.HasValue)
                sb.Add(string.Format("on {0}", Appointment.Date.ToDisplayString()));

            if (Appointment.Date.HasValue && Appointment.Time.HasValue)
            {
                 sb.Add(string.Format("at {0}", user.ConvertFromUTC(Appointment.Date.Add(Appointment.Time.Value))
                                                   .GetTime()
                                                   .ToTimeString()));
            }

            return string.Join(" ", sb);
        }
    }

примечание: в примере, видно user.ConvertFromUTC который как метод, но куда лучше использовать отдельный Query

Пример использования
var taskCommand = new UpdatePatientTaskCommand()
                              {
 Priority = PatientTask.TaskPriority.Normal,
 Status = Status,
 AssignedTo = assignedTo.GetValueOrDefault(),
 Task = Dispatcher.Query(new GetTaskNameFromAppointmentQuery() { Appointment = appointment })
                               };

Dispatcher.Push(taskCommand);

3. Пример с Query (выше) показал, то что в рамках Query/Command можно использовать Repository/Dispatcher, а вот в рамках Entity Вы очень ограничены.
4. Удобство тестирования, дело в том, что Mock для Entity можно сделать только, если Вы получаете её через метод, что позволяет Вам её подменить, но не когда делает new T()

P.S. Я не говорю, что мне открылась истина и DDD это не лучший подход, но я столкнулся с аргументами, которые вынудили сделать такой вывод )))
1. Если у вас 18 полей, то либо это песец какая сложная предметная область, либо legacy и 80% полей — мусор, либо не верно декомпозированы сущности, либо это «плоские» данные из аггегата. Я согласен, что такие конструкторы чудовищны, но с большой вероятностью проблема в проектировании этой Entity
2. Verify, HasRule и прочее не надо сувать в один доменный объект. Не все объекты предметной области вообще должны мапиться на модель и ничего не мешает внутри SaleRule использовать Validator и т.п. С моей точки зрения код, который вы привели в пример имеет много проблем (как раз из серии перемешивания бизнес-правил и построения строчки). Я не знаю деталей вашего приложения, но такая организация кода мне не нравится. Самое простое — ваш код можно «разломать» не передав Appointment. Вам не хватает второго параметра — типа спецификации.

Меня удивляет, что вы противопоставляете DDD и CQRS. Моя цель, наоборот, использовать полезные элементы обоих подходов. В рамках Repository я не мог решить проблему с generic-спецификацией, поэтому решение «переехать» на Query пришло само собой. Термин «команда», достаточно простой, чтобы использовать его в рамках единого языка при коммуникации с заказчиком.

Для меня DDD — способ писать код, близкий к DSL, лучше составлять спецификацию, уменьшить затраты на коммуникацию и «костыли». CQRS — способ безболезненно «рулить» инфраструктурой. Другой вопрос, что вот прям богатая модель нужна для сложных мест из предметной области (там где действительно важно и не понятно, что происходит).

Тупой CRUD можно писать на чистом CQRS и не париться.
Если у вас 18 полей, то либо это песец какая сложная предметная область, либо legacy и 80% полей — мусор, либо не верно декомпозированы сущности, либо это «плоские» данные из аггегата. Я согласен, что такие конструкторы чудовищны, но с большой вероятностью проблема в проектировании этой Entity

    public class PatientTask : ActiveHistoryEntityBase<PatientTask>, IPatient
    {
        public virtual DateTime? StartDate { get; set; }
        public virtual DateTime? DueDate { get; set; }
        public virtual string Task { get; set; }
        public virtual User AssignedTo { get; set; }
        public virtual Patient Patient { get; set; }
        public virtual TaskStatus Status { get; set; }
        public virtual TaskPriority Priority { get; set; }
        public virtual string Notes { get; set; }
   }

Какие поля тут мусор? Просто, очень странно, что Вы не сталкивались с длинными форма регистрации и т.д.

Verify, HasRule и прочее не надо сувать в один доменный объект.

Даже, пара методов могут быть очень громоздкими.

Не все объекты предметной области вообще должны мапиться на модель и ничего не мешает внутри SaleRule использовать Validator и т.п.

Очень сложно придумывать названия данным агрегатам, куда проще строить блоки из готовых глаголов ( GetLastUserQuery,ChangeUserStatusCommand and etc). Validator, прекрасно работает на Command/Query, потому что именно они, а не DDD передаются из форм приложения.

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

А построение строчки это не бизнес правило?

Самое простое — ваш код можно «разломать» не передав Appointment.

Для этого пишутся Unit Test, на тот код который вызывает данный Query.

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

По DDD, что Вы будете делать если Вам надо получить доступ к Repository/Query из метода Domain?
примечание: для метода ChangeStatus из класса User, требуется получить доступ к стороннему сервису банка, что бы проверить платеж.

Тупой CRUD можно писать на чистом CQRS и не париться

По мне вызов методов DDD, возможен только через Command/Query (ну это в рамках моей концепции)

Плюс, который я вижу в повсеместно использование CQRS, в том что Вы не привязаны к шаблонам (определенной модели) и у Вас нету ограничений, потому что каждый Query/Command обладает одинаковым набором доступных инструментов (Repository/Dispatcher).
Я бы с удовольствием продолжил эту дискуссию с вами где-нибудь на берегу океана со стаканчиком коктейля, плодить дальшие комменатрии не вижу смысла Я думаю, мы примерно одни задачи решаем, примерно одним способом. По крайней мере, мне понятен ход ваших мыслей. Каков поп, таков и приход: кому больше юнитов, кому — инкапсуляции :)
Sign up to leave a comment.

Articles