Executando verificação de segurança...
5

DDD - Resignificando conceitos: Value Objects

No último post, exploramos a Modelagem Tática e os Padrões para DDD (Domain Driven Design), destacando a diferença entre abordagens de Entidades em ORM (Mapeamento Objeto Relacional) e DDD. Enquanto o primeiro foca na persistência de dados, o segundo considera as entidades como ricas em comportamentos que refletem as necessidades de negócios.

Agora, vamos discutir Value Objects e Agregados. O conceito de "Screaming Architecture", enfatizado por Uncle Bob (autor de "Código Limpo"), ressalta a importância de uma arquitetura que indique claramente como tudo deve ser feito. Isso requer abandonar hábitos prejudiciais, como reduzir a declaração de propriedades a simples tipos primitivos, e adotar Value Objects para uma modelagem mais expressiva.

Value Objects

Um Objeto de Valor é muito parecido com uma entidade, a diferença principal é que ele não possui uma identidade. Segundo Eric Evans (autor de “Domain Driven Design - Atacando as Complexidades no Coração do Software”):

“Chamamos um Objeto de Valor de um objeto que representa um aspecto descritivo do domínio, sem nenhuma identidade conceitual. Estes são instanciados para representar elementos do design com os quais nos preocupamos só pelo que eles são, e não quem são. Eles descrevem coisas.”

Trazendo para algo mais concreto, vamos pensar em um endereço Address. Num endereço, nós temos:

  • street
  • city
  • state
  • zip_code

De maneira geral, mapearíamos o endereço da nossa entidade Customer da seguinte forma:

class Customer:
    # Constructor of the class
    def __init__(self, id: str, name: str, street: str, city: str, state: str, zip_code: str):
        # Initialization of class attributes
        self._id = id
        self._name = name
        self._address = Address(street, city, state, zip_code)
        # Validation of attributes
        self.validate()

Assim, declaramos todas as propriedades de endereço como strings e elas estão intrínsecas à entidade Cliente. Mas se o cliente mudar para uma casa na mesma rua, ou seja, apenas mudou de número, é correto alterar esse atributo sem alterar o endereço do cliente? Não. Alterando qualquer atributo de endereço implica estar alterando um endereço em si. Nesse caso, não alteramos um endereço e sim, trocamos de endereço e por isso ele não precisa ter um identificador, pois ele não precisa ser único nesse sistema. Ele pode ser trocado. Assim, neste sistema, um endereço não se caracteriza como uma entidade e sim como um objeto de valor, pois ele descreve algo e não é único.

Agora, se pensarmos em um endereço no contexto de uma companhia de fornecimento de energia, cada casa tem um endereço único, pois obviamente casas não trocam de endereço, então faz sentido um endereço de fornecimento ser uma entidade e não como um objeto de valor, pois neste caso ele é único e tem uma identidade. Assim, temos que ter o cuidado para não generalizar e analisar caso a caso, de forma que faça sentido para as necessidades de negócios.

Modelando um Objeto de Valor

Vamos então modelar esse objeto de valor da maneira correta. Neste contexto de gestão de clientes, vamos criar um módulo address.py na pasta value_object, de modo que a estrutura do projeto fique dessa forma:

└── src
    └── domain
        └── customer
            ├── entity
            │   └── customer.py
            └── value_object
                └── address.py

Assim, implementamos a classe do nosso objeto de valor endereço da seguinte maneira:

class Address:
    # Constructor of the class
    def __init__(self, street: str, city: str, state: str, zip_code: str):
        # Initialization of class attributes
        self._street = street
        self._city = city
        self._state = state
        self._zip_code = zip_code
        # Validation of attributes
        self.validate()

    # Method to validate the attributes
    def validate(self):
        if len(self._street) == 0:
            raise ValueError("Street is required")
        if len(self._city) == 0:
            raise ValueError("City is required")
        if len(self._state) == 0:
            raise ValueError("State is required")
        if len(self._zip_code) == 0:
            raise ValueError("Zip code is required")

    # Method to get the full address
    def __str__(self):
        return f"{self._street}, {self._city}, {self._state}, {self._zip_code}"

Note que essa é uma classe privada, pois podemos apenas acessar seus atributos, mas não alterá-los. Também temos uma validação desse objeto de valor para que na hora que formos instanciá-lo, possamos garantir uma consistência e coesão desses dados. Por último, temos um método simples para obter o endereço completo. A partir disso, quando o nosso cliente precisar mudar de endereço, ele terá que trocar de endereço e não alterar o atual.

Agora podemos compreender que o papel dos Value Objects na modelagem de domínio é fundamental para desenvolver sistemas mais coesos e expressivos. Ao adotar uma abordagem que valoriza a representação precisa dos elementos do negócio, podemos construir aplicações mais robustas e alinhadas com as necessidades reais dos usuários.

No próximo post, exploraremos o ciclo de vida de um objeto de domínio, proporcionando uma visão abrangente sobre como gerenciar e manipular entidades dentro de um contexto de desenvolvimento de software. Aprofundar esse tema nos permitirá avançar ainda mais na jornada de design de software guiada pelo Domain Driven Design.


Este artigo é resultado de estudos pessoais e foi profundamente inspirado pelo conteúdo apresentado no curso FullCycle 3.0. Agradeço à equipe da FullCycle pela excelente qualidade do material educacional fornecido. Todos os créditos e reconhecimentos vão para a FullCycle pela sua contribuição significativa para o meu aprendizado.

2
0