Olá!

$whoami

  • Arquiteto de software na Talentify.io
  • Ex sysadmin.
  • Usuário GNU/Linux há 11 anos e entusiasta de tecnologias livres desde o tempo em que isso era ruim.
  • Atualmente dedicado a criar softwares e arquiteturas que persistam ao tempo e estudando aprendizado de máquina.
  • @gnumoksha

atualmente trabalho como arquiteto de software buscando criar softwares que persistam ao tempo. Utilizo GNU/Linux e outras tecnologias, desenvolvendo software há cerca de 11 anos.

Tópicos

  • Visão geral do DDD
  • Entidades (filhas e relacionadas)
  • Value Objects
  • Agregados
  • Repositórios (com Doctrine, em memória, entre outros)

vou mostrar algumas técnicas para sair da aplicação PHP tradicional

Visão geral do Domain-driven design (DDD)

DDD é primariamente sobre modelar uma Linguagem Ubíqua em um Contexto Limitado explícito.

Domain-Driven Design Distilled

O que é, o que faz, quais problemas resolve messaging middleware product for this case, but it’s easy to do so since you already use it for publishing to other Bounded Contexts.

Entidades

Active Record e Data Mapper

data mapper tem maior curva de aprendizado mas nao é fortemente acoplado como o AR, nem tem tantas responsabilidaes, por há uma camada separada para a abstraçao do DB.

Exemplo de entidade.

class Customer
{
    /** @var string */
    private $name;

    /** @var EmailAddress */
    private $email;

    /** @var CustomerStatus **/
    private $status;
}

What Is an Entity? An Entity models an individual thing. Each Entity has a unique identity in that you can distinguish its individuality from among all other Entities of the same or a different type. Many times, perhaps even most times, an Entity will be mutable; that is, its state will change over time. Still, an Entity is not of necessity mutable and may be immutable. The main thing that separates an Entity from other modeling tools is its uniqueness—its individuality. Entidades e o agregado, que será visto mais a frente, são os primeiros locais onde voces devem olhar para criar o código da solução.

Entidades anêmicas

orientação a objetos é sobre colocar dados e comportamentos no mesmo lugar. You should be on a mission to fight the Anemic Domain Model. If you expose public setter methods, it could quickly lead to anemia, because the logic for setting values on Product would be implemented outside the model. Think hard before doing this, and keep this warning in mind.

class Customer
{
    private $id;
    private $name;
    private $email;
    private $status;

    public function getId(): ?int
    {
        return $this->id;
    }
    public function getName(): ?string
    {
        return $this->name;
    }
    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }
    public function getEmail(): ?string
    {
        return $this->email;
    }
    public function setEmail(string $email): self
    {
        $this->email = $email;
        return $this;
    }
    public function setStatus(CustomerStatus $status): self
    {
        $this->status = $status;
        return $this;
    }}

Adicionando comportamento 1/2

class Customer
{
    private $id;
    private $name;
    private $email;
    private $status;

    public function __construct(
        string $name,
        string $email
    ) {
        $this->name = $name;
        $this->email = $email;
    }
    // ...

    public function updateEmail(string $newEmail) : void
    {
        $event = new EmailChanged($this->id, $this->email, $newEmail);
        $dispatcher->dispatch(EmailChanged::NAME, $event);

        $this->email = $newEmail;
    }
    public function getEmail(): string
    {
        return $this->email;
    }
}

minha entidade tem campos obrigatorios. Posso querer tomar uma ação quando o customer alterar seu e-mail.

Adicionando comportamento 2/2

class Customer
{
    // ...
    /** @var int whether or not the customer is approved */
    private $status;

    public function __construct(
        string $name,
        string $email
    ) {
        $this->name = $name;
        $this->email = $email;
        $this->status = 0;
    }

    // ...

    public function updateEmail(string $newEmail) : void
    {
        $event = new EmailChanged($this->id, $this->email, $newEmail);
        $dispatcher->dispatch(EmailChanged::NAME, $event);

        $this->email = $newEmail;
    }

    public function approve() : void
    {
        $this->status = 1;
    }

    public function getStatus() : int
    {
        return $this->status;
    }
}

Ao inves de deixar a cargo de outras classes a lógica de aprovação do customer, posso colocar isso nela.

Objetos de valor

A Value Object, or simply a Value, models an immutable conceptual whole. Within the model the Value is just that, a value. Unlike an Entity, it does not have a unique identity, and equivalence is determined by comparing the attributes encapsulated by the Value type. Furthermore, a Value Object is not a thing but is often used to describe, quantify, or measure an Entity.

https://github.com/moneyphp/money

/**
 * Representa um endereço de e-mail tratado e válido.
 */
class EmailAddress
{
    /** @var string */
    private $emailAddress;

    /** @throws \InvalidArgumentException */
    public function __construct(string $emailAddress)
    {
        // remove characters that are considered spaces.
        $formatted = mb_ereg_replace('\s', '', $emailAddress);
        // remove dot at the end of the email address (as it is a common mistake).
        $formatted = rtrim($formatted, '.');
        if (!filter_var($formatted, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException(sprintf("Value '%s' is not a email address.", $emailAddress));
        }
        $this->emailAddress = $formatted;
    }

    public function getEmailAddress() : string
    {
        return $this->emailAddress;
    }

    public function equals($other) : bool
    {
        if (! $other instanceof self) {
            return false;
        }
        return $this->getEmailAddress() === $other->getEmailAddress();
    }}

imutável, sem setters, precisa ter equals (data+behavior).

Refactor 1/2

class Customer
{
    // ...

    /** @var EmailAddress */
    private $email;

    public function __construct(
        string $name,
        EmailAddress $email
    ) {
        $this->name = $name;
        $this->email = $email;
        $this->status = 0;
    }

    // ...

    public function updateEmail(EmailAddress $newEmail) : void
    {
        if (!$this->email->equals($newEmail)) {
            $event = new EmailChanged($this->id, $this->email, $newEmail);
            $dispatcher->dispatch(EmailChanged::NAME, $event);

            $this->email = $newEmail;
        }
    }

    public function getEmail(): EmailAddress
    {
        return $this->email;
    }}
/**
 * Contem todos os status que um Customer pode ter.

 * @method static CustomerStatus WAITING_CREATION_APPROVAL()
 * @method static CustomerStatus ACTIVE()
 */
class CustomerStatus extends \MyCLabs\Enum\Enum
{
    private const WAITING_CREATION_APPROVAL = 10;
    private const ACTIVE                    = 20;

    public function getName() : string
    {
        return mb_convert_case($this->getKey(), MB_CASE_TITLE);
    }

    public function getDisplayName() : string
    {
        $values = [
            10 => 'Aguardando aprovação',
            20 => 'Ativo',
        ];
        return $values[$this->getValue()];
    }
}

Refactor 2/2

class Customer
{
    // ...

    /** @var CustomerStatus */
    private $status;

    public function __construct(
        string $name,
        string $email
    ) {
        $this->name = $name;
        $this->email = $email;
        $this->status = CustomerStatus::WAITING_CREATION_APPROVAL();
    }

    public function approve() : void
    {
        if (!$this->status->equals(CustomerStatus::ACTIVE())) {
            $this->status = CustomerStatus::ACTIVE();

            $event = new CustomerApproved($this->id);
            $dispatcher->dispatch(CustomerApproved::NAME, $event);
        }
    }

    public function getStatus() : CustomerStatus
    {
        return $this->status;
    }
}

Entidades filhas vs relacionadas

explicar o conceito (ciclo de vida)

perguntar qual acham que sao as filhas e relacionadas

falar de como mapear o relacionamento destas entidades. (não usar Doctrine para isso)

Agregados

o que é um agregado? Conjunto de uma ou mais entidades, uma delas é raiz, podem ter VO.

composição dele, regras de negócio, transação, consistência, obtencao sempre da raiz.

Regras de ouro dos Agregados

nao sao regras escritas em pedra.

1 - proteja as invariantes do negócio dentro dos limites do Agregado

com base em consistencia das regras de negocio. Ao final da unica transaçao deve estar tudo ok.

valor total = de cada item

2 - Desenhe pequenos agregados

uso de memoria e escopo da transaçao devem ser relativamente pequenos. teste facil.

Single Responsibility Principle (SRP). 1 unico motivo para mudar.

3 - Referencie outros Agregados apenas pela identidade

carregamento rapido, salvamente facil, não há jeito fácil de acessar outros.

modificar apenas um. outros agregados referenciam apenas o root.

class Customer
{
    // ...

    /** @var int */
    private $id;

    public function __construct(
        string $name,
        string $email
    ) {
        $this->name = $name;
        $this->email = $email;
        $this->status = CustomerStatus::WAITING_CREATION_APPROVAL();
    }
class Customer
{
    // ...

    /** @var CustomerId */
    private $id;

    public function __construct(
        CustomerId $customerId,        string $name,
        string $email
    ) {
        $this->id = $customerId;
        $this->name = $name;
        $this->email = $email;
        $this->status = CustomerStatus::WAITING_CREATION_APPROVAL();
    }
class CustomerId
{
    /** @var string */
    private $id;

    public function __construct(?string $id = null)
    {
        $this->id = $id ?? \Ramsey\Uuid\Uuid:uuid4()->toString();
    }

    public function id() : string
    {
        return $this->id;
    }

    /**
     * @param CustomerId $object
     */
    public function equals($object) : bool
    {
        if (!$object instanceof self) {
            return false;
        }
        return $this->id() === $object->id();
    }
}

4 - Atualize outros Agregados utilizando consistência eventual

class Order
{
    // ...

    public function approve() : void
    {
        if (!$this->status->equals(OrderStatus::APPROVED())) {
            $this->status = OrderStatus::APPROVED();

            $event = new OrderApproved($this->id);
            $dispatcher->dispatch(OrderApproved::NAME, $event);
        }
    }
}

Repositórios

Faz a mediação entre o domínio e as camadas de mapeamento de dados utilizando uma interface do tipo coleção para acessar os objetos de domínio.

https://martinfowler.com/eaaCatalog/repository.html

interface CustomerRepository
{
    public function ofCustomerId(CustomerId $customerId) : ?Customer;

    public function ofEmail(EmailAddress $email) : ?Customer;

    public function add(Customer $customer) : void;
    public function nextIdentity() : CustomerId;
}
class DoctrineCustomerRepository extends ServiceEntityRepository implements CustomerRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Customer::class);
    }

    public function ofId(int $id) : ?Customer
    {
        return $this->find($id);
    }

    public function ofCustomerId(CustomerId $customerId) : ?Customer
    {
        return $this->findOneBy(['customerId' => $customerId]);
    }

    public function ofEmail(EmailAddress $email) : ?Customer
    {
        return $this->findOneBy(['email' => $email]);
    }
}
class InMemoryCustomerRepository implements CustomerRepository
{
    /** @var Customer[] */
    private $customers;

    public function __construct()
    {
        $this->customers = [
            new Customer(new CustomerId(), 'FooBar', new EmailAddress('foo@bar.com')),
            new Customer(new CustomerId(), 'FooBarzinho', new EmailAddress('foo@barzinho.com')),
        ];
    }

    public function ofCustomerId(CustomerId $id) : ?Customer
    {
        foreach ($this->customers as $customer) {
            if ($customer->getCustomerId()->equals($id)) {
                return $customer;
            }
        }
        return null;
    }
}

Referências

http://verraes.net/2013/12/related-entities-vs-child-entities/

Perguntas?

  • @gnumoksha
  • me@tobias.ws