Y
Y
yalandaev2017-07-06 14:11:57
C++ / C#
yalandaev, 2017-07-06 14:11:57

How to make friends REST API and DDD concept?

Recently, I started to get acquainted with the concept of DDD, and the question arose - how to properly handle the CREATE and UPDATE operations when the data comes through the Web API. For example, on a certain form on a web page, we fill in the fields, click "Save". A POST request with form data flies to the /api/customers address:

{
  "Id": "93967a3e-384f-459a-8b50-0a0f4cc66d66",
  "firstName": "Иван",
  "lastName": "Череззаборногузадерищенко",
  "zipCode": 245876,
  "city": "Самара",
  "street": "Николая Панова",
  "houseNumber": 64,
  "appartmentsNumber": 62,
}

On the Web API side, it is deserialized into a DTO class:
public class CustomerDTO
{
  public Guid Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public int ZipCode { get; set; }
  public string City { get; set; }
  public string Street { get; set; }
  public int HouseNumber { get; set; }
  public int AppartmentsNumber { get; set; }
  public Guid CustomerStateId { get; set; }
}

We have the following domain model (done very crudely to demonstrate the example):
public class Customer: Entity, IAggreagtionRoot
{
        public Customer(Name name)
        {
            Name = name;
        }
  public Name Name { get; private set; }
  public Address Address { get; private set; }
  public CustomerState State { get; private set; }

  public void ChangeState(CustomerState newState)
  {
    //some logic...
  }

  public void ChangeAddress(Address newAddress)
  {
    //some logic...
    Address = newAddress;
  }

  public void ChangeName(Name newName)
  {
    //some logic...
    Name = newName;
  }
}

public class CustomerState: Entity
{
  public static readonly CustomerState Regular = new CustomerState(new Guid("7beb8006-1b70-4d47-bb95-1976a2c18e9a"), "Regular");
  public static readonly CustomerState VIP = new CustomerState(new Guid("c27e9e0c-a2dc-4093-80f5-e75b66997746"), "VIP");

  public CustomerState(Guid id, string name)
  {
    Id = id;
    Name = name;
  }
  public string Name { get; private set; }
}
public class Address: ValueType
{
  public Address(int zipCode, string city, string street, int houseNumber, int appartmentsNumber)
  {
    ZipCode = zipCode;
    City = city;
    Street = street;
    HouseNumber = houseNumber;
    AppartmentsNumber = appartmentsNumber;
  }

  public int ZipCode { get; private set; }  
  public string City { get; private set; }
  public string Street { get; private set; }
  public int HouseNumber { get; private set; }
  public int AppartmentsNumber { get; private set; }
}

public class Name: ValueType
{
  public Name(string firstName, string lastName)
  {
    FirstName = firstName;
    LastName = lastName;
  }

  public string FirstName { get; private set; }
  public string LastName { get; private set; }
}

Question 1 . What is the correct way to change the Customer object from a DTO object? The following comes to mind:
public class CustomerService
{
  private CustomerRepository _customerRepository;
  public CustomerService(CustomerRepository customerRepository)
  {
    _customerRepository = customerRepository;
  }

  public void UpdateCustomer(CustomerDTO customerDto)
  {
    Guid customerId = customerDto.Id;
    Customer customer = _customerRepository.GetById(customerId);

    //Как мне дейстовать здесь?
    customer.ChangeName(new Name(customerDto.FirstName, customerDto.LastName));
    customer.ChangeAddress(new Address(customerDto.ZipCode, customerDto.City, customerDto.Street, customerDto.HouseNumber, customerDto.AppartmentsNumber));

    if(customerDto.CustomerStateId == CustomerState.VIP.Id)
      customer.ChangeState(CustomerState.VIP);
    if (customerDto.CustomerStateId == CustomerState.Regular.Id)
      customer.ChangeState(CustomerState.Regular);
    //Что-то вроде этого?

    //Может быть согласовать дизайн UI с дизайном доменной модели, и не позволять 
    //изменять статус с помощью полей формы, и изменять его отдельными действиями (например, специальными кнопками, 
    //которые инициируют POST-запрос www.site.ru/api/customers/2432352/PromoteToVip ?

    _customerRepository.Update(customer);
  }
}

Question 2. What if I want to send only changes, and not the entire object? For example, only the house number has changed, and in order to minimize the data sent, I want to send a request like this:
{
  "houseNumber": 164
}

How can I change the Value-type Address in this case if its constructor requires all parameters?
Could it be something like this?:
Customer customer = _customerRepository.GetById(customerId);

var zipCode = customerDto.ZipCode == 0 ? customer.Address.ZipCode : customerDto.ZipCode;
var street = string.IsNullOrEmpty(customerDto.Street) ? customer.Address.Street : customerDto.Street;
var city = string.IsNullOrEmpty(customerDto.City) ? customer.Address.City : customerDto.City;
var appartmentsNumber = customerDto.AppartmentsNumber == 0 ? customer.Address.AppartmentsNumber : customerDto.AppartmentsNumber;
var houseNumber = customerDto.HouseNumber == 0 ? customer.Address.HouseNumber : customerDto.HouseNumber;

customer.ChangeAddress(new Address(zipCode, city, street, houseNumber, appartmentsNumber));

Question 3. What if the object has many properties, say 40. Let's say 20 of them do not affect the consistency of the model (i.e. purely informational, and you do not need to monitor their change). Make setter methods for each (which makes me sad), or just expose them for editing (make the setter public) to external code?
PS I think that the public void ChangeState(CustomerState newState) method should be removed according to the canons of DDD, and something like PromoteToVip() and DowngradeToRegular() should be introduced instead, but let everything be as it is in this example.

Answer the question

In order to leave comments, you need to log in

2 answer(s)
V
vova07, 2017-07-13
@vova07

Greetings!
I will try to explain as briefly as possible the essence and missed points that are observed in your example.
Read about the so-called domain services ( Domain Services ) and application services ( Application Services ). In other articles, people refer to the answer to your question as Use Cases .
Use Case is a separate service that does not break stateless and has one specific task.

final class CreateCustomer
{
  /**
   * @var IWriteRepository Интерфейс репозитория, где на самом деле хранистя конкретная имплементация записи сущности.
   */
  private $repository;

  public function __construct(IWriteRepository $repositorym Validator $validator)
  {
    $this->repository = $repository;
    ...
  }

  /**
   * @param string $id ИД будущей записи.
   * @param array $data ПОТ данные которые мы получаем в момент АПи запроса.
   */
  public function handle(string $id, array $data) : void
  {
    /**
     * Валидация выбрасывает исключение если данные не валидны или возвращает массив валидных данных.
     */
    $dto = $this->validator->validate($data);

    $customer = new CustomerEntity(new UUID($id));
    /* Ентити это сущность которое содержить исключительно только бизнес логику и безнесс поведения. 
     * В вашем примере у вас были разные сеттеры, в правильном подходе это лишенно смысла,
     * и совсем неправильно. Думайте об методах аггрегата (Сушность с которой работает репозиторий это ни * что инное как аггрегат) как проекция бизесс логики. 
     * для примера в реальном мире принято говорить `Новый клиент зарегистрировался в системе`, мы никогда * не перечисляем цепь проделанных событий. Кто-то говорит `Клиент заполнил свой аддрес, ФИО, потом
     * телефон, и отправил данные` ?!
     * 
     * Если вы заметили правильно, то в конструкторе сущности передается только идентификатор.
     * Такой подход похож на реальную ситуацию, что позволяет легко проектировать бизнес логику.
     * регистрация клиента можно сравнить с регистрацией карточки пользователя в магазине.
     * - Берём бумагу (или специальный блокнот)
     * - Указываем номер клиента (обычно его как-то генерят например дата, время, или номер карточки 
     * которую ему выдают)
     * - начинаем спрашивать клиента кго персональные данные и заполняем все в ОДНОМ процессе.
     * - Потом кидаем блокнот/бумагу оратно или в колецию где лежит другая похожая инфа.
     */
    $customer->register($dto['name'], ...);

    /**
     * Именно этот метод делает сохранение нового клиента.
     * Сама реализация интереса живет в персистентном слое (`persistence layer`).
     */
    $this->repository->store($customer);
  }
}

The example above demonstrates a common user key which is ( Application Service ) and which lives in the application layer. I want to note that it does exactly one operation (creates a new client) and returns nothing.
Note: If you pay attention, the user-keys in its "handle" method accepts only scalar data, this is an important point that allows you to write reusable user-keys. The simplest example is a web API and a console command that is entered in the terminal.
Data validation is done in the same class (this doesn't override domain validation but shows one of the good places to do it in the application layer.)
Another note is that it's very important to follow a simple rule: ( SRP ) - one user case one operation.
The answer to the question that is most likely to be: What if we need to create a client and return a response, that is, in a normal REST situation?
final class PostController extend Controller
{
  public function __construct(IWriteRepository, CreateCustomer $createCustomUseCase, RetrieveCustomer $retrieveCustomerUseCase)
  {
    ...
  }

  public function __invoke(array $data)
  {
    $id = $this->repository->pickNextId();
    
    $this->createCustomUseCase->handle($id, $data);

    return $this->retrieveCustomerUseCase->handle($id);
  }
}

REST API - does not mean that what you are describing.
In REST ideology, the same response must be returned to a client request. That is, in short, this means receiving an entire resource (a resource is the so-called request / response scheme) and returning it. (The response may contain additional data, but they are auxiliary, such as headers, pagination, and so on.)
Everything that is not business logic should be taken out of the entity.
Essence is the state of the absence of a process.
If some attributes do not make sense for the business, then they should be avoided.
If you give specific examples of such attributes that I could understand, then I can write an answer in more detail on their solution, otherwise it is difficult for me to express it.
If it includes logs, temporary tags, additional data for other subsequent operations, then the entity is designed incorrectly.
In one entity ( Entity ) there can only be attributes to satisfy the behaviors of this entity, everything else is not part of the entity.
Setters are an anti-pattern that should always be avoided.
Design entities so that only business behaviors are present. In real code, these are methods that project the same behavior. As a result, we get a constructor, and many methods that follow strictly business processes, third-party setters are a sign of a problematic architecture.
different entities can contain (encapsulate) different Value Objects , the separation of classes by strict types is very important. If there is a life cycle and an identifier, then this is an entity, if there is no cycle, then this is VO (VO = Value Object).
Your CustomerStatedoes not have a life cycle, which immediately limits you in using the essence. This is a sirogo VO. And if different "if", "else" appear in the logic (my personal opinion, which is shared by other developers), then this is a sign of bad architecture, and a reason to think about dividing Vo into two or subtypes. "VipState", "RegularState" but this is more true for situations with more than 10 2 values, if there are two of them, then it is possible to make a protein flag that will indicate one or another value.
P.S: To make your life easier in the future, try to write code that will never have default values ​​in methods, this will change your perception of the code, and help you write better.
PS: All of the above is personal observation and personal interpretation of the theory. You CAN consider my IDEAS but DO NOT FOLLOW my interpretations.
DDD is the principle of thinking, no one in the world has yet been able to prove and describe a 100% correct approach that should be a standard, even the author of thinking.
Try, experiment and share your experience!
Good luck!

M
MrDywar Pichugin, 2017-07-06
@Dywar

the ChangeAddress method is no different from the same code in a property.
As far as I understand DDD is cool for very large and complex projects.
In a typical project - there are many business operations, all this is implemented in services that are used everywhere. One change can affect the operation of several, even unrelated business processes. Monolithic business model where everything is collected.
In a DDD project, for each business operation, almost its own project is started with its own DTOs, tables with a database and its own services. It is much easier to develop one thing without delving into and without fear of hurting other business operations.
The difficulty here is to put it all together.
So a method or property in one Entity is not really something you need to think about for a long time.
Pluralsight - Entity Framework in the Enterprise. Great example.
Question 2.
It is better to send everything, there is not much data. The very fact of the request is a significantly higher load.
But there will be problems with understanding the absence of a value from an empty field.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question