📜 ⬆️ ⬇️

The answer to the introduction to the design of entities, the problem of creating objects

After reading the article Introduction to the design of entities, the problem of creating objects in Habré, I decided to write a detailed comment on examples of using the Domain-driven design (DDD) , but, as usual, the comment was too big and I thought it was right to write a full article, especially since DDD, on Habré and not only, little attention is removed.


I recommend reading the article about which I will speak here.
In short, the author suggests using builders to control the consistency of data in essence when using the DDD approach. I want to suggest using Data Transfer Object (DTO) for this purpose.



The general structure of the entity class discussed by the author:


final class Client { public function __construct( $id, $corporateForm, $name, $generalManager, $country, $city, $street, $subway = null ); public function getId(): int; } 

and an example of using the builder


 $client = $builder->setId($id) ->setName($name) ->setGeneralManagerId($generalManager) ->setCorporateForm($corporateForm) ->setAddress($address) ->buildClient(); 

You can not go into details of the implementation, I think the general sense is clear.


The idea of ​​using the builder in this example is not bad, but in my opinion the builder is not needed here. Having taken setters from the entity to the builder, they did not cease to be setters. The author created an extra builder when you could just pass the parameters to the constructor or the factory method. Forgetting a setter is easier than an argument.


I think you know without me what the setters are bad with DDD approach. In short, they break encapsulation and do not guarantee the consistency of data at any time.

If we are talking about DDD, then it is better to consider the business processes associated with the entity.


For example, consider registering a new client and transferring an existing client to another manager. This can be considered as requests to perform operations on an entity and create DTOs for each action. We get this picture:


 namespace Domain\Client\Request; class RegisterClient { public $name = ''; public $manager; // Manager public $address; // Address } 

 namespace Domain\Client\Request; class DelegateClient { public $new_manager; // Manager } 

Based on the request from the user, we create a DTO, validate and create / edit an entity based on it.


 namespace Domain\Client; class Client { private $id; private $name = ''; private $manager; // Manager private $address; // Address private function __construct( IdGenerator $generator, string $name, Manager $manager, Address $address ) { $this->id = $generator->generate(); $this->name = $name; $this->manager = $manager; $this->address = $address; } //   ,      public static function register(IdGenerator $generator, RegisterClient $request) : Client { return new self($generator, $request->name, $request->manager, $request->address); } public function delegate(DelegateClient $request) { $this->manager = $request->new_manager; } } 

Wait. That's not all. Suppose we need to know when the client card was registered and updated. This is done in just a couple of lines:


 class Client { // ... private $date_create; // \DateTime private $date_update; // \DateTime private function __construct( IdGenerator $generator, string $name, Manager $manager, Address $address ) { // ... $this->date_create = new \DateTime(); $this->date_update = clone $this->date_create; } // ... public function delegate(DelegateClient $request) { $this->manager = $request->new_manager; $this->date_update = new \DateTime(); } } 

The obvious solution at first glance has a flaw that will manifest itself during testing. The problem is that we explicitly initialize the date object. In reality, this is the date the action is performed on the entity and the logical decision will be to initialize the request DTO.


 class RegisterClient { // ... public $date_action; // \DateTime public function __construct() { $this->date_action = new \DateTime(); } } 

 class DelegateClient { // ... public $date_action; // \DateTime public function __construct() { $this->date_action = new \DateTime(); } } 

 class Client { // ... private function __construct( IdGenerator $generator, string $name, Manager $manager, Address $address, \DateTime $date_action ) { $this->id = $generator->generate(); $this->name = $name; $this->manager = $manager; $this->address = $address; $this->date_create = clone $date_action; $this->date_update = clone $date_action; } public static function register(IdGenerator $generator, RegisterClient $request) : Client { return new self( $generator, $request->name, $request->manager, $request->address, $request->date_action ); } public function delegate(DelegateClient $request) { $this->manager = $request->new_manager; $this->date_update = clone $request->date_action; } } 

If we know when the card was edited, then it would be nice to know who edited it. Again, it is logical to bring this to the DTO. The request for editing someone performs.


 class RegisterClient { // ... public $user; // User public function __construct(User $user) { // ... $this->user = $user; } } 

 class DelegateClient { // ... public $user; // User public function __construct(User $user) { // ... $this->user = $user; } } 

 class Client { // ... private $user; // User private function __construct( IdGenerator $generator, string $name, Manager $manager, Address $address, \DateTime $date_action, User $user ) { $this->id = $generator->generate(); $this->name = $name; $this->manager = $manager; $this->address = $address; $this->date_create = clone $date_action; $this->date_update = clone $date_action; $this->user = $user; } public static function register(IdGenerator $generator, RegisterClient $request) : Client { return new self( $generator, $request->name, $request->manager, $request->address, $request->date_action, $request->user ); } public function delegate(DelegateClient $request) { $this->manager = $request->new_manager; $this->date_update = clone $request->date_action; $this->user = $request->user; } } 

Now we want to add more action on the entity. Add a change to the name of the client and his address These are the same actions on an entity as others, therefore we create DTO by analogy.


 namespace Domain\Client\Request; class MoveClient { public $new_address; // Address public $date_action; // \DateTime public $user; // User public function __construct(User $user) { $this->date_action = new \DateTime(); $this->user = $user; } } 

 namespace Domain\Client\Request; class RenameClient { public $new_name = ''; public $date_action; // \DateTime public $user; // User public function __construct(User $user) { $this->date_action = new \DateTime(); $this->user = $user; } } 

 class Client { // ... public function move(MoveClient $request) { $this->address = $request->new_address; $this->date_update = clone $request->date_action; $this->user = $request->user; } public function rename(RenameClient $request) { $this->name = $request->new_name; $this->date_update = clone $request->date_action; $this->user = $request->user; } } 

Do you notice code duplication? Then it will be even worse.


Now we want to log a change in the client's card in the database in order to know which employees to kick their ears in case something happens. This is a new entity. In the log we will write:



I give it only as an example. In this case, you can get by the log file, but for example, in the case of voting or likes, each request may be important to us separately.


 namespace Domain\Client; class Change { private $client; // Client private $change = ''; private $user; // User private $user_ip = ''; private $user_agent = ''; private $date_action; // \DateTime public function __construct( Client $client, string $change, User $user, string $user_ip, string $user_agent, \DateTime $date_action ) { $this->client= $client; $this->change = $change; $this->user = $user; $this->user_ip = $user_ip; $this->user_agent = $user_agent; $this->date_action = clone $date_action; } } 

Thus, in the DTO action we need to add information from the HTTP request.


 use Symfony\Component\HttpFoundation\Request; class RegisterClient { public $name = ''; public $manager; // Manager public $address; // Address public $date_action; // \DateTime public $user; // User public $user_ip = ''; public $user_agent = ''; public function __construct(User $user, string $user_ip, string $user_agent) { $this->date_action = new \DateTime(); $this->user = $user; $this->user_ip = $user_ip; $this->user_agent = $user_agent; } //     public static function createFromRequest(User $user, Request $request) : RegisterClient { return new self($user, $request->getClientIp(), $request->headers->get('user-agent')); } } 

The remaining DTOs are modified by analogy.


The author of the change and the date of the change we no longer need to be kept in essence, since we have a change log. Remove these fields from the entity and add logging.


 class Client { private $id; private $name = ''; private $manager; // Manager private $address; // Address private $changes = []; // Change[] private function __construct( IdGenerator $generator, string $name, Manager $manager, Address $address, \DateTime $date_action, User $user, string $user_ip, string $user_agent ) { $this->id = $generator->generate(); $this->name = $name; $this->manager = $manager; $this->address = $address; $this->date_create = clone $date_action; $this->changes[] = new Change($this, 'create', $user, $user_ip, $user_agent, $date_action); } public static function register(IdGenerator $generator, RegisterClient $request) : Client { return new self( $generator, $request->name, $request->manager, $request->address, $request->date_action, $request->user, $request->user_ip, $request->user_agent ); } public function delegate(DelegateClient $request) { $this->manager = $request->new_manager; $this->changes[] = new Change( $this, 'delegate', $request->user, $request->user_ip, $request->user_agent, $request->date_action ); } //     } 

Now we create a new log instance for each action and we cannot put it into a separate method as the query class differs, although the fields are similar.


To solve this problem, I use contracts. Let's create this:


 namespace Domain\Security\UserAction; interface AuthorizedUserActionInterface { public function getUser() : User; public function getUserIp() : string; public function getUserAgent() : string; public function getDateAction() : \DateTime; } 

The interface can contain only methods. It cannot contain properties. This is one of the reasons why I prefer to use getters and setters in DTO, rather than public properties.


We will immediately implement for quick connection of this contract:


 namespace Domain\Security\UserAction; use Symfony\Component\HttpFoundation\Request; trait AuthorizedUserActionTrait { public function getUser() : User { return $this->user; } public function getUserIp() : string { return $this->user_ip; } public function getUserAgent() : string { return $this->user_agent; } public function getDateAction() : \DateTime { return clone $this->date_action; } //    protected function fillFromRequest(User $user, Request $request) { $this->user = $user; $this->user_agent = $request->headers->get('user-agent'); $this->user_ip = $request->getClientIp(); $this->date_action = new \DateTime(); } } 

Add our contract to DTO:


 class RegisterClient implements AuthorizedUserActionInterface { use AuthorizedUserActionTrait; protected $name = ''; protected $manager; // Manager protected $address; // Address protected $date_action; // \DateTime protected $user; // User protected $user_ip = ''; protected $user_agent = ''; public function __construct(User $user, Request $request) { $this->fillFromRequest($user, $request); } //... } 

Update the client change log so that it uses our new contract:


 class Change { private $client; // Client private $change = ''; private $user; // User private $user_ip = ''; private $user_agent = ''; private $date_action; // \DateTime //      public function __construct( Client $client, string $change, AuthorizedUserActionInterface $action ) { $this->client = $client; $this->change = $change; $this->user = $action->getUser(); $this->user_ip = $action->getUserIp(); $this->user_agent = $action->getUserAgent(); $this->date_action = $action->getDateAction(); } } 

Now we will create a change log based on our contract:


 class Client { // ... private function __construct( IdGenerator $generator, string $name, Manager $manager, Address $address, \DateTime $date_action ) { $this->id = $generator->generate(); $this->name = $name; $this->manager = $manager; $this->address = $address; $this->date_create = $date_action; } public static function register(IdGenerator $generator, RegisterClient $request) : Client { $self = new self( $generator, $request->getName(), $request->getManager(), $request->getAddress(), $request->getDateAction() ); $self->changes[] = new Change($self, 'register', $request); return $self; } public function delegate(DelegateClient $request) { $this->manager = $request->getNewManager(); $this->changes[] = new Change($this, 'delegate', $request); } public function move(MoveClient $request) { $this->address = $request->getNewAddress(); $this->changes[] = new Change($this, 'move', $request); } public function rename(RenameClient $request) { $this->name = $request->getNewName(); $this->changes[] = new Change($this, 'rename', $request); } } 

We have already significantly simplified the client classes and requests for its change. The next stage of development can be domain events. Should I use them is a moot point, but I will give them as an example:


 class Client implements AggregateEventsInterface { use AggregateEventsRaiseInSelfTrait; // ... public static function register(IdGenerator $generator, RegisterClient $request) : Client { // ... $self->raise(new ChangeEvent($self, 'register', $request)); return $self; } public function delegate(DelegateClient $request) { // ... $this->raise(new ChangeEvent($this, 'delegate', $request)); } //     //         $this->raise(); public function onChange(ChangeEvent $event) { $this->changes[] = new Change($this, $event->getChange(), $event->getRequest()); } } 

This was a small example of the evolution of the project using the DDD approach. This example is not the ultimate truth. Many things can be done differently. DDD is so good that everyone has his own.


Links



')

Source: https://habr.com/ru/post/321892/


All Articles