Как создать доменную модель
Перейти к содержимому

Как создать доменную модель

  • автор:

Принципы построения доменной модели

Одна из основных причин разделения сервиса на два слоя:

  • Абстрактая часть сервиса;
  • Конфигурация сервиса под конкретную информационную систему;

это дать возможность дорабатывать доменную модель под конкретного заказчика.

Рассмотрим принципы построения доменной модели сервиса на примере.

Есть две информационные системы для разных заказчиков customer1 и customer2. Цель — разработать сервис контактов для эти двух информационных систем. Одним из требований является то, что информация об организациях в этих информационных системах имела бы разную структуру.

Для каждого из заказчиков создается два приложения, каждое из которых является контейнером для сервиса контактов. Упрощенная структура модулей:

  • Приложение-контейнер для сервиса контактов заказчика customer1:
project vendor nnx-contract contract contract-core customer1-contract organization 
  • Приложение-контейнер для сервиса контактов заказчика customer2:
project vendor nnx-contract contract contract-core customer2-contract organization 

Т.е реализация сущностей, подстраивающих доменную модель под конкретного заказчика, выносится в соответствующие модули customer1-contract\organization и customer2-contract\organization.

Доменная модель в Core модулях.

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

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

В случае модуля nnx-contract/contract-core заранее неизвестно имя класса организации. Так как сущность организации, которая будет использоваться в сервисе, может быть реализована в другом модуле (в приведенном примере это customer1-contract\organization и customer2-contract\organization).

Для достижения такой гибкости используется следующий подход:

  • Для каждой сущности, которая может расширяться в других модулях, заводится интерфейс;
  • В рамках сервиса модуля работа осуществляется с объектами, имлементирующими заданный интерфейс, т.е. не допускается использовать имена классов;
  • Если есть часть свойств, которые гарантированно будут у всех классов, имплементирующих данный интерфейс, то создается абстрактный класс;
  • Создается класс сущности, являющейся родительской, для сущностей, располагаемых в других модулях.

Расмотрим конкретный пример:

Создание сущности контракта:

 namespace Nnx\Contract\Core\Entity; //Интерфейс сущности контракта. В сервисах работает с интерфейсом, а не с реализацией. interface ContractInterface
 namespace Nnx\Contract\Core\Entity; use Doctrine\ORM\Mapping as ORM; //Выносим в абстрактную часть, свойства, которые гарантированно есть у всех классов контрактов /** * Class AbstractContract * * @ORM\MappedSuperclass() */ abstract class AbstractContract implements ContractInterface < /** * @var int * * @ORM\Id() * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** * Реестровый номер * * @var string * * @ORM\Column(name="register_number", type="string", nullable=true) */ protected $registerNumber; /** * @return int */ public function getId() < return $this->id; > /** * @return string */ public function getRegisterNumber() < return $this->registerNumber; > > 
 namespace Nnx\Contract\Core\Entity; use Doctrine\ORM\Mapping as ORM; /** * Непосредственная реализация сущности контракта * * @ORM\Entity * @ORM\Table(name="contract") */ class Contract extends AbstractContract < /** * Заказчик * * @var Customer * * @ORM\ManyToOne(targetEntity="DefaultCustomerInfo", fetch="LAZY", * cascade=) * @ORM\JoinColumn(name="customer_id", referencedColumnName="id") */ protected $customer; /** * Поставщики * * @var Supplier * * @ORM\ManyToMany(targetEntity="Supplier", cascade=) * @ORM\JoinTable(name="contract_supplier", * joinColumns=<@ORM\JoinColumn(name="contract_id", referencedColumnName="id")>, * inverseJoinColumns= <@ORM\JoinColumn(name="supplier_id", referencedColumnName="id")>* ) */ protected $suppliers; /** * @return Customer */ public function getCustomer() < return $this->customer; > /** * @return ArrayCollection|Supplier[] */ public function getSuppliers() < return $this->suppliers; > > 

Создаем сущности заказчика и поставщика:

 namespace Nnx\Contract\Core\Entity; /** * Interface OrganizationInterface * */ interface OrganizationInterface < /** * @return int */ public function getId(); /** * @return string */ public function getInn(); >

Абстрактная реализация организации:

 namespace Nnx\Contract\Core\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() * @ORM\Table(name="contract_organization") * @ORM\InheritanceType(value="SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="type", type="string") */ abstract class AbstractOrganization implements OrganizationInterface < /** * @var int * * @ORM\Id() * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** * ИНН * @var string * * @ORM\Column(name="inn", type="string", nullable=true) */ protected $inn; /** * @return int */ public function getId() < return $this->id; > /** * @return string */ public function getInn() < return $this->inn; > > 
 namespace Nnx\Contract\Core\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class Customer extends AbstractOrganization
 namespace Nnx\Contract\Core\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class Supplier extends AbstractOrganization

Расширение доменной модели под конкретного заказчика

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

Пусть у нас есь сервис контрактов со следующей структурой каталогов:

project vendor nnx-contract contract contract-core customer1-contract organization 

В nnx-contract/contract-core описаны сущность Contract, а также Customer и Supplier.

Расмотрим на примере расширение этих сущностей в модуле:

 namespace Customer1\Contract\Core\Entity; use Doctrine\ORM\Mapping as ORM; use Nnx\Contract\Core\Entity\Customer; /** * @ORM\Entity() */ class ExtendedCustomer extends Customer < /** * @var string * * @ORM\Column(name="custom_field1", type="string", nullable=true) */ protected $customField1; >
 namespace Customer1\Contract\Core\Entity; use Doctrine\ORM\Mapping as ORM; use Nnx\Contract\Core\Entity\Supplier; /** * @ORM\Entity() */ class ExtendedSupplier extends Supplier < /** * @var string * * @ORM\Column(name="custom_field2", type="string", nullable=true) */ protected $customField2; >

Copyright (c) 2016 NNX-Group

Ценности DDD

Основоположником DDD (Domain Driven Design, предметно-ориентированное проектирование) является Эрик Эванс, который в довольно далеком 2003 году подарил миру свою знаменитую книгу о предметно-ориентированном проектировании. Безусловно, не все, что описано в книге придумал автор с нуля. Многие идеи и практики существовали и до него, но у Эванса получилось все это систематизировать и правильно расставить акцента. Давайте попробуем разобраться, что же именно предлагает Эванс.

На мой субъективный взгляд DDD стоит на трех основных столпах (и это если что не три буквы Д):

  • Доменная модель
  • Ограниченный контекст
  • Агрегаты

Доменная модель

Transaction Script и Domain Model

Данное понятие существовало и до Эванса. Например, Мартин Фаулер ставит доменную модель в противоположность так называемому подходу Transaction Script, который представляет своего рода более процедурный стиль кодирования, чем объектно-ориентированный. Обычно при таком подходе акцент смещается в сторону базы данных и манипуляций с данными, чем в сторону работы с бизнес-логикой. Обычно такой подход реализуется как некая сущность, отображаемая на базу данный, с большим количеством геттеров и сеттеров. И управляющей класс, некий сервис, который и вызывает эти сеттеры и по определенным правилам обновляет базу данных. Доменный слой, построенный на основе подобных сущностей, Фаулер также называет анти паттерном — анемичная модель. Фаулер обосновывает это тем, что в настоящем объекте должны быть не только данные, но и поведение.

If all your logic is in services, you’ve robbed yourself blind.

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

В отличие от Transaction Script, где логика содержится в сервисах, а данные в сущностях, в доменной модели и логика и данные размещены в доменных объектах. Согласно идеям объектного-программирования такие объекты инкапсулируют свое внутреннее состояние, а для работы с ним предоставляют вполне определенный внешний интерфейс. Например, у объекта корзины может быть метод добавления товара. Тут можно возразить и сказать, что у нашей сущности вполне может быть сеттер для добавления товара.

Да, все это верно. Но не стоит забывать, что в идеале класс корзины должен соблюдать ряд бизнес-инвариантов. Например, после добавления товара в корзину, итоговая стоимость корзины должна увеличится на сумму добавленных товаров. В подходе Transaction Script данная логика размещается в сервисе. Но при таком раскладе соблюдение инвариантов не обеспечивается ничем, кроме хороших тестов и внимательности программиста. Существует не нулевая вероятность, что в каком-то другом сервисе проявится ошибка и он изменит данные корзины неверным образом. В случае же с доменной моделью, за корректность изменения данных (за соблюдение инвариантов) отвечает только один объект сама корзина (может быть еще ее «внутренние классы», но опять же об этом мы не знаем, за счет соблюдения подхода сокрытия информации). Таким образом мы формируем абстракцию корзины, с которой должны взаимодействовать другие классы модели, через ее определенный интерфейс, а не влияя напрямую на ее внутреннее состояние. Также автоматически начинают соблюдаться еще и такие принципы, как SRP (принцип единственной ответственности), low coupling и high cohesion (слабая внешняя связанность и высокое внутреннее зацепление).

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

Единый язык

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

DDD — это про общение между людьми, одна из его задач — сломать имеющийся «языковой барьер» между бизнесом и разработкой.

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

Размышляя на тему DDD и хорошо проработанной доменной модели у меня всегда возникает ассоциация с небезызвестным высказыванием:

Сначала ты работаешь на репутацию, а потом она работает на тебя.

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

Ограниченный контекст

Тут уже все несколько посложнее. Есть понятие предметная область, она же и есть домен (domain). Это та сфера деятельности, в которой работает наш бизнес. Например, тот же самый e-commerce, доставка еды из ресторанов, бухгалтерская сфера или что-то иное. В любом случае это весьма обширная сфера и при разработке ПО нет смысла моделировать всю эту огромную область.

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

Разное использование понятий в зависимости от контекста

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

Например, заказ для отдела продаж содержит информацию о покупателе, набор заказанных товаров. Он может предоставлять такие методы, как изменение статуса или выполнение возврата. Для службы доставки столь подробная информация не требуется, курьеру понадобится знать вес и габариты заказа, но вовсе не обязательно знать, что внутри. В свою очередь, если заказ передается на производство, то там не требуется информация о клиенте и о ценах. Для производства важно, то что требуется сделать, т.е. только сами товарные позиции. Также у понятия заказа в разных подразделениях может быть абсолютно различный жизненный цикл. Понятие заказ для разных подразделений отличается не только разными данными, но и различным поведением. Мы видим, что казалось бы одно и то же понятие может использоваться по-разному. Можно прийти к выводу, что такие понятия должны и моделироваться по-разному. В виде различных классов, размещенных в различных моделях.
Также если продолжить анализ задач наших подразделений, то непременно всплывут и такие понятия, которые никак не пересекаются и не накладываются друг на друга. Например, в службе приема заказов, может появиться понятие корзины покупателя, которое отсутствует как в производстве так и в доставке. В службе производства вполне может быть понятие материала или некого ресурса. А в службе доставки может существовать такое понятие как интервал доставки, которого также нет ни в одном из других подразделений.

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

Области задач и области решений

Вон Вернон рассматривает субдомены, как области бизнес-задач, а ограниченные контексты, как области решений.

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

Ограниченный контекст представляется как реализация узкоспециализированной модели, которая не пытается охватить все и сразу и в которой нет противоречий. Единый язык в данном случае является тем инструментом, который помогает этого достичь.

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

Ограниченный контекст — это то, что призвано улучшить доменную модель, сосредоточившись на лишь на одной подобласти. Это инструмент призванный ограничить размер модели.

Ограниченный контекст как способ декомпозиции системы

Идея ограниченного контекста это своего рода желание декомпозировать большую систему на более простые компоненты, с которыми понятней и более удобно работать. Также можно сказать, что данная идея реализует все те же принципы проектирования SRP, low coupling и high cohesion, но только на более высоком уровне. Об этом также говорит принцип CCP (Common Closure Principle), который похож на SRP, но только для классов, изменяющимся по одной и той же причине и следовательно должны находится вместе, например, в одном пакете. Также эта идея отлично согласуется с другими подходами, например, с микро сервисной архитектурой и с гибкими командами в Agile.

Закон Конвея

Говоря о декомпозиции систем вспоминается закон Конвея.

Организации проектируют системы, которые копируют структуру коммуникаций в этой организации

Даже когда я приводил пример с подразделениями организации, то невольно рассматривалась декомпозиция системы по бизнес-возможностям предприятия, которые уже структурированы определенным образом. Что на мой взгляд перекликается с законом Конвея.

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

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

Агрегаты

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

Приходилось ли вам в коде видеть что-то подобное?

payment.GetOrder().getAccount().getClient().getAddress() 

Данный код представляет своего рода довольно глубокий обход графа объектов нашей предметной модели. В нашей модели имеются несколько объектов-сущностей: Payment, Order, Account, Client И Address. И все эти объекты имеют некоторые связи друг с другом. B это довольно знакомая и распространенная ситуация. И само собой такая тесная связь между объектами вызывает и большую связанность самого кода. И это даже не говоря о том, что такая связь может быть не всегда обязательной и тем самым, подобный невнимательный обход объектов может вызывать исключение NullPointerException.

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

Агрегат как граница транзакционной согласованности

Когда говорят про агрегаты не редко упоминают транзакционную согласованность этих агрегированных объектов. Например, в качестве агрегата, можно рассмотреть корзину товаров. Корзина помимо своих основных свойств таких, как подытог, скидка и итоговая сумма содержит такие объекты как CartItem. Данный объект представляет элемент корзины и может содержать такие свойства, как добавленный товар и его количество, а также может вычислять подытог, как произведение количества на стоимость товара. Агрегат корзина (как и любой доменный объект) обеспечивает необходимые бизнес-инварианты (например, пересчет стоимости при добавление еще одного товара). Также очевидно, что при сохранении корзины должны одновременно сохраняться и ее элементы в рамках одной транзакции, что удовлетворяет транзакционной согласованности.

По этому при проектировании агрегата всегда можно задаться вопросом:

А должны ли эти объекты сохраняться вместе?

Агрегаты и границы ограниченных контекстов

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

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

Агрегаты и событийно-ориентированный архитектура

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

Заключение

В статье были рассмотрены на мой субъективный взгляд самые интересные подходы используемые в DDD. Несмотря на то, что Эванс представил свою книгу чуть ли не двадцать лет назад, до России все доходит и небольшим опозданием. И даже 20 лет спустя в нас DDD до сих пор имеет некую таинственность и массу непонимания. Надеюсь, что данная статья сможет внести свой небольшой вклад и прояснить некоторые моменты.

  • DDD
  • проектирование систем
  • доменная модель
  • архитектура приложений
  • Программирование
  • Анализ и проектирование систем
  • Проектирование и рефакторинг

Создаем доменную модель

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

Поскольку мы разрабатываем приложение для электронной коммерции, очевидно, самой главной доменной сущностью у нас будет товар. Создайте новую папку под названием Entities в проекте SportsStore.Domain , а в ней — новый класс C# под названием Product . Искомая структура показана на рисунке 7-4.

Рисунок 7-4: Создаем класс Product

Содержание класса Product вам уже известно, так как мы собираемся использовать тот же класс, что и в предыдущих главах. Отредактируйте файл класса Product , чтобы он соответствовал листингу 7-3.

Листинг 7-3: Файл класса Product

namespace SportsStore.Domain.Entities < public class Product < public int ProductID < get; set; >public string Name < get; set; >public string Description < get; set; >public decimal Price < get; set; >public string Category < get; set; >> >

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

Создаем абстрактное хранилище

Разумеется, нам нужно каким-то образом получать объекты Product из базы данных. Как мы уже объяснили в главе 3, мы хотим держать логику хранения изолированно от объектов доменной модели, и для этого мы будем использовать шаблон хранилища. Сейчас мы не будем думать о том, как мы собираемся реализовать хранение, и начнем с того, что определим для него интерфейс.

Создайте новую папку верхнего уровня в проекте SportsStore.Domain под названием Abstract и новый интерфейс под названием IProductsRepository , содержание которого показано в листинге 7-4. Чтобы добавить новый интерфейс, кликните правой кнопкой мыши папку Abstract , выберите Add — New Item и шаблон Interface .

Листинг 7-4: Файл интерфейса IProductRepository

using System.Linq; using SportsStore.Domain.Entities; namespace SportsStore.Domain.Abstract < public interface IProductRepository < IQueryableProducts < get; >> >

Здесь используется интерфейс IQueryable , который позволяет получить последовательность объектов Product и не требует указаний на то, как и где хранятся данные или как следует их извлекать. Класс, который использует интерфейс IProductRepository , может получить объекты Product , не зная того, где они содержатся или каким образом будут ему поставлены. Это и есть суть шаблона хранилища. Мы будем возвращаться к этому интерфейсу на протяжении всего процесса разработки, чтобы добавлять новые функции.

Создаем имитированное хранилище

Определив абстрактный интерфейс, мы можем реализовать механизм хранения и подключить его к базе данных. Мы сделаем это позже в этой главе. Чтобы начать писать другие части приложения, мы собираемся создать имитированную реализацию интерфейса IProductRepository . Это мы сделаем в методе AddBindings класса NinjectControllerFactory в проекте SportsStore.WebUI , как показано в листинге 7-5.

Листинг 7-5: Добавляем имитированную реализацию IProductRepository

using System; using System.Web.Mvc; using System.Web.Routing; using Ninject; using SportsStore.Domain.Entities; using SportsStore.Domain.Abstract; using System.Collections.Generic; using System.Linq; using Moq; namespace SportsStore.WebUI.Infrastructure < public class NinjectControllerFactory : DefaultControllerFactory < private IKernel ninjectKernel; public NinjectControllerFactory() < ninjectKernel = new StandardKernel(); AddBindings(); >protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) < return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); >private void AddBindings() < Mock mock = new Mock(); mock.Setup(m => m.Products).Returns(new List < new Product < Name = "Football", Price = 25 >, new Product < Name = "Surf board", Price = 179 >, new Product < Name = "Running shoes", Price = 95 >>.AsQueryable()); ninjectKernel.Bind().ToConstant(mock.Object); > > >

Для этого дополнения мы должны были добавить в файл несколько пространств имен, но процесс, который создает имитированное хранилище, использует те же самые техники Moq , которые мы рассмотрели в главе 4. AsQueryable является методом расширения LINQ, который преобразует IEnumerable в IQueryable . Это необходимо для соответствия сигнатуры интерфейса.

Мы используем метод ToConstant , потому что хотим, чтобы Ninject возвращал имитацию объекта, когда он получает запрос от реализации интерфейса IProductRepository :

ninjectKernel.Bind().ToConstant(mock.Object);

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

Что новенького на smarly.net

или RSS канал:

Как создать доменную модель

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

Анемичная доменная модель

Если ваши доменные объекты являются контейнерами данных и всё, что в них есть, это свойства get/set , то вы используете анемичную доменную модель. Её особенностью является то, что доменный объект не имеет поведения.

Сценарии использования, к примеру, интернет-магазина:

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

Начнем с анемичной модели данных. У нас будет класс Account :

public class Account < public int Id < get; set; >public bool IsApproved < get; set; >public DateTime? ActivationDate < get; set; >public List Orders < get; set; >>
public class Order < public int Id < get; set; >public int Price < get; set; >public Account Account < get; set; >public bool IsComplete < get; set; >>

Каждый из сценариев работы довольно просто реализовать:

Сценарий №1. Активация пользователя

account.ActivationDate = DateTime.Now; account.IsApproved = true;

Сценарий №2. Добавление заказа

account.Orders.Add(order); order.Account = account;

Сценарий №3. Подсчёт общей суммы

account.Orders .Where(order => order.IsComplete == false) .Sum(order => order.Price);

Главный вопрос: где будет располагаться этот код?

Есть самое простое и неправильное решение. Мы будем писать этот код прямо в обработчиках на aspx -страницах или WinForms:

public partial class Default : Page < protected void Page_Load(object sender, EventArgs e) < // выборка объекта account AccountOrdersSumLabel.Text = account.Orders .Where(order =>order.IsComplete == false) .Sum(order => order.Price); > protected void AddOrderButton_Click(object sender, EventArgs e) < // выборка объекта account account.Orders.Add(order); order.Account = account; // сохранение объекта account >>

Все будет хорошо, пока добавлять продукт можно только из этой формы, а подсчёт общей суммы происходит только по этой формуле. Проблемы начнутся, когда на другой форме потребуется такая же функциональность. Придется дублировать код. Тогда, при изменении логики работы, придется исправлять её во всех code-behind’ах.

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

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

public static class AccountHelper < public static void Activate(Account account) < account.ActivationDate = DateTime.Now; account.IsApproved = true; >public static void AddOrder(Account account, Order order) < account.Orders.Add(order); order.Account = account; >public static int CalculateOrdersSum(Account account) < return account.Orders .Where(order =>order.IsComplete == false) .Sum(order => order.Price); > >

Проблема классов с названием *Helper или *Manager в том, что они могут себе позволить делать всё, что угодно. Их абстрактные названия позволяют «помогать» классу Account делать абсолютно разные вещи. Такие классы со временем становятся God-object.

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

К тому же, это решение обладает всеми недостатками следующего.

Начнем бороться с сильной связанностью в коде. Сделаем всё правильно и создадим класс AccountService с интерфейсом IAccountService . Все объекты, которым понадобится активация или добавление заказа будут использовать интерфейс IAccountService вместо конкретной реализации. Это также поможет нам в тестировании кода.

public interface IAccountService < void Activate(Account account); void AddOrder(Account account, Order order); int CalculateOrdersSum(Account account); >public class AccountService : IAccountService < public void Activate(Account account) < account.ActivationDate = DateTime.Now; account.IsApproved = true; >public void AddOrder(Account account, Order order) < account.Orders.Add(order); order.Account = account; >public int CalculateOrdersSum(Account account) < return account.Orders .Where(order =>order.IsComplete == false) .Sum(order => order.Price); > >

Разобрались со связанностью и тестирование. Уже шаг вперёд. Но я вижу ещё две проблемы.

Функций типа AddOrder и CalculateOrdersSum будет довольно много. Через пол года разработки интерфейс IAccountService вырастит до 40-50 функций. «Загрязнение» интерфейса можно было бы пережить, если бы не вторая проблема.

В коде в любом месте можно в обход сервиса написать «свою активацию» пользователя. Например, взять объект Account из базы, выставить ему поле IsApproved в true и при этом забыть обновить поле ActivationDate . Тоже самое касается сценария добавления заказа. Можно вызвать функцию Add у свойства Orders где угодно и забыть выставить поле Account у добавляемого заказа. Это делает систему нестабильной. API приложения беззащитно перед пользователями системы. С таким подходом остается только надеятся, что программист найдёт нужную ему функцию в IAccountService , а не станет изобретать свой подход.

Поместим все эти функции в сам доменный объект Account . Обратите внимание на то, как изменились модификаторы доступа к полям объекта:

public class Account < private readonly Listorders; public Account() < orders = new List(); > public int Id < get; set; >public bool IsApproved < get; private set; >public DateTime? ActivationDate < get; private set; >public IEnumerable Orders < get < return orders; >> public int OrdersSum < get < return Orders .Where(order =>order.IsComplete == false) .Sum(order => order.Price); > > public void Activate() < ActivationDate = DateTime.Now; IsApproved = true; >public void AddOrder(Order order) < orders.Add(order); order.Account = this; >>

Теперь домен нашего приложения даёт пользователю готовое API, которое не требудет ни Helper ‘ов, ни сервисов. К тому же мы уберегаем пользователя от ошибок. Он уже не сможет активировать Account выставив только IsApproved . Теперь функция Activate сама заполнит нужные поля.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *