From e43efd840436cefe59a8553463d737da259589f5 Mon Sep 17 00:00:00 2001 From: eatae Date: Wed, 10 Apr 2024 13:12:00 +0300 Subject: [PATCH 1/2] =?UTF-8?q?Chapter4:=20=D0=95=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4,=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ru-RU/Chapter4/Entities.md | 1639 ++++++++++++++++++++++++++++++++++ ru-RU/Chapter4/codeBlock.php | 15 + ru-RU/Chapter4/q.sql | 7 + ru-RU/Chapter4/x.xml | 30 + 4 files changed, 1691 insertions(+) create mode 100644 ru-RU/Chapter4/Entities.md create mode 100644 ru-RU/Chapter4/codeBlock.php create mode 100644 ru-RU/Chapter4/q.sql create mode 100644 ru-RU/Chapter4/x.xml diff --git a/ru-RU/Chapter4/Entities.md b/ru-RU/Chapter4/Entities.md new file mode 100644 index 0000000..c36a7d9 --- /dev/null +++ b/ru-RU/Chapter4/Entities.md @@ -0,0 +1,1639 @@ +# Chapter4 +#BOOKS/DDD-IN-PHP + + +Глава 4. Сущности (Entities) +== + +-------- + +
+ + +Мы говорили о преимуществах попытки сначала смоделировать все в Домене как Объект Значения. Но при моделировании Домена, вероятно, возникнут ситуации, когда вы обнаружите, что какая-то концепция во повсеместном языке требует потока идентичности. + +
+ +## Введение + +
+Четкие примеры объектов, требующих идентификатора: + +- Человек. У человека всегда есть удостоверение личности, и оно всегда одинаково с точки зрения его имени или удостоверения личности. + +- Заказ в системе электронной коммерции. В таком контексте каждый новый созданный порядок имеет свою собственную идентичность, и она одинакова с течением времени. + +
+У этих понятий есть идентичность, которая неизменна со временем. Независимо от того, сколько раз данные в концепциях меняются, их идентичности остаются прежними. +Именно это делает их сущностями, а не Объектами Значений. С точки зрения реализации PHP, они были бы простыми классами. +Например, рассмотрим следующее в случае человека: +
+
+ +```php +namespace Ddd\Identity\Domain\Model; + +class Person +{ + private $identificationNumber; + private $firstName; + private $lastName; + public function __construct( + $anIdentificationNumber, $aFirstName, $aLastName + ) { + $this->identificationNumber = $anIdentificationNumber; + $this->firstName = $aFirstName; + $this->lastName = $aLastName; + } + public function identificationNumber() + { + return $this->identificationNumber; + } + public function firstName() + { + return $this->firstName; + } + public function lastName() + { + return $this->lastName; + } +} +} +``` +
+ + +Или рассмотрим следующее в случае заказа: + +```php +namespace Ddd\Billing\Domain\Model\Order; + +class Order +{ + private $id; + private $amount; + private $firstName; + private $lastName; + public function __construct( + $anId, Amount $amount, $aFirstName, $aLastName + ) { + $this->id = $anId; + $this->amount = $amount; + $this->firstName = $aFirstName; + $this->lastName = $aLastName; + } + public function id() + { + return $this->id; + } + public function firstName() + { + return $this->firstName; + } + public function lastName() + { + return $this->lastName; + } +} +``` + + +
+
+ + +## Объекты Vs. Примитивные типы +
+Большую часть времени Идентификатор Сущности представляется как примитивный тип - обычно строка или целое число. Но использование объекта Value для его представления имеет больше преимуществ: + +- Объекты Value являются неизменяемыми, поэтому их нельзя изменять. + +- Объекты-значения - это сложные типы, которые могут иметь пользовательское поведение, то, что примитивные типы не могут иметь. Возьмем в качестве примера операцию равенства. С помощью Value Objects операции равенства могут моделироваться и инкапсулироваться в свои собственные классы, делая концепции переходящими от неявных к явным. + +
+ +Рассмотрим возможную реализацию для OrderId, идентификатора заказа, который превратился в объект Value: + +```php +namespace Ddd\Billing\Domain\Model; + +class OrderId +{ + private $id; + public function __construct($anId) + { + $this->id = $anId; + } + public function id() + { + return $this->id; + } + public function equalsTo(OrderId $anOrderId) + { + return $anOrderId->id === $this->id; + } +} +``` +
+ + +Существует несколько реализаций, которые можно использовать для реализации идентификатора заказа. Приведенный выше пример довольно прост. +Как описано в главе 3, «Объекты значений», можно сделать метод __construct () частным и использовать статические фабричные методы для создания новых экземпляров. +Поговорите со своей командой, проведите эксперимент и договоритесь. Поскольку идентификаторы сущностей не являются сложными Объектами Значений, мы рекомендуем вам здесь не беспокоиться слишком много. + +
+Возвращаясь к Order, пришло время обновить ссылки на OrderId: + +```php +class Order +{ + private $id; + private $amount; + private $firstName; + private $lastName; + public function __construct( + OrderId $anOrderId, Amount $amount, $aFirstName, $aLastName + ) { + $this->id = $anOrderId; + $this->amount = $amount; + $this->firstName = $aFirstName; + $this->lastName = $aLastName; + } + public function id() + { + return $this->id; + } + public function firstName() + { + return $this->firstName; + } + public function lastName() + { + return $this->lastName; + } + public function amount() + { + return $this->amount; + } +} +``` + +- Наша сущность имеет идентификатор, смоделированный с использованием Value Object. + +Рассмотрим различные способы создания идентификатора заказа. + +
+
+ +## Операция идентификации + +
+Как было указано ранее, идентификатор сущности определяет его. Таким образом, обработка этого является важным аспектом Сущности. +Обычно существует четыре способа определения Identity of an Entity: механизм персистентности предоставляет Identity, клиент предоставляет Identity, +само приложение предоставляет Identity или другой ограниченный контекст предоставляет Identity. + +
+
+ +### Механизм хранения(БД) генерирует Идентификатор +
+Обычно самый простой способ генерации Identity - делегировать его механизму персистентности, потому что подавляющее большинство механизмов персистентности +поддерживают некую генерацию Identity - как атрибут `AUTO_INCREMENT` MySQL или последовательности Postgres и Oracle. +Это, хотя и просто, имеет главный недостаток: мы не будем иметь Идентичность Сущности, пока мы не сохраним ее. Таким образом, в некоторой степени, если мы идем с Mechanism Generated Identity, +мы свяжем операцию Identity с базовым хранилищем персистентности: + +```sql +CREATE TABLE `orders` ( + `id` int(11) NOT NULL auto_increment, + `amount` decimal (10,5) NOT NULL, + `first_name` varchar(100) NOT NULL, + `last_name` varchar(100) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` +
+ +И тогда мы могли бы рассмотреть этот код: + +```php +namespace Ddd\Identity\Domain\Model; + +class Person +{ + private $identificationNumber; + private $firstName; + private $lastName; + public function __construct( + $anIdentificationNumber, $aFirstName, $aLastName + ) { + $this->identificationNumber = $anIdentificationNumber; + $this->firstName = $aFirstName; + $this->lastName = $aLastName; + } + public function identificationNumber() + { + return $this->identificationNumber; + } + public function firstName() + { + return $this->firstName; + } + public function lastName() + { + return $this->lastName; + } +} +``` +
+ +Если вы когда-либо пытались создать собственный ORM, вы уже испытали эту ситуацию. + +Каков подход к созданию нового Person? Если база данных собирается создать идентификатор, нужно ли передавать его в конструктор? +Когда и где магия, которая обновит Person? Что произойдет, если мы в конечном итоге не будем сохранять Сущность? + +
+ +#### Суррогатная идентичность +
+Иногда при использовании ORM для отображения сущностей в хранилище сохраняемости накладываются некоторые ограничения - +например, доктрина требует целочисленное поле, если используется стратегия генератора IDENTITY. Это может привести к конфликту с моделью домена, если для нее требуется другой тип удостоверения. +Простейшим способом обработки такой ситуации является использование супертипа слоя, в котором помещается поле Identity, созданное для хранилища сохраняемости: + +```php +namespace Ddd\Common\Domain\Model; + +abstract class IdentifiableDomainObject +{ + private $id; + protected function id() + { + return $this->id; + } + protected function setId($anId) + { + $this->id = $anId; + } +} +``` + +
+ +```php +namespace Acme\Billing\Domain; + +use Acme\Common\Domain\IdentifiableDomainObject; + +class Order extends IdentifiableDomainObject +{ + private $orderId; + public function orderId() + { + if (null === $this->orderId) { + $this->orderId = new OrderId($this->id()); + } + return $this->orderId; + } +} +``` + +
+
+ +#### Active Record Vs. Data Mapper для Богатых Доменных Моделей +
+Каждый проект всегда сталкивается с решением, какой ORM должен использоваться. Там много хороших ОRM для PHP: Doctrine, Propel, Eloquent, Paris, и многое другое. + +Большинство из них являются реализациями Active Record. Реализация Active Record в основном подходит для приложений CRUD, но она не является идеальным решением для моделей Rich Domain по следующим причинам: + +- Шаблон активной записи предполагает связь «один к одному» между сущностью и таблицей базы данных. Таким образом, она связывает конструкцию базы данных с конструкцией объектной системы. А в модели Rich Domain иногда Сущности строятся с информацией, которая может поступать из разных источников данных. + +- Продвинутые вещи, такие как коллекции и наследование, сложно реализовать. + +- Большинство реализаций вынуждают использовать посредством наследования некие конструкции, которые навязывают несколько конвенций. Это может привести к постоянной утечке в модель домена путем соединения модели домена с ORM. Единственная реализация Active Record, которая не навязывает наследование от базового класса, -Castle ActiveRecord от Castle Project, .NET framework. В то время как это приводит к некоторой степени разделения между устойчивостью и проблемами Домена в созданных Сущностях, это не отделяет детали устойчивости низкого уровня от конструкции домена высокого уровня. + +
+ +Как упоминалось в предыдущей главе, в настоящее время лучшим ORM для PHP является Doctrine, которая является реализацией шаблона Отображения Данных. Data Mapper отделяет проблемы персистентности от проблем домена, что приводит к появлению сущностей, свободных от персистентности. Это делает инструмент лучшим для тех, кто хочет построить модель Rich Domain. + + +### Клиент предоставляет Идентификатор + +Иногда при работе с определенными Доменами Идентификация приходят естественным образом, когда клиент использует Модель Домена. Это, скорее всего, идеальный случай, потому что Идентичность может быть легко смоделирована. Рассмотрим рынок продажи книг: + +```php +namespace Ddd\Catalog\Domain\Model\Book; + +class ISBN +{ + private $isbn; + private function __construct($anIsbn) + { + $this->setIsbn($anIsbn); + } + private function setIsbn($anIsbn) + { + $this->assertIsbnIsValid($anIsbn, 'The ISBN is invalid.'); + $this->isbn = $anIsbn; + } + public static function create($anIsbn) + { + return new static($anIsbn); + } + private function assertIsbnIsValid($anIsbn, $string) + { + // ... Validates an ISBN code + } +} +``` + + +Согласно Википедии: Международный стандартный номер книги (ISBN) является уникальным числовым коммерческим идентификатором книги. ISBN назначается каждому изданию и варианту (за исключением повторной печати) книги. Например, электронная книга, обратная страница и печатное издание одной и той же книги будут иметь разные ISBN. Длина ISBN составляет 13 цифр, если она назначена 1 января 2007 года или позднее, и 10 цифр, если она назначена до 2007 года. Метод присвоения ISBN является национальным и варьируется от страны к стране, часто в зависимости от того, насколько велика издательская индустрия в стране. + +Самое интересное в ISBN то, что он уже определен в Домене, это действительный идентификатор, потому что он уникален, и его можно легко проверить. Это хороший пример идентификатора, предоставленного клиентом: + +```php +namespace Ddd\Catalog\Domain\Model\Book; + +class Book +{ + private $isbn; + private $title; + public function __construct(ISBN $anIsbn, $aTitle) + { + $this->isbn = $anIsbn; + $this->title = $aTitle; + } +} +``` + +Теперь, это простой вопрос использования его: + +```php +$book = new Book( + ISBN::create('...'), + 'Domain-Driven Design in PHP' +); +``` + + + +### Приложение создаёт Идентификатор + +Если клиент не может предоставить идентификатор в целом, предпочтительным способом обработки операции идентификации является разрешение приложению генерировать идентификаторы, обычно через UUID. Это наш рекомендуемый подход в случае, если у вас нет сценария, как показано в предыдущем разделе. + +==Согласно Википедии:== +Цель UUID - дать возможность распределенным системам однозначно идентифицировать информацию без существенной централизованной координации. В этом контексте слово «уникальный» следует понимать как практически уникальный, а не как гарантированный уникальный. Поскольку идентификаторы имеют конечный размер, два различных элемента могут совместно использовать один и тот же идентификатор. Это форма хеш-коллизии. Размер идентификатора и процесс генерации должны быть выбраны так, чтобы сделать это достаточно маловероятным на практике. Любой может создать UUID и использовать его для идентификации чего-либо с разумной уверенностью, что тот же идентификатор никогда не будет непреднамеренно создан кем-либо для идентификации чего-либо другого. Поэтому информация, помеченная UUID, может быть впоследствии объединена в единую базу данных без необходимости разрешения конфликтов идентификаторов (ID). + + +> Существует несколько библиотек в PHP, которые генерируют UUID, и они могут быть найдены на Packagist: https ://packagist.org/search/?q=uuid. +> Лучшая рекомендация - разработанная Беном Рэмзи: https ://github.com/ramsey/uuid +> потому что имеет много наблюдателей на GitHub и миллионы установок на Packagist. + + +Предпочтительным местом для создания Идентификатора будет Репозиторий (подробнее об этом в Главе 10. Пример Репозитория: + +```php +namespace Ddd\Billing\Domain\Model\Order; + +interface OrderRepository +{ + public function nextIdentity(); + public function add(Order $anOrder); + public function remove(Order $anOrder); +} +``` + + +При использовании Доктрины необходимо создать пользовательский Репозиторий, реализующий такой интерфейс. Он в основном создает новый Идентификатор и использует EntityManager для сохранения и удаления сущностей. Небольшое изменение состоит в том, чтобы поместить реализацию nextIdentity в интерфейс, который станет абстрактным классом: + +```php +namespace Ddd\Billing\Infrastructure\Domain\Model\Order; + +use Ddd\Billing\Domain\Model\Order\Order; +use Ddd\Billing\Domain\Model\Order\OrderId; +use Ddd\Billing\Domain\Model\Order\OrderRepository; +use Doctrine\ORM\EntityRepository; + +class DoctrineOrderRepository + extends EntityRepository + implements OrderRepository +{ + public function nextIdentity() + { + return OrderId::create(); + } + public function add(Order $anOrder) + { + $this->getEntityManager()->persist($anOrder); + } + public function remove(Order $anOrder) + { + $this->getEntityManager()->remove($anOrder); + } +} +``` + +Давайте быстро рассмотрим конечную реализацию объекта OrderId Value Object: + +```php +namespace Ddd\Billing\Domain\Model\Order; + +use Ramsey\Uuid\Uuid; + +class OrderId +{ + private $id; + private function __construct($anId = null) + { + $this->id = $id ? :Uuid::uuid4()->toString(); + } + public static function create($anId = null ) + { + return new static($anId); + } +} +``` + +Основная проблема, связанная с этим подходом, как будет показано в следующих разделах, заключается в том, насколько просто сохранять объекты, содержащие объекты значений. Однако сопоставление встроенных объектов значений, находящихся внутри сущности, может быть сложным в зависимости от ORM. + + +### Другой ограниченный контекст генерирует идентификатор + +Вероятно, это наиболее сложная стратегия создания Identity, поскольку она вынуждает локальную сущность зависеть не только от локальных событий ограниченного контекста, но и от внешних событий ограниченного контекста. Так что с точки зрения технического обслуживания стоимость была бы высокой. + +Другой ограниченный контекст предоставляет интерфейс для выбора идентификатора из локальной сущности. Некоторые из экспонируемых свойств могут восприниматься как собственные. + +Когда необходима синхронизация между сущностями ограниченных контекстов, она обычно может быть достигнута с помощью управляемой событиями архитектуры в каждом из ограниченных контекстов, которые должны быть уведомлены при изменении исходной сущности. + + + + +## Сохраняющиеся сущности + +В настоящее время, как обсуждалось ранее в главе, лучшим инструментом для сохранения состояния сущности в постоянном хранилище является доктрина ORM. Доктрина имеет несколько способов определения метаданных Сущности: с помощью аннотаций в коде Сущности, XML, YAML или простого PHP. В этой главе мы подробно обсудим, почему аннотации не являются лучшими для использования при сопоставлении сущностей. + + +### Настройка доктрины + +Прежде всего, мы должны установить Доктрину через Composer. В корневой папке проекта должна быть выполнена следующая команда: + +> php composer.phar require "doctrine/orm=^2.5" + +Затем эти строки позволяют настроить доктрину: + +```php +require_once '/path/to/vendor/autoload.php'; + +use Doctrine\ORM\Tools\Setup; +use Doctrine\ORM\EntityManager; + +$paths = ['/path/to/entity-files']; +$isDevMode = false; + +// the connection configuration +$dbParams = [ + 'driver' => 'pdo_mysql', + 'user' => 'the_database_username', + 'password' => 'the_database_password', + 'dbname' => 'the_database_name' +]; + +$config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode); +$entityManager = EntityManager::create($dbParams, $config); +``` + + + +### Маппинг Entities + +По умолчанию в документации доктрины представлены примеры кода с использованием аннотаций. Итак, мы начинаем пример кода, используя аннотации и обсуждая, почему их следует избегать, когда это возможно. + +Для этого мы вернем класс Order, обсуждавшийся ранее в этой главе. + + +#### Маппинг Entities с помощью аннотированного кода + +Когда была выпущена доктрина, броским способом отображения объектов в примерах кода было использование аннотаций. + +**Что такое аннотация?** +Аннотация - это особая форма метаданных. В PHP он помещен в комментарии к исходному коду. Например, PHPDocumentor использует аннотации для построения информации API, а PHPUnit использует некоторые аннотации для указания поставщиков данных или для предоставления ожиданий относительно исключений, порождаемых фрагментом кода: + +```php +class SumTest extends PHPUnit_Framework_TestCase +{ + /** @dataProvider aMethodName */ + public function testAddition() { + //... + } +} +``` + +Чтобы сопоставить объект Order с хранилищем, исходный код Order должен быть изменен для добавления аннотаций доктрины: + +```php +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\GeneratedValue; +use Doctrine\ORM\Mapping\Column; + +/** @Entity */ +class Order { + + /** @Id @GeneratedValue(strategy="AUTO") */ + private $id; + + /** @Column(type="decimal", precision="10", scale="5") */ + private $amount; + + /** @Column(type="string") */ + private $firstName; + + /** @Column(type="string") */ + private $lastName; + + public function __construct( + Amount $anAmount, + $aFirstName, + $aLastName + ) { + $this->amount = $anAmount; + $this->firstName = $aFirstName; + $this->lastName = $aLastName; + } + public function id() + { + return $this->id; + } + public function firstName() + { + return $this->firstName; + } + public function lastName() + { + return $this->lastName; + } + public function amount() + { + return $this->amount; + } +} + +``` + + +Затем, чтобы сохранить Сущность в постоянном хранилище, так же легко сделать следующее: + +```php +$order = new Order( + new Amount(15, Currency::EUR()), + 'AFirstName', + 'ALastName' +); +$entityManager->persist($order); +$entityManager->flush(); + +``` + + +На первый взгляд, этот код выглядит просто, и это может быть простым способом указать информацию сопоставления. Но это имеет определенную цену. Что странного в окончательном коде? + +Во-первых, проблемы домена смешиваются с проблемами инфраструктуры. Order является концепцией домена, тогда как Table, Column и т. д. являются проблемами инфраструктуры. + +В результате эта Сущность тесно связана с информацией отображения, указанной аннотациями в исходном коде. Если бы Сущность требовалось сохранить с помощью другого менеджера Сущности и с другими метаданными сопоставления, это было бы невозможно. + +Аннотации, как правило, приводят к побочным эффектам и плотной связи, поэтому их лучше не использовать. + +Так какой лучший способ указать информацию о сопоставлении? Лучший способ - это способ, позволяющий отделить информацию о сопоставлении от самой сущности. Это может быть достигнуто с помощью сопоставления XML, сопоставления YAML или PHP. В этой книге мы рассмотрим сопоставление XML. + + +#### Маппинг Entities с помощью XML + +Чтобы отобразить объект Order с помощью сопоставления XML, код настройки доктрины должен быть слегка изменен: + +```php + +require_once '/path/to/vendor/autoload.php'; + +use Doctrine\ORM\Tools\Setup; +use Doctrine\ORM\EntityManager; + +$paths = ['/path/to/mapping-files']; +$isDevMode = false; + +// the connection configuration +$dbParams = [ + 'driver' => 'pdo_mysql', + 'user' => 'the_database_username', + 'password' => 'the_database_password', + 'dbname' => 'the_database_name', +]; + +$config = Setup::createXMLMetadataConfiguration($paths, $isDevMode); +$entityManager = EntityManager::create($dbParams, $config); + +``` + +Файл сопоставления должен быть создан по пути, по которому доктрина будет выполнять поиск файлов сопоставления, а файлы сопоставления должны быть названы в честь полного имени класса, заменяя обратную косую черту \ точками. + +Рассмотрим следующее: +`Acme\Billing\Domain\Model\Order` +- файл сопоставления будет иметь имя: + `Acme.Billing.Domain.Model.Order.dcm.xml` + +Кроме того, удобно, чтобы все файлы сопоставления использовали специальную XML-схему, созданную специально для указания информации сопоставления: +https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd + + +#### Сопоставление Идентификатора Сущности + +Наш идентификатор, OrderId, является объектом значения. Как видно из предыдущей главы, существуют различные подходы к отображению объекта Value Object с использованием доктрины, встраиваемых объектов и пользовательских типов. +Если в качестве идентификаторов используются объекты значений, лучшим вариантом является использование пользовательских типов. + +Интересной новой особенностью Doctrine 2.5 является то, что теперь можно использовать Объекты в качестве идентификаторов для Сущностей, если они реализуют магический метод __toString (). Таким образом, мы можем добавить __toString к нашим объектам Identity Value Object и использовать их в наших сопоставлениях: + +```php +namespace Ddd\Billing\Domain\Model\Order; + +use Ramsey\Uuid\Uuid; + +class OrderId +{ +// ... + public function __toString() + { + return $this->id; + } +} + +``` + +Проверьте реализацию пользовательских типов доктрины. Они наследуют GuidType, поэтому их внутреннее представление будет UUID. Необходимо указать собственный перевод базы данных. Затем необходимо зарегистрировать пользовательские типы, прежде чем использовать их. Если требуется справка по этим шагам, то [Custom Mapping Types](https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/cookbook/custom-mapping-types.html) являются хорошей ссылкой. + +```php +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\GuidType; + +class DoctrineOrderId extends GuidType +{ + public function getName() + { + return 'OrderId'; + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $value->id(); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return new OrderId($value); + } +} + +``` + + +Наконец, мы настроим регистрацию пользовательских типов. Опять же, мы должны обновить начальную загрузку: + +```php +require_once '/path/to/vendor/autoload.php'; +// ... + +\Doctrine\DBAL\Types\Type::addType( + 'OrderId', + 'Ddd\Billing\Infrastructure\Domain\Model\DoctrineOrderId' +); + +$config = Setup::createXMLMetadataConfiguration($paths, $isDevMode); +$entityManager = EntityManager::create($dbParams, $config); + +``` + + + +#### Окончательный файл сопоставления + +Со всеми изменениями, мы наконец-то готовы, так что давайте посмотрим на окончательный файл сопоставления. +Наиболее интересной детализацией является проверка того, как идентификатор сопоставляется с нашим определенным пользовательским типом для OrderId: + +```xml + + + + + + + + + + +``` + + + +## Тестирование Entities + +Относительно легко протестировать Сущности, просто потому, что они являются простыми старыми PHP-классами с действиями, производными от концепции Домена, которую они представляют. В центре внимания теста должны быть инварианты, которые защищает Сущность, потому что поведение на Сущностях, вероятно, будет смоделировано вокруг этих инвариантов. + +Например, для простоты предположим, что для блога необходима модель домена. Один из возможных вариантов мог бы заключаться в следующем: + +```php +class Post +{ + private $title; + private $content; + private $status; + private $createdAt; + private $publishedAt; + + public function __construct($aContent, $title) + { + $this->setContent($aContent); + $this->setTitle($title); + $this->unpublish(); + $this->createdAt(new DateTimeImmutable()); + } + + private function setContent($aContent) + { + $this->assertNotEmpty($aContent); + $this->content = $aContent; + } + + private function setTitle($aPostTitle) + { + $this->assertNotEmpty($aPostTitle); + $this->title = $aPostTitle; + } + + private function setStatus(Status $aPostStatus) + { + $this->assertIsAValidPostStatus($aPostStatus); + $this->status = $aPostStatus; + } + + private function createdAt(DateTimeImmutable $aDate) + { + $this->assertIsAValidDate($aDate); + $this->createdAt = $aDate; + } + + private function publishedAt(DateTimeImmutable $aDate) + { + $this->assertIsAValidDate($aDate); + $this->publishedAt = $aDate; + } + + public function publish() + { + $this->setStatus(Status::published()); + $this->publishedAt(new DateTimeImmutable()); + } + + public function unpublish() + { + $this->setStatus(Status::draft()); + $this->publishedAt = null ; + } + + public function isPublished() + { + return $this->status->equalsTo(Status::published()); + } + + public function publicationDate() + { + return $this->publishedAt; + } +} + +``` + + +```php +class Status +{ + const PUBLISHED = 10; + const DRAFT = 20; + private $status; + public static function published() + { + return new self(self::PUBLISHED); + } + public static function draft() + { + return new self(self::DRAFT); + } + private function __construct($aStatus) + { + $this->status = $aStatus; + } + public function equalsTo(self $aStatus) + { + return $this->status === $aStatus->status; + } +} + +``` + + +Чтобы проверить эту модель домена, мы должны убедиться, что тест охватывает все Post инварианты: + +```php +class PostTest extends PHPUnit_Framework_TestCase +{ + /** @test */ + public function aNewPostIsNotPublishedByDefault() + { + $aPost = new Post( + 'A Post Content', + 'A Post Title' + ); + $this->assertFalse( + $aPost->isPublished() + ); + $this->assertNull( + $aPost->publicationDate() + ); + } + + /** @test */ + public function aPostCanBePublishedWithAPublicationDate() + { + $aPost = new Post( + 'A Post Content', + 'A Post Title' + ); + $aPost->publish(); + $this->assertTrue($aPost->isPublished()); + $this->assertInstanceOf( + 'DateTimeImmutable', + $aPost->publicationDate() + ); + } +} + +``` + + + +### Дата Время (DateTimes) + +Поскольку DateTimes широко используется в сущностях, мы считаем важным указать конкретные подходы к единицам тестирования сущностей, которые имеют поля с типами дат. Учтите, что Пост является новым, если он был создан в течение последних 15 дней: + +```php +class Post +{ + const NEW_TIME_INTERVAL_DAYS = 15; + // ... + private $createdAt; + + public function __construct($aContent, $title) + { + // ... + $this->createdAt(new DateTimeImmutable()); + } + + // ... + public function isNew() + { + return + (new DateTimeImmutable()) + ->diff($this->createdAt) + ->days <= self::NEW_TIME_INTERVAL_DAYS; + } +} + +``` + +Метод isNew () должен сравнивать два значения DateTimes. Это сравнение между датой создания Post и сегодняшней датой. Мы вычисляем разницу и проверяем, меньше ли она указанного количества дней. Как выполнить единичное тестирование метода isNew ()? Как мы продемонстрировали в реализации, трудно воспроизвести конкретные потоки в наших тестовых комплектах. Какие у нас есть варианты? + + +#### Передача все дат в виде параметров + +Одним из возможных вариантов может быть передача всех дат в качестве параметров при необходимости: + +```php +class Post +{ + // ... + public function __construct($aContent, $title, $createdAt = null) + { + // ... + $this->createdAt($createdAt ?: new DateTimeImmutable()); + } + + // ... + public function isNew($today = null) + { + return + ($today ? :new DateTimeImmutable()) + ->diff($this->createdAt) + ->days <= self::NEW_TIME_INTERVAL_DAYS; + } +} + +``` + +Это самый простой подход для целей модульного тестирования. Просто проходите различные пары дат, чтобы проверить все возможные сценарии со 100-процентным охватом. Однако, если учесть код клиента, который создает и запрашивает результат метода isNew (), все выглядит не так хорошо. Полученный в результате код может быть немного странным из-за постоянной передачи текущего DateTime: + +```php +$aPost = new Post( + 'Hello world!', + 'Hi', + new DateTimeImmutable() +); +$aPost->isNew( + new DateTimeImmutable() +); + +``` + + +#### Класс тестирования + +Другой альтернативой является использование шаблона Test Class. Идея состоит в том, чтобы расширить класс Post новым, которым мы можем манипулировать, чтобы форсировать определенные сценарии. Этот новый класс будет использоваться только в целях модульного тестирования. Плохая новость в том, что мы должны немного изменить исходный класс Post, извлекая некоторые методы и изменяя некоторые поля и методы с частных на защищенные. Некоторые разработчики могут беспокоиться об увеличении видимости свойств класса только из-за причин тестирования. Однако мы считаем, что в большинстве случаев это того стоит: + +```php +class Post +{ + protected $createdAt; + public function isNew() + { + return + ($this->today()) + ->diff($this->createdAt) + ->days <= self::NEW_TIME_INTERVAL_DAYS; + } + protected function today() + { + return new DateTimeImmutable(); + } + protected function createdAt(DateTimeImmutable $aDate) + { + $this->assertIsAValidDate($aDate); + $this->createdAt = $aDate; + } +} + +``` + + +Как вы видите, мы извлекли логику для получения сегодняшней даты в метод today (). Таким образом, применяя шаблон метода, можно изменить его поведение от производного класса. Нечто подобное происходит с методом и полем createAt. Теперь они защищены, поэтому их можно использовать и переопределять в производных классах: + +```php +class PostTestClass extends Post +{ + private $today; + + protected function today() + { + return $this->today; + } + public function setToday($today) + { + $this->today = $today; + } +} + +``` + +С учетом этих изменений теперь мы можем протестировать наш исходный класс Post с помощью тестирования PostTestClass: + +```php +class PostTest extends PHPUnit_Framework_TestCase +{ + // ... + + /** @test */ + public function aPostIsNewIfIts15DaysOrLess() + { + $aPost = new PostTestClass( + 'A Post Content', + 'A Post Title' + ); + $format = 'Y-m-d'; + $dateString = '2016-01-01'; + + $createdAt = DateTimeImmutable::createFromFormat( + $format, + $dateString + ); + + $aPost->createdAt($createdAt); + + $aPost->setToday( + $createdAt->add( + new DateInterval('P15D') + ) + ); + + $this->assertTrue($aPost->isNew()); + + $aPost->setToday( + $createdAt->add( + new DateInterval('P16D') + ) + ); + + $this->assertFalse($aPost->isNew()); + } +} + +``` + +Последняя деталь: при таком подходе невозможно достичь 100-процентного покрытия в классе Post, потому что метод today () никогда не будет выполняться. Однако он может быть охвачен другими тестами. + + +#### External Fake + +Другой вариант заключается в переносе вызовов конструктораDateTimeImmutable или именованных конструкторов с использованием нового класса и некоторых статических методов. При этом мы можем статически изменить результат этих методов, чтобы вести себя по-разному на основе конкретных сценариев тестирования: + +```php +class Post +{ + // ... + private $createdAt; + public function __construct($aContent, $title) + { + // ... + $this->createdAt(MyCustomDateTimeBuilder::today()); + } + // ... + public function isNew() + { + return + (MyCustomDateTimeBuilder::today()) + ->diff($this->createdAt) + ->days <= self::NEW_TIME_INTERVAL_DAYS; + } +} + +``` + +Для получения текущегоDateTime теперь используется статический вызов MyCustomDateTimeBuilder:: today (). Этот класс также имеет несколько методов установки, чтобы подделать результат для возврата в следующих вызовах: + +```php +class PostTest extends PHPUnit_Framework_TestCase +{ + // ... + /** @test */ + public function aPostIsNewIfIts15DaysOrLess() + { + $createdAt = DateTimeImmutable::createFromFormat( + 'Y-m-d', + '2016-01-01' + ); + MyCustomDateTimeBuilder::setReturnDates( + [ + $createdAt, + $createdAt->add( + new DateInterval('P15D') + ), + $createdAt->add( + new DateInterval('P16D') + ) + ] + ); + $aPost = new Post( + 'A Post Content' , + 'A Post Title' + ); + $this->assertTrue( + $aPost->isNew() + ); + $this->assertFalse( + $aPost->isNew() + ); + } +} + +``` + + +Основная проблема этого подхода заключается в том, что он статически связан с объектом. +В зависимости от вашего варианта использования, также будет сложно создать гибкий поддельный объект. + + +#### Рефлексия + +Можно также использовать методы Reflection для построения нового класса Post с пользовательскими датами. +Рассмотрим [Mimic](https://github.com/keyvanakbary/mimic), простую функциональную библиотеку для прототипирования объектов, гидратации данных и отображения данных: + +```php +namespace Domain; + +use mimic as m; + +class ComputerScientist { + private $name; + private $surname; + + public function __construct($name, $surname) { + $this->name = $name; + $this->surname = $surname; + } + + public function rocks() { + return $this->name . ' ' . $this->surname . ' rocks!'; + } +} + +assert(m\prototype('Domain\ComputerScientist') instanceof Domain\ComputerScientist); + +m\hydrate('Domain\ComputerScientist', array( + 'name' => 'John', + 'surname' => 'McCarthy' +))->rocks(); //John McCarthy rocks! + +assert(m\expose(new Domain\ComputerScientist('Grace', 'Hopper')) == array( + 'name' => 'Grace', + 'surname' => 'Hopper' +)); + +``` + + +> **Поделиться и обсудить** +>Обсудите с коллегами по работе, как правильно протестировать объекты с фиксированнымидатами и придумайте дополнительные альтернативы. + +Если вы хотите узнать больше о тестировании шаблонов и подходов, ознакомьтесь с книгойxUnit Test Patterns: Refactoring Test Code by Gerard Meszaros. + + + +## Валидация + +Валидация является очень важным процессом в нашей модели домена. Она проверяет не только правильность атрибутов, но и правильность целых объектов и их состава. Для поддержания этой модели в допустимом состоянии требуются различные уровни проверки. То, что объект состоит из допустимых атрибутов (базовых), не обязательно означает, что объект (в целом) является действительным. И наоборот: действительные объекты не обязательно равны действительным составам. + + +### Валидация атрибутов + +Некоторые люди понимают проверку как процесс, посредством которого служба проверяет состояние данного объекта. В этом случае проверка соответствует подходу Design-by-contract, который состоит из предварительных условий, постусловий и инвариантов. Одним из таких способов защиты отдельного атрибута является использование главы 3 «Объекты значений». Чтобы сделать наш дизайн более гибким для изменений, мы концентрируемся только на утверждении предварительных условий Домена, которые должны быть выполнены. Здесь мы будем использовать охрану как простой способ проверки предварительных условий: + +```php +class Username +{ + const MIN_LENGTH = 5; + const MAX_LENGTH = 10; + const FORMAT = '/^[a-zA-Z0-9_]+$/'; + private $username; + + public function __construct($username) + { + $this->setUsername($username); + } + + private function setUsername($username) + { + $this->assertNotEmpty($username); + $this->assertNotTooShort($username); + $this->assertNotTooLong($username); + $this->assertValidFormat($username); + $this->username = $username; + } + + private function assertNotEmpty($username) + { + if (empty($username)) { + throw new InvalidArgumentException('Empty username'); + } + } + + private function assertNotTooShort($username) + { + if (strlen($username) < self::MIN_LENGTH) { + throw new InvalidArgumentException(sprintf( + 'Username must be %d characters or more', + self::MIN_LENGTH + )); + } + } + + private function assertNotTooLong($username) + { + if (strlen($username) > self::MAX_LENGTH) { + throw new InvalidArgumentException(sprintf( + 'Username must be %d characters or less', + self::MAX_LENGTH + )); + } + } + + private function assertValidFormat($username) + { + if (preg_match(self:: FORMAT, $username) !== 1) { + throw new InvalidArgumentException( + 'Invalid username format' + ); + } + } +} + +``` + + +Как видно из приведенного выше примера, для создания объекта «Значение имени пользователя» необходимо выполнить четыре предварительных условия. Оно: + +- Не должно быть пустым +- Должно содержать не менее 5 символов +- Должно содержать менее 10 символов +- Должен иметь формат буквенно-цифровых символов или знаков подчеркивания + +Если все предварительные условия выполнены, атрибут будет установлен, и объект будет успешно построен. В противном случае будет создано исключение InvalidArgumentException, выполнение будет остановлено, и клиенту будет выдана ошибка. + +Некоторые разработчики могут рассмотреть этот вид проверочного защитного программирования. Однако мы не проверяем, является ли ввод строкой или что значения null не разрешены. Мы не можем избегать людей, использующих наш код неправильно, но мы можем контролировать правильность нашего состояния Домена. Как видно из главы 3, «Объекты значений», проверка также может помочь нам в обеспечении безопасности. + +[Defensive programming](https://en.wikipedia.org/wiki/Defensive_programming) не плохая вещь. В общем, это имеет смысл при разработке компонентов или библиотек, которые будут использоваться в качестве третьей стороны в других проектах. +Однако при разработке собственного ограниченного контекста этих дополнительных параноидных проверок (нулей, основных типов, подсказок типа и т. д.) можно избежать, чтобы увеличить скорость разработки, полагаясь на охват набора модульных тестов. + + +### Проверка всего объекта + +Бывают случаи, когда объект, состоящий из допустимых свойств в целом, все еще может считаться недействительным. Может быть заманчиво добавить этот вид проверки к самому объекту, но обычно это ==антипаттерн==. Проверка на более высоком уровне изменяется в ритме, отличном от ритма самой логики объекта. Кроме того, рекомендуется разделять эти обязанности. +Проверка информирует клиента о любых обнаруженных ошибках или собирает результаты для последующего просмотра, так как иногда мы не хотим останавливать выполнение при первом признаке неисправности. + +Абстрактный и повторно используемый валидатор может быть примерно таким: + +```php +abstract class Validator +{ + private $validationHandler; + + public function __construct(ValidationHandler $validationHandler) + { + $this->validationHandler = $validationHandler; + } + protected function handleError($error) + { + $this->validationHandler->handleError($error); + } + abstract public function validate(); +} + +``` + +В качестве конкретного примера мы хотим проверить весь объект Location, состоящий из допустимых объектов Country, City и Postcode Value Objects. Однако эти отдельные значения могут находиться в невалидном состоянии во время проверки. Может быть, город не является частью страны, или почтовый индекс не соответствует формату города: + +```php +class Location +{ + private $country; + private $city; + private $postcode; + public function __construct( + Country $country, City $city, Postcode $postcode + ) { + $this->country = $country; + $this->city = $city; + $this->postcode = $postcode; + } + public function country() + { + return $this->country; + } + public function city() + { + return $this->city; + } + public function postcode() + { + return $this->postcode; + } +} +``` + +Validator проверяет состояние объекта Location в целом, анализируя значение взаимосвязей между свойствами: + +```php +class LocationValidator extends Validator +{ + private $location; + public function __construct( + Location $location, ValidationHandler $validationHandler + ) { + parent:: __construct($validationHandler); + $this->location = $location; + } + public function validate() + { + if (!$this->location->country()->hasCity( + $this->location->city() + )) { + $this->handleError('City not found'); + } + if (!$this->location->city()->isPostcodeValid( + $this->location->postcode() + )) { + $this->handleError('Invalid postcode'); + } + } +} + +``` + +После установки всех свойств мы сможем проверить Сущность, скорее всего, после некоторых описанных процессов. На поверхности это выглядит так, как если бы местоположение подтвердило себя. Однако это не так. Класс Location делегирует эту проверку конкретному экземпляру валидатора, разделяя эти две четкие обязанности: + +```php +class Location +{ + // ... + public function validate(ValidationHandler $validationHandler) + { + $validator = new LocationValidator($this, $validationHandler); + $validator->validate(); + } +} + +``` + + +#### Decoupling сообщений валидации + +С некоторыми незначительными изменениями в существующей реализации мы можем отделить сообщения проверки от средства проверки: + +```php +class LocationValidationHandler implements ValidationHandler +{ + public function handleCityNotFoundInCountry(); + public function handleInvalidPostcodeForCity(); +} +``` + +```php +class LocationValidator +{ + private $location; + private $validationHandler; + + public function __construct( + Location $location, + LocationValidationHandler $validationHandler + ) { + $this->location = $location; + $this->validationHandler = $validationHandler; + } + public function validate() + { + if (!$this->location->country()->hasCity( + $this->location->city() + )) { + $this->validationHandler->handleCityNotFoundInCountry(); + } + if (! $this->location->city()->isPostcodeValid( + $this->location->postcode() + )) { + $this->validationHandler->handleInvalidPostcodeForCity(); + } + } +} + +``` + +Нам также необходимо изменить подпись метода проверки следующим образом: + +```php +class Location +{ + // ... + public function validate( + LocationValidationHandler $validationHandler + ) { + $validator = new LocationValidator($this, $validationHandler); + $validator->validate(); + } +} +``` + + + +### Валидация композиции объектов + +Проверка достоверности композиций объектов может быть сложной задачей. Таким образом, предпочтительным способом достижения этого является использование доменной службы. Затем служба взаимодействует с репозиториями, чтобы получить действительный агрегат. Из-за возможных сложных графов объектов, которые могут быть созданы, агрегат может находиться в промежуточном состоянии, требуя предварительной проверки других агрегатов. +События домена можно использовать для уведомления других частей системы о проверке определенного элемента. + + +### Entities и Events домена + +Мы рассмотрим главу 6 «Доменные события» в будущем; однако важно подчеркнуть, что операции, выполняемые с сущностями, могут инициировать события домена. Этот подход используется для передачи изменения Домена другим частям Приложения или даже другим Приложениям, как показано в Главе 12, Интеграция ограниченных контекстов: + +```php +class Post +{ + // ... + public function publish() + { + $this->setStatus( + Status::published() + ); + $this->publishedAt(new DateTimeImmutable()); + DomainEventPublisher::instance()->publish( + new PostPublished($this->id) + ); + } + public function unpublish() + { + $this->setStatus( + Status::draft() + ); + $this-> publishedAt = null; + DomainEventPublisher::instance()->publish( + new PostUnpublished($this->id) + ); + } + // ... +} +``` + + +События домена могут даже запускаться при создании нового экземпляра нашей Сущности: + +```php +class User +{ + // ... + public function __construct(UserId $userId, $email, $password) + { + $this->setUserId($userId); + $this->setEmail($email); + $this->setPassword($password); + DomainEventPublisher::instance()->publish( + new UserRegistered($this->userId) + ); + } +} +``` + + +### В заключении + +Некоторые понятия в Домене требуют Идентичность - то есть изменения их внутренних состояний не меняют их собственные уникальные идентичности. Мы видели, как моделирование Identity как объекта-значения приносит такие преимущества, как неизменяемость, в дополнение к логике для работы с самой Identity. Мы также показали несколько способов предоставления Identity, переформулированных в следующих указателях: + +- Механизм персистентности: Простой в реализации, но вы не будете иметь Identity до сохранения объекта, что задерживает и затрудняет распространение событий. + +- Суррогатный ID: Некоторым ОРМ требуется дополнительное поле в Сущности, чтобы сопоставить Идентификатор с сохраняющимся механизмом. + +- Предоставляется клиентом: Иногда Identity подходит под концепцию Домена и вы можете смоделировать его внутри вашего Домена. + +- Создано приложением: Для создания идентификаторов можно использовать библиотеку. + +- Генерируется ограниченным контекстом: возможно, самая сложная стратегия. Другие ограниченные контексты обеспечивают интерфейс для создания идентификатора. + + +Мы видели и обсуждали Doctrine как механизм персистентности, мы смотрели на недостатки использования шаблона Active Record и, наконец, мы проверили различные уровни проверки Сущности: + +- Проверка атрибутов: Проверка специфики в состоянии объекта через предварительные условия, постусловия и инварианты. + +- Проверка всего объекта: поиск непротиворечивости объекта в целом. Извлечение проверки во внешнюю службу является хорошей практикой. + +- Композиции объектов: Сложные композиции объектов можно проверить с помощью доменных служб. Хороший способ донести это до остальной части приложения - через События домена. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ru-RU/Chapter4/codeBlock.php b/ru-RU/Chapter4/codeBlock.php new file mode 100644 index 0000000..715ebd2 --- /dev/null +++ b/ru-RU/Chapter4/codeBlock.php @@ -0,0 +1,15 @@ +setUserId($userId); + $this->setEmail($email); + $this->setPassword($password); + DomainEventPublisher::instance()->publish( + new UserRegistered($this->userId) + ); + } +} \ No newline at end of file diff --git a/ru-RU/Chapter4/q.sql b/ru-RU/Chapter4/q.sql new file mode 100644 index 0000000..923a75f --- /dev/null +++ b/ru-RU/Chapter4/q.sql @@ -0,0 +1,7 @@ +CREATE TABLE `orders` ( +`id` int(11) NOT NULL auto_increment, +`amount` decimal (10,5) NOT NULL, +`first_name` varchar(100) NOT NULL, +`last_name` varchar(100) NOT NULL, +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file diff --git a/ru-RU/Chapter4/x.xml b/ru-RU/Chapter4/x.xml new file mode 100644 index 0000000..914f182 --- /dev/null +++ b/ru-RU/Chapter4/x.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file From 66c9560f362a1db6084a2cdf133cef15adbd8d6c Mon Sep 17 00:00:00 2001 From: eatae Date: Wed, 10 Apr 2024 14:37:03 +0300 Subject: [PATCH 2/2] =?UTF-8?q?Chapter4:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20
=20=D0=B8=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B8=D1=85=20=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ru-RU/Chapter4/Entities.md | 416 ++++++++++++++++++++++++++--------- ru-RU/Chapter4/codeBlock.php | 15 -- ru-RU/Chapter4/q.sql | 7 - ru-RU/Chapter4/x.xml | 30 --- 4 files changed, 314 insertions(+), 154 deletions(-) delete mode 100644 ru-RU/Chapter4/codeBlock.php delete mode 100644 ru-RU/Chapter4/q.sql delete mode 100644 ru-RU/Chapter4/x.xml diff --git a/ru-RU/Chapter4/Entities.md b/ru-RU/Chapter4/Entities.md index c36a7d9..fe101fc 100644 --- a/ru-RU/Chapter4/Entities.md +++ b/ru-RU/Chapter4/Entities.md @@ -1,7 +1,3 @@ -# Chapter4 -#BOOKS/DDD-IN-PHP - - Глава 4. Сущности (Entities) == @@ -62,9 +58,10 @@ class Person ```
- Или рассмотрим следующее в случае заказа: +
+ ```php namespace Ddd\Billing\Domain\Model\Order; @@ -108,12 +105,15 @@ class Order - Объекты Value являются неизменяемыми, поэтому их нельзя изменять. -- Объекты-значения - это сложные типы, которые могут иметь пользовательское поведение, то, что примитивные типы не могут иметь. Возьмем в качестве примера операцию равенства. С помощью Value Objects операции равенства могут моделироваться и инкапсулироваться в свои собственные классы, делая концепции переходящими от неявных к явным. +- Объекты-значения - это сложные типы, которые могут иметь пользовательское поведение, то, что примитивные типы не могут иметь. Возьмем в качестве примера операцию равенства. +С помощью Value Objects операции равенства могут моделироваться и инкапсулироваться в свои собственные классы, делая концепции переходящими от неявных к явным.
Рассмотрим возможную реализацию для OrderId, идентификатора заказа, который превратился в объект Value: +
+ ```php namespace Ddd\Billing\Domain\Model; @@ -140,10 +140,12 @@ class OrderId Существует несколько реализаций, которые можно использовать для реализации идентификатора заказа. Приведенный выше пример довольно прост. Как описано в главе 3, «Объекты значений», можно сделать метод __construct () частным и использовать статические фабричные методы для создания новых экземпляров. Поговорите со своей командой, проведите эксперимент и договоритесь. Поскольку идентификаторы сущностей не являются сложными Объектами Значений, мы рекомендуем вам здесь не беспокоиться слишком много. -
+ Возвращаясь к Order, пришло время обновить ссылки на OrderId: +
+ ```php class Order { @@ -195,12 +197,14 @@ class Order

-### Механизм хранения(БД) генерирует Идентификатор +### Механизм хранения (БД) генерирует Идентификатор
Обычно самый простой способ генерации Identity - делегировать его механизму персистентности, потому что подавляющее большинство механизмов персистентности поддерживают некую генерацию Identity - как атрибут `AUTO_INCREMENT` MySQL или последовательности Postgres и Oracle. Это, хотя и просто, имеет главный недостаток: мы не будем иметь Идентичность Сущности, пока мы не сохраним ее. Таким образом, в некоторой степени, если мы идем с Mechanism Generated Identity, мы свяжем операцию Identity с базовым хранилищем персистентности: +
+
```sql CREATE TABLE `orders` ( @@ -215,6 +219,8 @@ CREATE TABLE `orders` ( И тогда мы могли бы рассмотреть этот код: +
+ ```php namespace Ddd\Identity\Domain\Model; @@ -251,6 +257,7 @@ class Person Каков подход к созданию нового Person? Если база данных собирается создать идентификатор, нужно ли передавать его в конструктор? Когда и где магия, которая обновит Person? Что произойдет, если мы в конечном итоге не будем сохранять Сущность? +

#### Суррогатная идентичность @@ -259,6 +266,9 @@ class Person например, доктрина требует целочисленное поле, если используется стратегия генератора IDENTITY. Это может привести к конфликту с моделью домена, если для нее требуется другой тип удостоверения. Простейшим способом обработки такой ситуации является использование супертипа слоя, в котором помещается поле Identity, созданное для хранилища сохраняемости: +
+
+ ```php namespace Ddd\Common\Domain\Model; @@ -301,24 +311,35 @@ class Order extends IdentifiableDomainObject #### Active Record Vs. Data Mapper для Богатых Доменных Моделей
-Каждый проект всегда сталкивается с решением, какой ORM должен использоваться. Там много хороших ОRM для PHP: Doctrine, Propel, Eloquent, Paris, и многое другое. +Каждый проект всегда сталкивается с решением, какая ORM должна использоваться. Есть много хороших ОRM для PHP: Doctrine, Propel, Eloquent, Paris, и много остальных. Большинство из них являются реализациями Active Record. Реализация Active Record в основном подходит для приложений CRUD, но она не является идеальным решением для моделей Rich Domain по следующим причинам: -- Шаблон активной записи предполагает связь «один к одному» между сущностью и таблицей базы данных. Таким образом, она связывает конструкцию базы данных с конструкцией объектной системы. А в модели Rich Domain иногда Сущности строятся с информацией, которая может поступать из разных источников данных. +- Шаблон активной записи предполагает связь «один к одному» между сущностью и таблицей базы данных. Таким образом, она связывает конструкцию базы данных с конструкцией объектной системы. +А в модели Rich Domain иногда Сущности строятся с информацией, которая может поступать из разных источников данных. + - Продвинутые вещи, такие как коллекции и наследование, сложно реализовать. -- Большинство реализаций вынуждают использовать посредством наследования некие конструкции, которые навязывают несколько конвенций. Это может привести к постоянной утечке в модель домена путем соединения модели домена с ORM. Единственная реализация Active Record, которая не навязывает наследование от базового класса, -Castle ActiveRecord от Castle Project, .NET framework. В то время как это приводит к некоторой степени разделения между устойчивостью и проблемами Домена в созданных Сущностях, это не отделяет детали устойчивости низкого уровня от конструкции домена высокого уровня. + +- Большинство реализаций вынуждают использовать посредством наследования некие конструкции, которые навязывают несколько конвенций. +Это может привести к постоянной утечке в модель домена путем соединения модели домена с ORM. Единственная реализация Active Record, которая не навязывает наследование от базового класса, -Castle ActiveRecord от Castle Project, .NET framework. +В то время как это приводит к некоторой степени разделения между устойчивостью и проблемами Домена в созданных Сущностях, это не отделяет детали устойчивости низкого уровня от конструкции домена высокого уровня.
-Как упоминалось в предыдущей главе, в настоящее время лучшим ORM для PHP является Doctrine, которая является реализацией шаблона Отображения Данных. Data Mapper отделяет проблемы персистентности от проблем домена, что приводит к появлению сущностей, свободных от персистентности. Это делает инструмент лучшим для тех, кто хочет построить модель Rich Domain. +Как упоминалось в предыдущей главе, в настоящее время лучшим ORM для PHP является Doctrine, которая является реализацией шаблона Отображения Данных. Data Mapper отделяет проблемы персистентности от проблем домена, что приводит к появлению сущностей, свободных от персистентности. +Это делает инструмент лучшим для тех, кто хочет построить модель Rich Domain. +
+
-### Клиент предоставляет Идентификатор +### Клиент предоставляет Идентификатор +
Иногда при работе с определенными Доменами Идентификация приходят естественным образом, когда клиент использует Модель Домена. Это, скорее всего, идеальный случай, потому что Идентичность может быть легко смоделирована. Рассмотрим рынок продажи книг: +
+
```php namespace Ddd\Catalog\Domain\Model\Book; @@ -345,11 +366,17 @@ class ISBN } } ``` +
+Согласно Википедии: Международный стандартный номер книги (ISBN) является уникальным числовым коммерческим идентификатором книги. +ISBN назначается каждому изданию и варианту (за исключением повторной печати) книги. Например, электронная книга, обратная страница и печатное издание одной и той же книги будут иметь разные ISBN. +Длина ISBN составляет 13 цифр, если она назначена 1 января 2007 года или позднее, и 10 цифр, если она назначена до 2007 года. Метод присвоения ISBN является национальным и варьируется от страны к стране, +часто в зависимости от того, насколько велика издательская индустрия в стране. -Согласно Википедии: Международный стандартный номер книги (ISBN) является уникальным числовым коммерческим идентификатором книги. ISBN назначается каждому изданию и варианту (за исключением повторной печати) книги. Например, электронная книга, обратная страница и печатное издание одной и той же книги будут иметь разные ISBN. Длина ISBN составляет 13 цифр, если она назначена 1 января 2007 года или позднее, и 10 цифр, если она назначена до 2007 года. Метод присвоения ISBN является национальным и варьируется от страны к стране, часто в зависимости от того, насколько велика издательская индустрия в стране. +Самое интересное в ISBN то, что он уже определен в Домене, это действительный идентификатор, потому что он уникален, и его можно легко проверить. +Это хороший пример идентификатора, предоставленного клиентом: -Самое интересное в ISBN то, что он уже определен в Домене, это действительный идентификатор, потому что он уникален, и его можно легко проверить. Это хороший пример идентификатора, предоставленного клиентом: +
```php namespace Ddd\Catalog\Domain\Model\Book; @@ -365,9 +392,12 @@ class Book } } ``` +
Теперь, это простой вопрос использования его: +
+ ```php $book = new Book( ISBN::create('...'), @@ -375,22 +405,35 @@ $book = new Book( ); ``` +
+
### Приложение создаёт Идентификатор +
+Если клиент не может предоставить идентификатор в целом, предпочтительным способом обработки операции идентификации является разрешение приложению генерировать идентификаторы, обычно через UUID. +Это наш рекомендуемый подход в случае, если у вас нет сценария, как показано в предыдущем разделе. -Если клиент не может предоставить идентификатор в целом, предпочтительным способом обработки операции идентификации является разрешение приложению генерировать идентификаторы, обычно через UUID. Это наш рекомендуемый подход в случае, если у вас нет сценария, как показано в предыдущем разделе. - -==Согласно Википедии:== -Цель UUID - дать возможность распределенным системам однозначно идентифицировать информацию без существенной централизованной координации. В этом контексте слово «уникальный» следует понимать как практически уникальный, а не как гарантированный уникальный. Поскольку идентификаторы имеют конечный размер, два различных элемента могут совместно использовать один и тот же идентификатор. Это форма хеш-коллизии. Размер идентификатора и процесс генерации должны быть выбраны так, чтобы сделать это достаточно маловероятным на практике. Любой может создать UUID и использовать его для идентификации чего-либо с разумной уверенностью, что тот же идентификатор никогда не будет непреднамеренно создан кем-либо для идентификации чего-либо другого. Поэтому информация, помеченная UUID, может быть впоследствии объединена в единую базу данных без необходимости разрешения конфликтов идентификаторов (ID). +**Согласно Википедии:**
+Цель UUID - дать возможность распределенным системам однозначно идентифицировать информацию без существенной централизованной координации. В этом контексте слово «уникальный» следует понимать как практически уникальный, а не как гарантированный уникальный. +Поскольку идентификаторы имеют конечный размер, два различных элемента могут совместно использовать один и тот же идентификатор. Это форма хеш-коллизии. +Размер идентификатора и процесс генерации должны быть выбраны так, чтобы сделать это достаточно маловероятным на практике. +Любой может создать UUID и использовать его для идентификации чего-либо с разумной уверенностью, что тот же идентификатор никогда не будет непреднамеренно создан кем-либо для идентификации чего-либо другого. +Поэтому информация, помеченная UUID, может быть впоследствии объединена в единую базу данных без необходимости разрешения конфликтов идентификаторов (ID). +
> Существует несколько библиотек в PHP, которые генерируют UUID, и они могут быть найдены на Packagist: https ://packagist.org/search/?q=uuid. > Лучшая рекомендация - разработанная Беном Рэмзи: https ://github.com/ramsey/uuid > потому что имеет много наблюдателей на GitHub и миллионы установок на Packagist. +
+ +Предпочтительным местом для создания Идентификатора будет Репозиторий (подробнее об этом в Главе 10. -Предпочтительным местом для создания Идентификатора будет Репозиторий (подробнее об этом в Главе 10. Пример Репозитория: +Пример Репозитория: + +
```php namespace Ddd\Billing\Domain\Model\Order; @@ -402,9 +445,13 @@ interface OrderRepository public function remove(Order $anOrder); } ``` +
+При использовании Доктрины необходимо создать пользовательский Репозиторий, реализующий такой интерфейс. Он в основном создает новый Идентификатор и использует EntityManager для сохранения и удаления сущностей. -При использовании Доктрины необходимо создать пользовательский Репозиторий, реализующий такой интерфейс. Он в основном создает новый Идентификатор и использует EntityManager для сохранения и удаления сущностей. Небольшое изменение состоит в том, чтобы поместить реализацию nextIdentity в интерфейс, который станет абстрактным классом: +Небольшое изменение состоит в том, чтобы поместить реализацию nextIdentity в интерфейс, который станет абстрактным классом: + +
```php namespace Ddd\Billing\Infrastructure\Domain\Model\Order; @@ -432,8 +479,12 @@ class DoctrineOrderRepository } } ``` +
Давайте быстро рассмотрим конечную реализацию объекта OrderId Value Object: +
+
+ ```php namespace Ddd\Billing\Domain\Model\Order; @@ -453,33 +504,47 @@ class OrderId } } ``` +
-Основная проблема, связанная с этим подходом, как будет показано в следующих разделах, заключается в том, насколько просто сохранять объекты, содержащие объекты значений. Однако сопоставление встроенных объектов значений, находящихся внутри сущности, может быть сложным в зависимости от ORM. +Основная проблема, связанная с этим подходом, как будет показано в следующих разделах, заключается в том, насколько просто сохранять объекты, содержащие объекты значений. +Однако сопоставление встроенных объектов значений, находящихся внутри сущности, может быть сложным в зависимости от ORM. +
+
### Другой ограниченный контекст генерирует идентификатор - -Вероятно, это наиболее сложная стратегия создания Identity, поскольку она вынуждает локальную сущность зависеть не только от локальных событий ограниченного контекста, но и от внешних событий ограниченного контекста. Так что с точки зрения технического обслуживания стоимость была бы высокой. +
+Вероятно, это наиболее сложная стратегия создания Identity, поскольку она вынуждает локальную сущность зависеть не только от локальных событий ограниченного контекста, но и от внешних событий ограниченного контекста. +Так что с точки зрения технического обслуживания стоимость была бы высокой. Другой ограниченный контекст предоставляет интерфейс для выбора идентификатора из локальной сущности. Некоторые из экспонируемых свойств могут восприниматься как собственные. Когда необходима синхронизация между сущностями ограниченных контекстов, она обычно может быть достигнута с помощью управляемой событиями архитектуры в каждом из ограниченных контекстов, которые должны быть уведомлены при изменении исходной сущности. - +
+
## Сохраняющиеся сущности - -В настоящее время, как обсуждалось ранее в главе, лучшим инструментом для сохранения состояния сущности в постоянном хранилище является доктрина ORM. Доктрина имеет несколько способов определения метаданных Сущности: с помощью аннотаций в коде Сущности, XML, YAML или простого PHP. В этой главе мы подробно обсудим, почему аннотации не являются лучшими для использования при сопоставлении сущностей. - +
+В настоящее время, как обсуждалось ранее в главе, лучшим инструментом для сохранения состояния сущности в постоянном хранилище является доктрина ORM. +Доктрина имеет несколько способов определения метаданных Сущности: с помощью аннотаций в коде Сущности, XML, YAML или простого PHP. В этой главе мы подробно обсудим, почему аннотации не являются лучшими для использования при сопоставлении сущностей. +
+
### Настройка доктрины - +
Прежде всего, мы должны установить Доктрину через Composer. В корневой папке проекта должна быть выполнена следующая команда: +
+
> php composer.phar require "doctrine/orm=^2.5" +
+ Затем эти строки позволяют настроить доктрину: +
+
```php require_once '/path/to/vendor/autoload.php'; @@ -502,21 +567,25 @@ $config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode); $entityManager = EntityManager::create($dbParams, $config); ``` - +
+
### Маппинг Entities - +
По умолчанию в документации доктрины представлены примеры кода с использованием аннотаций. Итак, мы начинаем пример кода, используя аннотации и обсуждая, почему их следует избегать, когда это возможно. - Для этого мы вернем класс Order, обсуждавшийся ранее в этой главе. - +
+
+
#### Маппинг Entities с помощью аннотированного кода - +
Когда была выпущена доктрина, броским способом отображения объектов в примерах кода было использование аннотаций. -**Что такое аннотация?** +**Что такое аннотация?**
Аннотация - это особая форма метаданных. В PHP он помещен в комментарии к исходному коду. Например, PHPDocumentor использует аннотации для построения информации API, а PHPUnit использует некоторые аннотации для указания поставщиков данных или для предоставления ожиданий относительно исключений, порождаемых фрагментом кода: +
+
```php class SumTest extends PHPUnit_Framework_TestCase @@ -527,8 +596,11 @@ class SumTest extends PHPUnit_Framework_TestCase } } ``` +
Чтобы сопоставить объект Order с хранилищем, исходный код Order должен быть изменен для добавления аннотаций доктрины: +
+
```php use Doctrine\ORM\Mapping\Entity; @@ -579,9 +651,11 @@ class Order { } ``` - +
Затем, чтобы сохранить Сущность в постоянном хранилище, так же легко сделать следующее: +
+
```php $order = new Order( @@ -593,22 +667,28 @@ $entityManager->persist($order); $entityManager->flush(); ``` - +
На первый взгляд, этот код выглядит просто, и это может быть простым способом указать информацию сопоставления. Но это имеет определенную цену. Что странного в окончательном коде? Во-первых, проблемы домена смешиваются с проблемами инфраструктуры. Order является концепцией домена, тогда как Table, Column и т. д. являются проблемами инфраструктуры. -В результате эта Сущность тесно связана с информацией отображения, указанной аннотациями в исходном коде. Если бы Сущность требовалось сохранить с помощью другого менеджера Сущности и с другими метаданными сопоставления, это было бы невозможно. +В результате эта Сущность тесно связана с информацией отображения, указанной аннотациями в исходном коде. +Если бы Сущность требовалось сохранить с помощью другого менеджера Сущности и с другими метаданными сопоставления, это было бы невозможно. Аннотации, как правило, приводят к побочным эффектам и плотной связи, поэтому их лучше не использовать. -Так какой лучший способ указать информацию о сопоставлении? Лучший способ - это способ, позволяющий отделить информацию о сопоставлении от самой сущности. Это может быть достигнуто с помощью сопоставления XML, сопоставления YAML или PHP. В этой книге мы рассмотрим сопоставление XML. +Так какой лучший способ указать информацию о сопоставлении? Лучший способ - это способ, позволяющий отделить информацию о сопоставлении от самой сущности. +Это может быть достигнуто с помощью сопоставления XML, сопоставления YAML или PHP. В этой книге мы рассмотрим сопоставление XML. +
+
#### Маппинг Entities с помощью XML - +
Чтобы отобразить объект Order с помощью сопоставления XML, код настройки доктрины должен быть слегка изменен: +
+
```php @@ -632,24 +712,35 @@ $config = Setup::createXMLMetadataConfiguration($paths, $isDevMode); $entityManager = EntityManager::create($dbParams, $config); ``` +
-Файл сопоставления должен быть создан по пути, по которому доктрина будет выполнять поиск файлов сопоставления, а файлы сопоставления должны быть названы в честь полного имени класса, заменяя обратную косую черту \ точками. +Файл сопоставления должен быть создан по пути, по которому доктрина будет выполнять поиск файлов сопоставления, +а файлы сопоставления должны быть названы в честь полного имени класса, заменяя обратную косую черту \ точками. +
-Рассмотрим следующее: +Рассмотрим следующее:
`Acme\Billing\Domain\Model\Order` -- файл сопоставления будет иметь имя: + +файл сопоставления будет иметь имя: `Acme.Billing.Domain.Model.Order.dcm.xml` -Кроме того, удобно, чтобы все файлы сопоставления использовали специальную XML-схему, созданную специально для указания информации сопоставления: +
+Кроме того, удобно, чтобы все файлы сопоставления использовали специальную XML-схему, созданную специально для указания информации сопоставления:
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd +
+
+
#### Сопоставление Идентификатора Сущности - +
Наш идентификатор, OrderId, является объектом значения. Как видно из предыдущей главы, существуют различные подходы к отображению объекта Value Object с использованием доктрины, встраиваемых объектов и пользовательских типов. Если в качестве идентификаторов используются объекты значений, лучшим вариантом является использование пользовательских типов. -Интересной новой особенностью Doctrine 2.5 является то, что теперь можно использовать Объекты в качестве идентификаторов для Сущностей, если они реализуют магический метод __toString (). Таким образом, мы можем добавить __toString к нашим объектам Identity Value Object и использовать их в наших сопоставлениях: +Интересной новой особенностью Doctrine 2.5 является то, что теперь можно использовать Объекты в качестве идентификаторов для Сущностей, если они реализуют магический метод __toString (). +Таким образом, мы можем добавить __toString () к нашим объектам Identity Value Object и использовать их в наших сопоставлениях: +
+
```php namespace Ddd\Billing\Domain\Model\Order; @@ -666,8 +757,13 @@ class OrderId } ``` +
-Проверьте реализацию пользовательских типов доктрины. Они наследуют GuidType, поэтому их внутреннее представление будет UUID. Необходимо указать собственный перевод базы данных. Затем необходимо зарегистрировать пользовательские типы, прежде чем использовать их. Если требуется справка по этим шагам, то [Custom Mapping Types](https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/cookbook/custom-mapping-types.html) являются хорошей ссылкой. +Проверьте реализацию пользовательских типов доктрины. Они наследуют GuidType, поэтому их внутреннее представление будет UUID. +Необходимо указать собственный перевод базы данных. Затем необходимо зарегистрировать пользовательские типы, прежде чем использовать их. +Если требуется справка по этим шагам, то [Custom Mapping Types](https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/cookbook/custom-mapping-types.html) являются хорошей ссылкой. +
+
```php use Doctrine\DBAL\Platforms\AbstractPlatform; @@ -692,9 +788,11 @@ class DoctrineOrderId extends GuidType } ``` - +
Наконец, мы настроим регистрацию пользовательских типов. Опять же, мы должны обновить начальную загрузку: +
+
```php require_once '/path/to/vendor/autoload.php'; @@ -709,13 +807,17 @@ $config = Setup::createXMLMetadataConfiguration($paths, $isDevMode); $entityManager = EntityManager::create($dbParams, $config); ``` - +
+
+
#### Окончательный файл сопоставления - +
Со всеми изменениями, мы наконец-то готовы, так что давайте посмотрим на окончательный файл сопоставления. Наиболее интересной детализацией является проверка того, как идентификатор сопоставляется с нашим определенным пользовательским типом для OrderId: +
+
```xml @@ -750,14 +852,18 @@ https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd"> ``` - +
+
## Тестирование Entities - -Относительно легко протестировать Сущности, просто потому, что они являются простыми старыми PHP-классами с действиями, производными от концепции Домена, которую они представляют. В центре внимания теста должны быть инварианты, которые защищает Сущность, потому что поведение на Сущностях, вероятно, будет смоделировано вокруг этих инвариантов. +
+Относительно легко протестировать Сущности, просто потому, что они являются простыми старыми PHP-классами с действиями, производными от концепции Домена, которую они представляют. +В центре внимания теста должны быть инварианты, которые защищает Сущность, потому что поведение на Сущностях, вероятно, будет смоделировано вокруг этих инвариантов. Например, для простоты предположим, что для блога необходима модель домена. Один из возможных вариантов мог бы заключаться в следующем: +
+
```php class Post @@ -830,7 +936,7 @@ class Post } ``` - +
```php class Status @@ -858,8 +964,11 @@ class Status ``` +
Чтобы проверить эту модель домена, мы должны убедиться, что тест охватывает все Post инварианты: +
+
```php class PostTest extends PHPUnit_Framework_TestCase @@ -896,12 +1005,16 @@ class PostTest extends PHPUnit_Framework_TestCase } ``` - - +
+
+
### Дата Время (DateTimes) - -Поскольку DateTimes широко используется в сущностях, мы считаем важным указать конкретные подходы к единицам тестирования сущностей, которые имеют поля с типами дат. Учтите, что Пост является новым, если он был создан в течение последних 15 дней: +
+Поскольку DateTimes широко используется в сущностях, мы считаем важным указать конкретные подходы к единицам тестирования сущностей, которые имеют поля с типами дат. +Учтите, что Пост является новым, если он был создан в течение последних 15 дней: +
+
```php class Post @@ -927,13 +1040,19 @@ class Post } ``` +
-Метод isNew () должен сравнивать два значения DateTimes. Это сравнение между датой создания Post и сегодняшней датой. Мы вычисляем разницу и проверяем, меньше ли она указанного количества дней. Как выполнить единичное тестирование метода isNew ()? Как мы продемонстрировали в реализации, трудно воспроизвести конкретные потоки в наших тестовых комплектах. Какие у нас есть варианты? - +Метод isNew () должен сравнивать два значения DateTimes. Это сравнение между датой создания Post и сегодняшней датой. Мы вычисляем разницу и проверяем, меньше ли она указанного количества дней. +Как выполнить единичное тестирование метода isNew ()? Как мы продемонстрировали в реализации, трудно воспроизвести конкретные потоки в наших тестовых комплектах. Какие у нас есть варианты? +
+
+
#### Передача все дат в виде параметров - +
Одним из возможных вариантов может быть передача всех дат в качестве параметров при необходимости: +
+
```php class Post @@ -956,8 +1075,11 @@ class Post } ``` - -Это самый простой подход для целей модульного тестирования. Просто проходите различные пары дат, чтобы проверить все возможные сценарии со 100-процентным охватом. Однако, если учесть код клиента, который создает и запрашивает результат метода isNew (), все выглядит не так хорошо. Полученный в результате код может быть немного странным из-за постоянной передачи текущего DateTime: +
+Это самый простой подход для целей модульного тестирования. Просто проходите различные пары дат, чтобы проверить все возможные сценарии со 100-процентным охватом. +Однако, если учесть код клиента, который создает и запрашивает результат метода isNew (), все выглядит не так хорошо. Полученный в результате код может быть немного странным из-за постоянной передачи текущего DateTime: +
+
```php $aPost = new Post( @@ -971,13 +1093,20 @@ $aPost->isNew( ``` +
+
-#### Класс тестирования -Другой альтернативой является использование шаблона Test Class. Идея состоит в том, чтобы расширить класс Post новым, которым мы можем манипулировать, чтобы форсировать определенные сценарии. Этот новый класс будет использоваться только в целях модульного тестирования. Плохая новость в том, что мы должны немного изменить исходный класс Post, извлекая некоторые методы и изменяя некоторые поля и методы с частных на защищенные. Некоторые разработчики могут беспокоиться об увеличении видимости свойств класса только из-за причин тестирования. Однако мы считаем, что в большинстве случаев это того стоит: +#### Класс тестирования +
+Другой альтернативой является использование шаблона Test Class. Идея состоит в том, чтобы расширить класс Post новым, которым мы можем манипулировать, чтобы форсировать определенные сценарии. +Этот новый класс будет использоваться только в целях модульного тестирования. Плохая новость в том, что мы должны немного изменить исходный класс Post, извлекая некоторые методы и изменяя некоторые поля и методы с частных на защищенные. +Некоторые разработчики могут беспокоиться об увеличении видимости свойств класса только из-за причин тестирования. Однако мы считаем, что в большинстве случаев это того стоит: +
+
```php -class Post +class Post { protected $createdAt; public function isNew() @@ -999,9 +1128,12 @@ $aPost->isNew( } ``` +
- -Как вы видите, мы извлекли логику для получения сегодняшней даты в метод today (). Таким образом, применяя шаблон метода, можно изменить его поведение от производного класса. Нечто подобное происходит с методом и полем createAt. Теперь они защищены, поэтому их можно использовать и переопределять в производных классах: +Как вы видите, мы извлекли логику для получения сегодняшней даты в метод today (). Таким образом, применяя шаблон метода, можно изменить его поведение от производного класса. +Нечто подобное происходит с методом и полем createAt. Теперь они защищены, поэтому их можно использовать и переопределять в производных классах: +
+
```php class PostTestClass extends Post @@ -1019,8 +1151,11 @@ class PostTestClass extends Post } ``` +
С учетом этих изменений теперь мы можем протестировать наш исходный класс Post с помощью тестирования PostTestClass: +
+
```php class PostTest extends PHPUnit_Framework_TestCase @@ -1063,13 +1198,20 @@ class PostTest extends PHPUnit_Framework_TestCase } ``` - -Последняя деталь: при таком подходе невозможно достичь 100-процентного покрытия в классе Post, потому что метод today () никогда не будет выполняться. Однако он может быть охвачен другими тестами. +
+Последняя деталь: при таком подходе невозможно достичь 100-процентного покрытия в классе Post, потому что метод today () никогда не будет выполняться. +Однако он может быть охвачен другими тестами. +
+
+
#### External Fake - -Другой вариант заключается в переносе вызовов конструктораDateTimeImmutable или именованных конструкторов с использованием нового класса и некоторых статических методов. При этом мы можем статически изменить результат этих методов, чтобы вести себя по-разному на основе конкретных сценариев тестирования: +
+Другой вариант заключается в переносе вызовов конструктора DateTimeImmutable или именованных конструкторов с использованием нового класса и некоторых статических методов. +При этом мы можем статически изменить результат этих методов, чтобы вести себя по-разному на основе конкретных сценариев тестирования: +
+
```php class Post @@ -1092,8 +1234,11 @@ class Post } ``` +
-Для получения текущегоDateTime теперь используется статический вызов MyCustomDateTimeBuilder:: today (). Этот класс также имеет несколько методов установки, чтобы подделать результат для возврата в следующих вызовах: +Для получения текущего DateTime теперь используется статический вызов MyCustomDateTimeBuilder:: today (). Этот класс также имеет несколько методов установки, чтобы подделать результат для возврата в следующих вызовах: +
+
```php class PostTest extends PHPUnit_Framework_TestCase @@ -1131,16 +1276,24 @@ class PostTest extends PHPUnit_Framework_TestCase } ``` - +
Основная проблема этого подхода заключается в том, что он статически связан с объектом. В зависимости от вашего варианта использования, также будет сложно создать гибкий поддельный объект. +
+
#### Рефлексия - +
Можно также использовать методы Reflection для построения нового класса Post с пользовательскими датами. -Рассмотрим [Mimic](https://github.com/keyvanakbary/mimic), простую функциональную библиотеку для прототипирования объектов, гидратации данных и отображения данных: +Рассмотрим + +[Mimic](https://github.com/keyvanakbary/mimic) + +простую функциональную библиотеку для прототипирования объектов, гидратации данных и отображения данных: +
+
```php namespace Domain; @@ -1174,23 +1327,34 @@ assert(m\expose(new Domain\ComputerScientist('Grace', 'Hopper')) == array( )); ``` - +
> **Поделиться и обсудить** >Обсудите с коллегами по работе, как правильно протестировать объекты с фиксированнымидатами и придумайте дополнительные альтернативы. -Если вы хотите узнать больше о тестировании шаблонов и подходов, ознакомьтесь с книгойxUnit Test Patterns: Refactoring Test Code by Gerard Meszaros. +
+Если вы хотите узнать больше о тестировании шаблонов и подходов, ознакомьтесь с книгой xUnit Test Patterns: Refactoring Test Code by Gerard Meszaros. +
+
+
## Валидация - -Валидация является очень важным процессом в нашей модели домена. Она проверяет не только правильность атрибутов, но и правильность целых объектов и их состава. Для поддержания этой модели в допустимом состоянии требуются различные уровни проверки. То, что объект состоит из допустимых атрибутов (базовых), не обязательно означает, что объект (в целом) является действительным. И наоборот: действительные объекты не обязательно равны действительным составам. - +
+Валидация является очень важным процессом в нашей модели домена. Она проверяет не только правильность атрибутов, но и правильность целых объектов и их состава. Для поддержания этой модели в допустимом состоянии требуются различные уровни проверки. +То, что объект состоит из допустимых атрибутов (базовых), не обязательно означает, что объект (в целом) является действительным. И наоборот: действительные объекты необязательно равны действительным составам. +
+
+
### Валидация атрибутов - -Некоторые люди понимают проверку как процесс, посредством которого служба проверяет состояние данного объекта. В этом случае проверка соответствует подходу Design-by-contract, который состоит из предварительных условий, постусловий и инвариантов. Одним из таких способов защиты отдельного атрибута является использование главы 3 «Объекты значений». Чтобы сделать наш дизайн более гибким для изменений, мы концентрируемся только на утверждении предварительных условий Домена, которые должны быть выполнены. Здесь мы будем использовать охрану как простой способ проверки предварительных условий: +
+Некоторые люди понимают проверку как процесс, посредством которого служба проверяет состояние данного объекта. В этом случае проверка соответствует подходу Design-by-contract, который состоит из предварительных условий, постусловий и инвариантов. +Одним из таких способов защиты отдельного атрибута является использование главы 3 «Объекты значений». Чтобы сделать наш дизайн более гибким для изменений, мы концентрируемся только на утверждении предварительных условий Домена, которые должны быть выполнены. +Здесь мы будем использовать охрану как простой способ проверки предварительных условий: +
+
```php class Username @@ -1252,7 +1416,7 @@ class Username } ``` - +
Как видно из приведенного выше примера, для создания объекта «Значение имени пользователя» необходимо выполнить четыре предварительных условия. Оно: @@ -1261,20 +1425,27 @@ class Username - Должно содержать менее 10 символов - Должен иметь формат буквенно-цифровых символов или знаков подчеркивания +
Если все предварительные условия выполнены, атрибут будет установлен, и объект будет успешно построен. В противном случае будет создано исключение InvalidArgumentException, выполнение будет остановлено, и клиенту будет выдана ошибка. -Некоторые разработчики могут рассмотреть этот вид проверочного защитного программирования. Однако мы не проверяем, является ли ввод строкой или что значения null не разрешены. Мы не можем избегать людей, использующих наш код неправильно, но мы можем контролировать правильность нашего состояния Домена. Как видно из главы 3, «Объекты значений», проверка также может помочь нам в обеспечении безопасности. +Некоторые разработчики могут рассмотреть этот вид проверочного защитного программирования. Однако мы не проверяем, является ли ввод строкой или что значения null не разрешены. +Мы не можем избегать людей, использующих наш код неправильно, но мы можем контролировать правильность нашего состояния Домена. Как видно из главы 3, «Объекты значений», проверка также может помочь нам в обеспечении безопасности. -[Defensive programming](https://en.wikipedia.org/wiki/Defensive_programming) не плохая вещь. В общем, это имеет смысл при разработке компонентов или библиотек, которые будут использоваться в качестве третьей стороны в других проектах. +[Defensive programming](https://en.wikipedia.org/wiki/Defensive_programming) не плохая вещь.
+В общем, это имеет смысл при разработке компонентов или библиотек, которые будут использоваться в качестве третьей стороны в других проектах. Однако при разработке собственного ограниченного контекста этих дополнительных параноидных проверок (нулей, основных типов, подсказок типа и т. д.) можно избежать, чтобы увеличить скорость разработки, полагаясь на охват набора модульных тестов. - +
+
+
### Проверка всего объекта - +
Бывают случаи, когда объект, состоящий из допустимых свойств в целом, все еще может считаться недействительным. Может быть заманчиво добавить этот вид проверки к самому объекту, но обычно это ==антипаттерн==. Проверка на более высоком уровне изменяется в ритме, отличном от ритма самой логики объекта. Кроме того, рекомендуется разделять эти обязанности. Проверка информирует клиента о любых обнаруженных ошибках или собирает результаты для последующего просмотра, так как иногда мы не хотим останавливать выполнение при первом признаке неисправности. Абстрактный и повторно используемый валидатор может быть примерно таким: +
+
```php abstract class Validator @@ -1293,8 +1464,11 @@ abstract class Validator } ``` - -В качестве конкретного примера мы хотим проверить весь объект Location, состоящий из допустимых объектов Country, City и Postcode Value Objects. Однако эти отдельные значения могут находиться в невалидном состоянии во время проверки. Может быть, город не является частью страны, или почтовый индекс не соответствует формату города: +
+В качестве конкретного примера мы хотим проверить весь объект Location, состоящий из допустимых объектов Country, City и Postcode Value Objects. +Однако эти отдельные значения могут находиться в невалидном состоянии во время проверки. Может быть, город не является частью страны, или почтовый индекс не соответствует формату города: +
+
```php class Location @@ -1323,8 +1497,11 @@ class Location } } ``` +
Validator проверяет состояние объекта Location в целом, анализируя значение взаимосвязей между свойствами: +
+
```php class LocationValidator extends Validator @@ -1352,8 +1529,12 @@ class LocationValidator extends Validator } ``` +
-После установки всех свойств мы сможем проверить Сущность, скорее всего, после некоторых описанных процессов. На поверхности это выглядит так, как если бы местоположение подтвердило себя. Однако это не так. Класс Location делегирует эту проверку конкретному экземпляру валидатора, разделяя эти две четкие обязанности: +После установки всех свойств мы сможем проверить Сущность, скорее всего, после некоторых описанных процессов. На поверхности это выглядит так, как если бы местоположение подтвердило себя. +Однако это не так. Класс Location делегирует эту проверку конкретному экземпляру валидатора, разделяя эти две четкие обязанности: +
+
```php class Location @@ -1367,11 +1548,15 @@ class Location } ``` +
+
#### Decoupling сообщений валидации - +
С некоторыми незначительными изменениями в существующей реализации мы можем отделить сообщения проверки от средства проверки: +
+
```php class LocationValidationHandler implements ValidationHandler @@ -1381,6 +1566,8 @@ class LocationValidationHandler implements ValidationHandler } ``` +
+ ```php class LocationValidator { @@ -1410,8 +1597,11 @@ class LocationValidator } ``` +
Нам также необходимо изменить подпись метода проверки следующим образом: +
+
```php class Location @@ -1425,18 +1615,27 @@ class Location } } ``` - - +
+
+
### Валидация композиции объектов - -Проверка достоверности композиций объектов может быть сложной задачей. Таким образом, предпочтительным способом достижения этого является использование доменной службы. Затем служба взаимодействует с репозиториями, чтобы получить действительный агрегат. Из-за возможных сложных графов объектов, которые могут быть созданы, агрегат может находиться в промежуточном состоянии, требуя предварительной проверки других агрегатов. +
+Проверка достоверности композиций объектов может быть сложной задачей. Таким образом, предпочтительным способом достижения этого является использование доменной службы. +Затем служба взаимодействует с репозиториями, чтобы получить действительный агрегат. Из-за возможных сложных графов объектов, которые могут быть созданы, +агрегат может находиться в промежуточном состоянии, требуя предварительной проверки других агрегатов. События домена можно использовать для уведомления других частей системы о проверке определенного элемента. +
+
+
### Entities и Events домена - -Мы рассмотрим главу 6 «Доменные события» в будущем; однако важно подчеркнуть, что операции, выполняемые с сущностями, могут инициировать события домена. Этот подход используется для передачи изменения Домена другим частям Приложения или даже другим Приложениям, как показано в Главе 12, Интеграция ограниченных контекстов: +
+Мы рассмотрим главу 6 «Доменные события» в будущем; однако важно подчеркнуть, что операции, выполняемые с сущностями, могут инициировать события домена. +Этот подход используется для передачи изменения Домена другим частям Приложения или даже другим Приложениям, как показано в Главе 12, Интеграция ограниченных контекстов: +
+
```php class Post @@ -1465,9 +1664,11 @@ class Post // ... } ``` - +
События домена могут даже запускаться при создании нового экземпляра нашей Сущности: +
+
```php class User @@ -1484,29 +1685,40 @@ class User } } ``` - +
+
+
### В заключении - -Некоторые понятия в Домене требуют Идентичность - то есть изменения их внутренних состояний не меняют их собственные уникальные идентичности. Мы видели, как моделирование Identity как объекта-значения приносит такие преимущества, как неизменяемость, в дополнение к логике для работы с самой Identity. Мы также показали несколько способов предоставления Identity, переформулированных в следующих указателях: +
+Некоторые понятия в Домене требуют Идентичность - то есть изменения их внутренних состояний не меняют их собственные уникальные идентичности. +Мы видели, как моделирование Identity как объекта-значения приносит такие преимущества, как неизменяемость, в дополнение к логике для работы с самой Identity. +Мы также показали несколько способов предоставления Identity, переформулированных в следующих указателях: - Механизм персистентности: Простой в реализации, но вы не будете иметь Identity до сохранения объекта, что задерживает и затрудняет распространение событий. + - Суррогатный ID: Некоторым ОРМ требуется дополнительное поле в Сущности, чтобы сопоставить Идентификатор с сохраняющимся механизмом. + - Предоставляется клиентом: Иногда Identity подходит под концепцию Домена и вы можете смоделировать его внутри вашего Домена. + - Создано приложением: Для создания идентификаторов можно использовать библиотеку. + - Генерируется ограниченным контекстом: возможно, самая сложная стратегия. Другие ограниченные контексты обеспечивают интерфейс для создания идентификатора. +
Мы видели и обсуждали Doctrine как механизм персистентности, мы смотрели на недостатки использования шаблона Active Record и, наконец, мы проверили различные уровни проверки Сущности: - Проверка атрибутов: Проверка специфики в состоянии объекта через предварительные условия, постусловия и инварианты. + - Проверка всего объекта: поиск непротиворечивости объекта в целом. Извлечение проверки во внешнюю службу является хорошей практикой. + - Композиции объектов: Сложные композиции объектов можно проверить с помощью доменных служб. Хороший способ донести это до остальной части приложения - через События домена. diff --git a/ru-RU/Chapter4/codeBlock.php b/ru-RU/Chapter4/codeBlock.php deleted file mode 100644 index 715ebd2..0000000 --- a/ru-RU/Chapter4/codeBlock.php +++ /dev/null @@ -1,15 +0,0 @@ -setUserId($userId); - $this->setEmail($email); - $this->setPassword($password); - DomainEventPublisher::instance()->publish( - new UserRegistered($this->userId) - ); - } -} \ No newline at end of file diff --git a/ru-RU/Chapter4/q.sql b/ru-RU/Chapter4/q.sql deleted file mode 100644 index 923a75f..0000000 --- a/ru-RU/Chapter4/q.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE `orders` ( -`id` int(11) NOT NULL auto_increment, -`amount` decimal (10,5) NOT NULL, -`first_name` varchar(100) NOT NULL, -`last_name` varchar(100) NOT NULL, -PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file diff --git a/ru-RU/Chapter4/x.xml b/ru-RU/Chapter4/x.xml deleted file mode 100644 index 914f182..0000000 --- a/ru-RU/Chapter4/x.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - \ No newline at end of file