Рентабельный код 2: крадущийся DDD, затаившийся CQRS

ca3b348d0d76405e94b08186043afb08.jpg Трем программистам предложили пересечь поле, и дойти до дома на другой стороне. Программист-новичок посмотрел на короткую дистанцию и сказал, «Это не далеко! Это займет у меня десять минут». Опытный программист посмотрел на поле, немного подумал, и сказал: «Я мог бы добраться туда за день». Новичок посмотрел на него с удивлением. Гуру-программист посмотрел на поле и сказал. «Кажется минут десять, но я думаю пятнадцати будет достаточно». Опытный программист рассмеялся.

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

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

Гуру программист пустился в путь, и пошел прямо через поле. Целеустремленно и прямо. Он достиг цели всего за десять минут.«Как тебе это удалось?» — спросили двое других — «Как ты умудрился не зацепить ни одной мины? «Легко.» — ответил он. «Я не закладывал мины на своем пути».

Как ни прискорбно, придется признать — мы сами закладываем себе мины. В первой части я подробно разобрал основные риски в разработке ПО и описал технологические и методологические способы ослабления этих рисков. За прошедший год я получил множество комментариев, основной смысл которых сводился к следующему: «все круто, но с чего начать и как все это будет выглядеть в реальном мире». Действительно, первый текст носит скорее теоретический характер и представляет собой каталог ссылок. В этой статье я постараюсь привести как можно больше примеров.Программистам, не обладающим достаточным опытом, многие проблемы, описанные в первой статье, могут быть не понятны, ведь они проявляются только в long run«е: нет большой разницы как написан сайт на десять страничек с парой формочек для фильтрации и CRUD-админкой, если весь код можно переписать с нуля за пару дней. Но что произойдет через полтора-два года? Наш сайт начинает развиваться, появляется новая функциональность, страничек и формочек становится уже несколько десятков, добавляются партнерки с внешними ресурами. Мы дали рекламу в паблике-миллионике «Вконтакте» и главная страница «сложилась» под нагрузкой. Не беда, втыкаем кеш. Теперь нужно побыстрее запартнериться с дружественным сервисом. Копипастим код интеграции с партнером из его кодовой базы, пишем хранимки для синхронизации «наших» и «внешних» данных.

Начинаются изменения в бизнес-правилах, которые приходится дублировать в коде хранимок и в предметной области, кеш главной страницы нужно периодически обновлять, количество багов и конфигурационных проблем в беклоге растет.Проходит 6–7 итераций изменения бизнес правил, интеграций, оптимизаций производительности и расширения функциональности и вот уже у нас чудовище Франкенштейна, сшитое из примеров кода со Stack Overflow, кода SDK партнеров, костылей и хотпатчей, разнообразных невероятно полезных 3rd party — компонентов и собственных велосипедов.

Основные проблемы, которые мне приходится исправлять в коде 2–3 летней давности повторение boiler-plate-кода (всевозможные using, try-catch, log и т.д.): затрудняет внесение изменений в кодовую базу и рефаторинг, незаметно съедает время на написание одинаковых конструкций (секунды в начале проекта, дни — через год-два существования кодовой базы) дублирование кода, в т.ч неявное, бездумное создание одинаковых классов Entity, DTO и ViewModel, дублирование Linq-запросов, создание однотипных интерфейсов ITEntityRepository: IRepository, имеющих только одну реализацию нарушение SRP: запихивание в Entity свойств из разных контекстов и даже не связанных с домменной моделью, создание разнообразных Manager«ов, Service«ов и Helper«ов с невнятной ответственностью нарушение LSP NullReferenceExcetion ошибки в данных создание обратной связи, перемешивание слоев приложения, инфраструктурного кода и кода доменной модели Обычно оправданием всему этому безобразию служат: cжатые сроки релиза отсутствие времени на рефакторинг отсутствие времени на планирование и проектирование (какая архитектура? фигачить надо!) В целом, все это правдиво, но нужно признать, что даже опытным разработчикам зачастую не хватает «мат-части», чтобы быстро принимать верные решения. Под верными я понимаю следующее положение вещей: на 80% вы пишете код, выполняющий бизнес-кейс, а на 20% — небольшой задел на будущее, каждый раз угадывая какие именно 20% надо написать (иначе говоря, как изменятся требования). Чтобы угадывать, что захотят стейкхолдеры (как будет изменяться доменная модель приложения) обычно требуется опыт управленческой работы и/или опыт работы в предметной области.Однако есть ряд требований, возникающих во многих растущих проектах:

многопользовательский доступ (печально известный хабра-эффект), подсчет просмотра показов, лайков и т.д. аналитика и персонализация (построение воронок продаж, анализ пользовательских предпочтений с целью предложить более релевантный контент) полнотекстовый поиск выполнение отложенных задач и задач по расписанию фильтрация, преобразование и постраничный вывод данных логирование, система нотификаций, мониторинг, само-диагностика, само-восстановление после сбоев и обработка ошибок В этой статье я подробно остановлюсь на отделение домена (бизнес-правил) от инфраструктуры приложения. Всякое AOP, динамическую компиляцию и прочую магию оставим на следующий раз.10 слоев приложения должны быть достаточно каждому 68f47a8dca0d4fa2b904ca407c45306c.pngДля «обычного веб-приложения в вакууме» я насчитал максимум 10 слоев. Много это или мало? Учитывая, что можно «срезать углы» и обойтись классическими тремя, думаю, что в самый раз. Выносим за скобки эндпоиты, дейта-маппер, паблишер-сабскрайбер, интрецепторы. Остались:

Сервисы Command/Query Entity DAO Для того, чтобы расставить все по местам начнем с самого простого случая веб-приложения — лендинг пейдж.

Целевая страница (англ. «landing page») — веб-страница, построенная определенным образом, основной задачей которой, является сбор контактных данных целевой аудитории. Используется для усиления эффективности рекламы, увеличения аудитории…

У нас есть одна страничка и она собирает «лидов». У лида должен быть email. Необязательными полями на форме будут телефон и имя. Создадим класс «лида»:

public interface IEntity { string GetId ();

}

public class Lead: IEntity { public static Expression> ProcessedRule = x => x.Processed;

private string _email;

[Key, Index («IX_Email», 1, IsUnique = true)] public string Email { get { return _email; } set { if (string.IsNullOrEmpty (value)) { throw new ArgumentNullException («value»); }

_email = value; } }

public string Phone { get; set; }

public bool Processed { get; set; }

public DateTime CreatedDate { get; set; }

[Obsolete («Only for model binders and EF, don’t use it in your code», true)] internal Lead () { }

public Lead ([NotNull] string email, string phone = null) { Email = email; Phone = phone; CreatedDate = DateTime.Now; }

public bool IsProcessed () { return this.Is (ProcessedRule); }

public string GetId () { return Email; } } Entity, Rich Domain Model и защитное программирование (aka инкапсуляция) Полемика: samolisov.blogspot.ru/2012/10/anemic-domain-model.htmlЯ сторонник богатой доменной модели (Rich Domain Model) и не люблю анемичную, поэтому Lead обладает правильным конструктором и не дает перевести себя в несогласованное состояние (нарушить инвариант). Современные ORM-фреймворки, чьи предки породили анемичные модели, уже позволяют соблюдать принципы инкапусляции (ну почти). На помощь приходит модификатор доступа internal. Специально для тех, кто не моет руки и внутри домменной сборки использует конструктор по-умолчанию есть атрибут [Obsolete]. Второй параметр сломает билд при попытке использовать этот конструктор в явном виде, при этом ваши ORM и ModelBinder спокойно воспользуется этим конструктором. [Obsolete («Only for model binders and EF, don’t use it in your code», true)] internal Lead () { }

public Lead ([NotNull] string email, string phone = null) { Email = email; Phone = phone; CreatedDate = DateTime.Now; } Гарантии, что джуниор не уберет его конечно нет, но такие вещи можно решить в ходе код-ревью.Свойства в .NET придумали не только для того, чтобы мапить их на бд. При такой организации кода вы упадете именно в том месте, где попытаетесь установить не верный 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; } } В конструкторе также используется свойство Email, так что инвариант надежно защищен.Мне так работать проще, отсюда вытекает правило — Code First. Сначала я пишу модель предметной области, а потом создаю авто-миграцию (поэтому использую Entity Framework) и выполняю ее.С моей точки зрения, нет ничего зазорного в том, чтобы заменить доступ конструктору без параметров на публичный (public) и писать так, не создавая лишних однотипных DTO и ViewModel:

[HttpPost] public ActionResult Index (Lead lead) { if (! ModelState.IsValid) { return Json (new { success = false }, JsonRequestBehavior.AllowGet); }

//… } При выполнении условий: класс Lead не является корнем аггрегации форма заявки имеет отображение на класс лида один к одному конструктор без параметров защищен атрибутом Obsolete В противном случае, следует создать DTO и/или ViewModel и использовать DataMapper. Пока Visual Studio 2015 с новыми операторами выходит из CTP, эта монада помогает поддерживать код читабельным. Использование Maybe в паре с JetBrains.Annotations и Possible NullReferenceException as error в R# гарантирует отсутствие NullReferenceException в вашем коде.Фактически, это реализация паттерна NullObject в функциональном стиле.Разделение бизнес-логики и инфраструктуры (DDD) Основная полемика вокруг DDD крутится вокруг следующих тезисов: cлишком мало публично-доступных примеров с DDD в сети, не понятно, что это вообще такое что считать доменом, а что инфраструктурой DDD — очень долго и дорого, по сравнению с методологией «оп-оп, готов код» Прочтение книги Эванса стало для меня вторым крутым поворотом в понимании кода, после The Art of Unit Testing. За 10 лет в разработке ПО, я успел насмотреться на большое количество кодовых баз. Слава богу, уже на всех платформах есть базовая платформа (фрейморк) и пакетный менеджер и никому не приходит в голову писать свой MVC-фреймворк с блекджеком и куртизанками.Однако, бизнес-логику можно найти в самых разнообразных местах приложения: в хранимых процедурах, helper«ах, manager’ах, service’ах, теле контроллеров, репозиториях, linq-запросах. В отсутствии четкого регламента каждый разработчик будет организовывать бизнес-логику в соответствии со своими представлениями о прекрасном. Это создает целую кучу проблем:

непонятно где искать бизнес-правило невозможно внятно ответить на вопрос, каково покрытие тестами кода доменной модели, код тяготеет к процедурному стилю, нарушается инкапсуляция есть опасность продублировать бизнес-правило в двух или более местах (например, в c#-коде и коде хранимой процедуры. При изменении требований с большой вероятностью вспомнят поменять правило только в одном месте. В итоге расхождение останется и может всплыть через несколько месяцев. Разбираться с проблемой будут уже другие люди и не факт, что они знают, как «должно быть правильно». Кроме этого, manager«ы и helper«ы — классы с невнятной ответственностью, с большой вероятностью превращающиеся со временем в God-object«ы никак не регламентируются зависимости между сборками. Классы доменных моделей с легкой руки junior«а запросто могут начать зависеть от веб-контекста и сборки Common.Web На более высоком уровне это выливается в: замедление темпов разработки, вплоть до состояния, когда вся команда фулл-тайм занята поддержкой и ликвидацией багов постоянный багфиксинг и костылинг необходимость выделения дополнительных ресурсов на поддержку и устранения ошибок в базе данных приложения Для меня DDD Эванса — это способ стандартизировать работу с бизнес-логикой. DDD предлагает набор паттернов для этого. Давайте рассмотрим основные из них. Заодно «поженим» их с еще одной «модной» концепцией — CQRS.Как это связано с DDD? DDD говорит нам мухи домен отдельно, инфраструктура — отдельно. Окей у нас есть «сущность».Entity public interface IEntity { string GetId (); } Сущностью называется все что угодно, обладающее уникальным идентификатором. Два гвоздя в мешке — не Entity, потому что нельзя отличить один от другого. С точки зрения домена они идентичны. А вот Вася и Петя для нашей налоговой — Entity. У них есть ИНН (идентификационный номер налогоплательщика). В современных приложениях в качестве Id чаще всего выступает автоинкрементируемое целочисленное значение или GUID. Не смотря на распространенность этого подхода, в ряде случае он может создавать ситуации, требующие специальной обработки. Если вы когда-нибудь покупали авиабилеты у аггрегатора, то знаете, что номер бронирования аггрегатора может не совпадать с номером бронирования авиаперевозчика. Это происходит из-за того, что у перевозчика своя ИТ-система, а у аггрегатора — своя.

Вернемся к примеру с лендинг пейдж и лидом. Моя реализация IEntity наиболее абстрактная — это метод, возвращающий Id в виде строки. Я намеренно использую метод, а не свойство. Все свойства класса Lead мапятся на поля БД. Это поможет избежать неоднозначности и необходимости подсматривать в маппинг. Первичным ключом выступает Email, а не Id. Если бы я реализовал Id свойством, мне бы пришлось явно указывать, что свойство Id мапится на поле Email в БД, кроме этого, это бы создало проблемы с первичными ключами других типов (целочисленными и guid«ами) public class Lead: IEntity { private string _email;

[Key, Index («IX_Email», 1, IsUnique = true)] public string Email { get { return _email; } set { if (string.IsNullOrEmpty (value)) { throw new ArgumentNullException («value»); }

_email = value; } }

public string Phone { get; set; }

public bool Processed { get; set; }

public DateTime CreatedDate { get; set; }

// IEntity Implementation public string GetId () { return Email; } Я уже говорил, что использую в качестве основновной, но не единственой, ORM Entity Framework. Основные причины: автоматическая генерация миграций (экономит кучу времени и освобождает от написания рутинного кода) лучшая в .NET поддержка linq развивается быстрее, чем NHibernate поддерживает DataAnnotation-атрибуты для маппинга данных и создания миграций Fluent mapping VS Atribute mapping На вкус и цвет все фломастеры конечно разные. Fluent mapping чище и позволяет не тащить EF в зависимости доменной сборки, но мне не нравится его многословность и необходимость поддерживать 2 класса: сущности и маппинга. Кто-то может сказать, дескать это нарушение SRP. Мое мнение — атрибуты не императивный код и такое сравнение не корректно. Я вижу разницу лишь в форме записи и лишней зависимости от EF, которая, впрочем, легко выпиливается при необходимости.Persistance ignorance Итак, у нас есть доменные сущности и их нужно создавать, обрабатывать, сохранять, фильтровать, получать из какого-то источника данных и удалять. В простонародье это называется CRUD-операциями. Создать сущность мы можем с помощью оператора new, не забывая о том, что использовать нужно «правильный» конструктор, не нарушающий инвариант объекта. Конструктор по-умолчанию мы объявили только для поддержки ORM и защитились от грязных рук атрибутом obsolete.Как мы можем сохранить объект и получить из источника данных? В современных фреймворках используется два подхода: Active Record (AR) и Unit of Work (UoW). Я категорический противник Active Record«а. Возможно, что в интерпретируемых ЯП AR и дает преимущества, но не в компилируем. AR самым безобразным образом нарушает SRP, добавляя всем Entity метод Save. Задача Entity — реализация бизнес-логики и инкапсуляция данных, а никак не сохранение себя в БД. Поэтому, мой выбор — UoW. public interface IUnitOfWork: IDisposable { void Commit ();

void Save(TEntity entity) where TEntity: class, IEntity;

void Delete(TEntity entity) where TEntity: class, IEntity; } Для EF реализацией UoW будет DataContext вашего приложения. Однако, сам DataContext торчать в доменные сборки не будет. Во-первых, зачем нам тут зависимость от EF? Во-вторых достаточно часто требуется реализовать Bounded Context, чтобы разделить команды разработчиков, при этом миграции лучше оставить в пределах одного DataContext, чтобы исключить вариант рассинхронизации схемы данных.Как сохранять, редактировать и удалять понятно. Осталась функция получения данных. Традиционно эта функция реализуется с помощью репозиториев. И традиционно реализуется «коряво». Подробно проблемы описаны в статье «проблемный шаблон репозиторий».Если коротко, сначала вы делаете так:

public interface IRepository { T GetById (int id); IEnumerable GetAll (); bool Add (T entity); bool Remove (T entity); } В итоге получается так:

class AccountRepository: IRepository { public Account GetByName (string name); public Account GetByEmail (string email); public Account GetByAge (int age); public Account GetByNameAndEmail (string name, string email); public Account GetByNameOrEmail (string name, string email); // … public Account GetByAreYouFuckingKiddingMe (SomeCriteria c); } Вы выходите из положения вот так (вариант с extension-методами я не рассматриваю, по причинам, изложенным ниже):

public interface IRepository { T GetById (int id); //во имя луны IQueryable Query (); bool Add (T entity); bool Remove (T entity); } Но обламываетесь вот так: // было такое бизнес-правило repo.Query ().Where (a => a.IsDeleted = false);

//, а стало такое, ищите теперь эти лямбды по всему приложению repo.Query ().Where (a => a.IsDeleted = false && a.Balance > 0);

// runtime error repo.Query ().Where (a => a.CreationDate < getCurrentDate()); В последнем примере первые два linq-запроса иллюстрирует изменения бизнес-правила «активный аккаунт». Сначала мы считали активными не удаленные, а потом добавилось требование «баланс должен быть больше нуля». Так как linq-запросы очень легко писать с большой вероятностью они будут скопипащены в десятке мест кодовой базы. Почти наверняка где-то поменяют, а где-то забудут.Третий пример отлично скомпилируется, но грохнется на этапе выполнения, потому что ORM не поймет как транслировать вашу функцию getCurrentDate в SQL. Если время поджимает, а таск достался junior’у, он быстренько «допилит» код напильником вот так:

repo.Query ().ToEnumerable ().Where (a => a.CreationDate < getCurrentDate()); И все 3 миллиона аккаунтов поднимутся в оперативную память.Есть еще парочка неявных проблем с предоставлением IQueryable наружу:IQueryable «протекает» и явно нарушает LSP. Единственная реализация IQueryable, которая «переварит» любые экспрешны, которые вы ей скормите – это in-memory. Но для in-memory у вас есть linq2object, что лишает IQueryable всякого смысла. Любой Where-запрос – потенциальная точка отказа вашего кода Не все источники данных поддерживают linq. В какой-то момент вам захочется полнотекстового поиска, а с ним Sphinx’а или Elastic’а. Я сомневаюсь, что предложения «давайте напишем свой linq-провайдер» найдут отклик у менеджмента (и правильно, кстати). Полнотекстом дело не ограничивается, данные могут прийти по сети, храниться на диске, в облачной файловой системе и еще много где Даже если в качестве источника данных выступает база данных, возможно в целях производительности часть данных находятся в денормализованном виде или перенесена в NOSQL-решение. Возможно, что придется писать запросы руками и тюнить все по-максимуму, в т.ч. маппинг объектов Первая проблема не решается в принципе. Это заложено в linq by-design. И грех жаловаться, linq – это очень удобно. Пункты два и три как-бе намекают, что IQueryable — не подходит в качестве абстракции на все случаи жизни, потому что в реальном мире еще не все .NET-разработчики с пол-пинка разбирают деревья выражения и в течение дня пишут свой linq-провайдер на любой источник данных.Хорошо, что все уже придумали за нас

Specification aka Filter public interface ISpecification where T: IEntity { bool IsSatisfiedBy (T o); }

public interface IRepository { T GetById (int id); //во имя луны IEnumerable GetBySpecification (ISpecification spec); bool Add (T entity); bool Remove (T entity); } Спецификация — это бизнес-правило фильтрации. Всего один метод, либо объект удовлетворяет условию, либо — нет. В текущем виде спецификация решает проблему дублирования кода: теперь у вас нет Linq и на каждое правило фильтрации придется написать свой класс спецификации.Но, позвольте, зачем это нужно? Мы не можем транслировать IsSatisfiedBy в SQL, а значит, снова придется поднимать все записи из БД и фильтровать по ним. Теперь нам нужно писать спецификацию на каждый чих, а значит создавать множество классов, используемых ровно один раз (в том месте интерфейса, где нужно отфильтровать данные определенным образом).Действительно, ведется целая дискуссия, дескать паттерн «спецификация» устарел с появлением linq.Первое, что предлагают астронавты архитектуры:

public interface IExpressionSpecification: ISpecification where T: class, IEntity { Expression> Expression { get; } }

public interface IRepository { T GetById (int id); //во имя луны IEnumerable GetBySpecification (IExpressionSpecification spec); bool Add (T entity); bool Remove (T entity); } Не нужно обладать сверх-способностями, чтобы увидеть те-же яйца, вид в профиль с дополнительной ненужной прослойкой в виде спецификации. Тем более, что проблему дублирования linq-запросов в коде можно изящно решить вот так: public class Account: IEntity { [BusinessRule] public static Expression> ActiveRule = x => x.IsDeleted && x.Ballance > 0; } В итоге репозиторий не получится эффективно использовать в качестве базовой абстракции для всех источников данных linq — это очень удобно, но подходит не везде по экономическим причинам или ограничениям производительности CQ[R]S — Command, Query [Responsibility] Segregation Или по-русски разделение чтение и записи. Наибольшее применение принцип нашел в нагруженных системах. Классический пример — фид в социальных сетях: необходимо вытащить данные из кучи таблиц для всех ваших друзей и не забыть учесть как все лайкают и репостят ваши фоточки. Классическая реализация для этой задачи не подходит — слишком много джоинов и блокировок чтения/записи.Обычное решение — разделить чтение и запись, чтобы избежать блокировок. Стратегии деноромализации могут быть разные, но основной смысл сводится к: избавится от джоинов, читать плоские данные избегать блокировок чтение/запись синхронизировать данные «в фоне», накапливая изменения Таким образом, классический репозиторий разделяется на два интерфейса: Command и Query.Command реализует добавление, изменение и удаление (CUD), а Query — чтение ® данных. public interface IQuery where TEntity: class, IEntity where TSpecification: ISpecification { IQuery Where ([NotNull] TSpecification specification);

IQuery OrderBy( [NotNull] Expression> expression, SortOrder sortOrder = SortOrder.Asc);

IQuery Include([NotNull] Expression> expression);

[NotNull] TEntity Single ();

[CanBeNull] TEntity FirstOrDefault ();

[NotNull] IEnumerable All ();

[NotNull] IPagedEnumerable Paged (int pageNumber, int take);

long Count (); }

public interface ICommand { void Execute (); }

public interface ICommand { void Execute (T context); }

public interface IPagedEnumerable: IEnumerable { long TotalCount { get; } }

public class CreateEntityCommand: UnitOfWorkScopeCommand where T: class, IEntity { public override void Execute (T context) { UnitOfWorkScope.GetFromScope ().Save (context); UnitOfWorkScope.GetFromScope ().Commit (); }

public CreateEntityCommand ([NotNull] IScope unitOfWorkScope) : base (unitOfWorkScope) { } }

public class DeleteEntityCommand: UnitOfWorkScopeCommand where T: class, IEntity { public DeleteEntityCommand ([NotNull] IScope unitOfWorkScope) : base (unitOfWorkScope) { }

public override void Execute (T context) { UnitOfWorkScope.GetFromScope ().Delete (context); UnitOfWorkScope.GetFromScope ().Commit (); } } Основная обязанность Query транслировать спецификацию (доменное правило фильтрации) в запрос к источнику данных (инфраструктура). Query предоставляет абстракцию от источника данных — нам не важно откуда мы получаем данные, а спецификация — это своеобразный linq+. Для источников данных, поддерживающих linq можно использовать ExpressionSpecification. В случаях, когда использование linq затруднено (нет провайдера, например как в случае с Elastic Search), выкидываем Expression«ы и используем свою спецификацию. public interface IExpressionSpecification: ISpecification where T: class, IEntity { Expression> Expression { get; } }

public static IQuery> Where( this IQuery> query, Expression> expression) where TEntity: class, IEntity { return query.Where (new ExpressionSpecification(expression)); } Для фильтрации данных в оперативной памяти можно использовать экземпляр спецификации, а трансляция спецификации в запрос к источнику данных ложиться на Query.

ICommandFactory, IQueryFactory Создание большого количества маленьких объектов command и query может быть утомительным занятием, логично зарегистрировать их в IOC-контейнере по конвеншнам. Чтобы не тащить ваш контейнер во все сборки и не создавать ServiceLocator, возложим эту обязанность на фабрики. public interface ICommandFactory { TCommand GetCommand() where TCommand: ICommand;

T GetCommand() where T: ICommand;

CreateEntityCommand GetCreateCommand() where T: class, IEntity;

DeleteEntityCommand GetDeleteCommand() where T: class, IEntity; }

public interface IQueryFactory { IQuery> GetQuery() where TEntity: class, IEntity;

IQuery GetQuery() where TEntity: class, IEntity where TSpecification: ISpecification;

TQuery GetQuery() where TEntity: class, IEntity where TSpecification: ISpecification where TQuery: IQuery; } Тогда получение объектов query будет выглядеть так: _queryFactory.GetQuery() .Where (Product.ActiveRule) // это статический экспрешн, как в примере с Account. Используется ExpressionSpecification .OrderBy (x => x.Id) .Paged (0, 10) // получаем 10 продуктов для первой страницы

// Мы решили подключить полнотекстовый поиск и добавили ElasticSearch, не вопрос: _queryFactory.GetQuery() .Where (new FullTextSpecification («зонтик»)) .All ()

// Или EF тормозит и мы решили переделать на хранимую процедуру и Dapper _queryFactory.GetQuery() .Where (new DictionarySpecification (someDirctionary)) .All () Во всех случаях мы используем один и тот-же код, а конструкция _queryFactory.GetQuery () явно указывает нам на то, что это оптимизация. Эта строчка появилась в коде только в ходе эволюционного рефакторинга, потому что сначала мы писала на ORM, ради скорости разработки. Если в команде есть человек, хорошо разбирающийся в деревьях выражений, постепенно можно перевести все запросы на linq (хотя в реальной жизни это почти невозможно по экономическим соображениям).В случае с _queryFactory.GetQuery() мы указываем «полнотекстовую спецификацию», однако доменный код ничего не знает о том, что возвращаемый экземпляр — ElasticSearchQuery. Для него это просто правило фильтрации «полнотекстовый поиск».

Полемика на тему: habrahabr.ru/post/125720

Немного синтаксического сахара Вернемся к примеру: public class Account: IEntity { [BusinessRule] public static Expression> ActiveRule = x => x.IsDeleted && x.Ballance > 0;

bool IsActive () { // как не дублировать код здесь??? } } Бизнес-правило «активный аккаунт» находится в логичном месте и переиспользуется. Не нужно бояться, разбросанных по всему проекту лямбд.Иногда это требование нужно в виде Expression> — для трансляции в запрос к источнику данных, а иногда в виде Func> — для фильтрации объектов в памяти и предоставления свойств, вроде IsActive. Создание класса спецификации на каждый чих не кажется хорошей идеей. Когда можно использовать следующую реализацию: public static class Extensions { private static readonly ConcurrentDictionary _cachedFunctions = new ConcurrentDictionary(); public static Func AsFunc(this object entity, Expression> expr) where TEntity: class, IEntity { if (!_cachedFunctions.ContainsKey (expr)) { _cachedFunctions[expr] = expr.Compile (); }

return (Func) _cachedFunctions[expr]; }

public static bool Is(this TEntity entity, Expression> expr) where TEntity: class, IEntity { return AsFunc (entity, expr).Invoke (entity); }

public static IQuery> Where( this IQuery> query, Expression> expression) where TEntity: class, IEntity { return query .Where (new ExpressionSpecification(expression)); } }

public class ExpressionSpecification: IExpressionSpecification where T: class, IEntity { public Expression> Expression { get; private set; }

private Func _func;

private Func Func { get { return this.AsFunc (Expression); } }

public ExpressionSpecification ([NotNull] Expression> expression) { if (expression == null) throw new ArgumentNullException («expression»); Expression = expression; }

public bool IsSatisfiedBy (T o) { return Func (o); } }

public class Account: IEntity { [BusinessRule] public static Expression> ActiveRule = x => x.IsDeleted && x.Ballance > 0;

bool IsActive () { this.Is (ActiveRule); } } А где Service«ы, Manager’ы и Helper«ы? При правильной организации код, никаких helper«ов в домене у вас нет. Может быть в слое представления что-то такое есть. Manager и Service — суть одно и тоже, поэтому название Manager лучше вообще не использовать. Service — это чисто технический термин. Используйте Service только как постфикс или не используйте вовсе (оставьте только namespace для того, чтобы зарегистрировать по соглашениям в IOC).В реальном бизнесе нет «сервисов», есть «кассы», «проводки», «квоты» и всякое такое. Так что лучше группировать вашу бизнес-логику и именовать классы сообразно домену приложения и создавать только по мере необходимости. Для CRUD-операций не нужны никакие сервисы. Связки UoW+Command+Query+Specification+Validator хватит, чтобы закрыть 90% потребностей учетных систем. Кстати, для этого нужен только один класс контроллера.Заключение Подобная архитектура может показаться «перегруженной». Действительно подобный подход накладывает определенные ограничения: квалификация разработчиков: требуется понимание паттернов программирования и хорошее знание платформы первоначальные вложения в код инфраструктуры (мне потребовалось почти 4 дня фулл-тайм для того, чтобы вытащить интерфейсы из своих проектов, отвязать ненужные зависимости и сделать инфраструктуру максимально абстрактной и легковесной, под нож пошло много кода) именно эта сборка еще только проходит испытание в реальном проекте, у меня на руках нет метрик и гарантий в том, что данный подход дает выигрыш в производительности за счет стандартизации (хотя субъективно, я в этом уверен на все 100%) Преимущества четкое отделение домена от инфраструктуры минимизация объема кода в проекте, устранение дублирования кода, устранение циклических зависимостей, устранение рутины, использование соглашений, вместо избыточных конфигураций регламентирование бизнес-логики и общей структуры проекта может использоваться в качестве repair-kit для внедрения в чужие кодовые базы абстракция от серверной инфраструктуры, поддержка горизонтального масштабирования Продолжение следует…

© Habrahabr.ru