paint-brush
Introdução prática ao design orientado a domíniopor@tobi
1,576 leituras
1,576 leituras

Introdução prática ao design orientado a domínio

por Piotr Tobiasz15m2023/07/22
Read on Terminal Reader

Muito longo; Para ler

Terminologia DDD, blocos de construção e outros conceitos para começar.
featured image - Introdução prática ao design orientado a domínio
Piotr Tobiasz HackerNoon profile picture
0-item
1-item

Não deve ser inovador dizer que escrever software não é apenas escrever código - é resolver um problema específico. Mesmo que sejam os desenvolvedores que eventualmente implementam a solução, não são os desenvolvedores que definem qual é o problema em primeiro lugar. Essa tarefa é realizada por vários empresários, que consideram processos, riscos e resultados para descrever qual é o problema, por que existe e como deve ser tratado. Em um contexto orientado ao domínio, esses profissionais são chamados de Especialistas em Domínio .


Do ponto de vista da engenharia, parece que os especialistas de domínio possuem um ativo valioso: seu conhecimento sobre o domínio. No entanto, esse conhecimento raramente é compartilhado em sua forma bruta. Em vez disso, geralmente é traduzido em requisitos para que os desenvolvedores possam entendê-lo e implementá-lo. O problema com essa abordagem é que o conhecimento de domínio de pessoas de negócios e desenvolvedores pode divergir. Isso significa que as perspectivas daqueles que definem o problema e daqueles que trabalham para resolvê-lo podem não se alinhar, levando a mal-entendidos e conflitos.


Então, qual é a saída? Certifique-se de que as pessoas de negócios e técnicas usem a mesma linguagem e terminologia.

O que é o Domain-Driven Design?

Domain-Driven Design (DDD) é uma metodologia que enfatiza a importância de criar um entendimento compartilhado entre especialistas de domínio e partes interessadas técnicas e alinhar a solução de software com os requisitos de negócios subjacentes. Esta parece ser uma definição não técnica de alto nível, mas também pode ser dividida em algo mais amigável ao desenvolvedor:


O DDD representa conceitos do mundo real no código e sua estrutura, cultivando e usando linguagem onipresente, construída pela modelagem do domínio de negócios.


Ainda há alguma terminologia a ser introduzida, então pode não estar 100% clara agora. O mais importante é que o DDD fornece ferramentas e atividades que permitem escrever e estruturar códigos alinhados com a visão do negócio. Não se trata apenas de comunicação, mas também de tomar decisões de design que realmente moldam a linguagem comum.

Terminologia

Não deveria ser nenhuma surpresa que o termo mais importante no mundo DDD é o domínio . Aqui está como Vlad Khononov, autor de "Learning Domain-Driven Design", descreve:


Um domínio de negócios define a principal área de atividade de uma empresa.


Isso significa que o domínio de negócios também pode ser considerado como:


  • A principal fonte de receita de uma empresa,
  • Pelo que a empresa é mais conhecida,
  • Qualquer coisa que a empresa faça melhor do que seus concorrentes.


O domínio também pode ser dividido em subdomínios - faixas de atividades mais específicas. Embora existam três tipos diferentes de subdomínios, o mais importante é o núcleo . Descreve como a empresa obtém a vantagem comercial. Os outros dois são sobre problemas genéricos mais comuns, como sistemas de autenticação ou painéis administrativos internos.


Ter uma compreensão profunda do domínio de negócios de uma empresa é absolutamente crucial para utilizar plenamente os benefícios do Domain-Driven Design. A melhor fonte desse entendimento não é outra senão os Especialistas de Domínio . Esses são os indivíduos cujo problema está sendo resolvido com o software - partes interessadas, várias pessoas de negócios e até mesmo usuários. Isso não quer dizer que os engenheiros estejam desinformados sobre o domínio em que estão trabalhando, mas sim que os Especialistas são a fonte da verdade do conhecimento do domínio. Ao trabalhar em conjunto com os especialistas de domínio, os desenvolvedores podem garantir que os modelos do domínio permaneçam precisos e atualizados.


Isso leva a outro termo crítico, mas potencialmente ambíguo: modelo . Eric Evans, em seu livro sobre o DDD, descreve o modelo da seguinte forma:


É uma interpretação da realidade que abstrai os aspectos relevantes para resolver o problema em questão e ignora os detalhes estranhos.


Vlad Khononov continua explicando essa ideia em termos mais relacionáveis:


Um modelo não é uma cópia do mundo real, mas uma construção humana que nos ajuda a entender os sistemas do mundo real.


Em conclusão, um modelo é uma representação de um conceito de negócio ou processo que facilita a compreensão e a comunicação da complexidade subjacente do domínio.


Vlad usou um mapa para ilustrar o conceito de um modelo de domínio de forma eficaz. Os mapas são um exemplo perfeito de como eles exibem apenas informações relevantes para o tipo de mapa, como topografia, estradas ou fronteiras. Um mapa que exibe todos os detalhes de uma só vez seria opressor e praticamente inútil. Os modelos de domínio também podem ser encontrados em outras formas, como:


  • Pedidos de clientes, que representam a versão simplificada de todos os processos que acontecem em segundo plano,
  • Menus de restaurantes, onde os itens listados no menu são os produtos finais, em vez de listar todos os ingredientes e etapas do processo de preparação,
  • Reservas de viagens, onde a viagem reservada destaca apenas os detalhes mais críticos, embora muito mais seja envolvido no planejamento da viagem, hotel, etc.


A última peça do quebra-cabeça da terminologia Domain-Driven Design (DDD) é a Linguagem Ubíqua. Refere-se à linguagem compartilhada usada pelas partes interessadas técnicas e de negócios em um projeto. Ter uma linguagem comum para descrever o domínio de negócios derivado do modelo de domínio é crucial no DDD. Isso ajuda a garantir que todos os membros da equipe tenham uma compreensão clara do espaço do problema, seus conceitos e seus relacionamentos. Isso leva a um melhor alinhamento e reduz o risco de mal-entendidos. Ao usar a linguagem ubíqua, a solução de software pode refletir com precisão os requisitos de negócios subjacentes, tornando-a um componente crítico do DDD.


Com a maior parte da terminologia abordada, deve ser mais fácil entender o que é design orientado a domínio . Agora é hora de mergulhar no como real - os blocos de construção do DDD.

Blocos de construção

Os blocos de construção DDD servem como base para a criação de um modelo de domínio eficaz e eficiente. Vlad Khononov define o Modelo de Domínio da seguinte maneira:


Um modelo de domínio é um modelo de objeto do domínio que incorpora comportamento e dados.


O Modelo de Domínio consiste em vários blocos de construção e estruturas. Os mais importantes são:


  • Objetos de valor,
  • Entidades,
  • Agregados,
  • eventos de domínio,
  • Repositórios,
  • Serviços de Domínio.

Objetos de valor

Os objetos de valor são os blocos de construção mais básicos disponíveis. São objetos definidos por um conjunto de atributos e valores. Eles não têm um identificador exclusivo - seus valores definem sua identidade. Eles são imutáveis no sentido de que valores diferentes já representam um objeto de valor diferente. Exemplos de objetos de valor incluem:


  • Valor monetário,
  • Intervalo de datas,
  • Endereço postal.


Aqui está como um objeto de valor simples pode ser implementado em Python:


 from pydantic import BaseModel class Address(BaseModel):  """Customer address."""  country: str  city: str  street: str  house_number: str  class Config:    frozen = True


Isso significa que usar o operador de igualdade ( == ) para comparar dois endereços retornará True somente se ambos os objetos tiverem exatamente os mesmos valores atribuídos.

Entidades

As entidades são o próximo tipo de bloco de construção. As entidades representam objetos individuais no domínio com uma identidade distinta, como uma pessoa ou uma ordem. Eles são semelhantes aos objetos de valor na maneira como também armazenam os dados, mas seus atributos podem e devem ser alterados e, portanto, precisam de um identificador exclusivo. Pedidos e informações pessoais são apenas duas instâncias simples das Entidades:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel):  """Personal data."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  first_name: str  last_name: str  address: Address class Order(BaseModel):  """Customer order."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  description: str  value: float


Como os valores das instâncias são modificáveis em ambos os casos, elas precisam de uma identificação, que pode ser um UUID. O mais importante é que, na maioria dos casos, as entidades não devem ser gerenciadas diretamente, mas por meio de um agregado .

Agregar

Um Agregado é um tipo de entidade porque é mutável e requer um identificador exclusivo. Sua principal responsabilidade, no entanto, não é armazenar dados, mas agrupar um conjunto de objetos relacionados (Entidades e Objetos de Valor) juntos como uma única unidade de consistência. O Aggregate é o objeto raiz, com um limite bem definido que encapsula seu estado interno e impõe invariantes para garantir a consistência de todo o grupo. Os agregados permitem raciocinar sobre o domínio de maneira mais natural e intuitiva, concentrando-se nas relações entre os objetos, e não nos próprios objetos.


Seguindo os exemplos anteriores, um agregado pode ser representado como um cliente:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel):  """Customer aggregate.  Manages personal information as well as orders.  """  id: uuid.UUID = Field(default_factory=uuid.uuid4)  person: Person  orders: list[Order] = Field(default_factory=list)  def change_address(self, new_address: Address) -> None:    self.person.address = new_address  def add_order(self, order: Order) -> None:    if self.total_value + order.value > 10000:      raise ValueError("Order cannot have value higher than 10000")    self.orders.append(order)  def remove_order(self, order_id: uuid.UUID) -> None:    order = next((order for order in self.orders if order.id == order_id), None)    if order is None:      raise IndexError("Order not found")    self.orders.remove(order)  @property  def total_value(self) -> float:    return sum(order.value for order in self.orders)


O cliente está diretamente ligado aos dados pessoais, e armazena todas as encomendas. Além disso, o agregado expõe uma interface para gerenciar o endereço da pessoa, bem como adicionar e remover pedidos. Isso se deve ao fato de que o estado do agregado só pode ser alterado executando os métodos correspondentes.


Embora o exemplo anterior seja relativamente direto, com apenas uma restrição (o valor do pedido não pode ser maior que 10.000), ele deve demonstrar o uso de blocos de construção DDD e seus relacionamentos. Em sistemas reais, os agregados geralmente são mais complexos, com mais restrições, limites e possivelmente mais relacionamentos. Afinal, sua própria existência é para administrar essa complexidade. Além disso, no mundo real, os agregados normalmente seriam mantidos em um armazenamento de dados, como um banco de dados. É aqui que o padrão de repositório entra em ação.

Repositório

Todas as alterações de estado do agregado devem ser confirmadas transacionalmente em uma única operação atômica. No entanto, não é responsabilidade do agregado “persistir”. O padrão de repositório permite abstrair os detalhes de armazenamento e recuperação de dados e, em vez disso, trabalhar com agregados em um nível mais alto de abstração. Simplificando, um repositório pode ser considerado como uma camada entre o agregado e o armazenamento de dados. Um arquivo JSON é um exemplo bastante simples de tal armazenamento. O agregado do cliente pode ter um repositório que opera em arquivos JSON:


 import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository:  """Customer repository operating on JSON files."""  def __init__(self, path: str) -> None:    self.path = path  def get(self, customer_id: uuid.UUID) -> Customer:    with open(self.path, "r") as file:      database = json.load(file)      customer = database["customers"].get(str(customer_id))      if customer is None:        raise IndexError("Customer not found")      person = database["persons"][str(customer["person"])]      orders = [database["orders"][order_id] for order_id in customer["orders"]]    return Customer(      id=customer["id"],      person=person,      orders=orders,    )  def save(self, customer: Customer) -> None:    with open(self.path, "r+") as file:      database = json.load(file)      # Save customer      database["customers"][str(customer.id)] = {        "id": customer.id,        "person": customer.person.id,        "orders": [o.id for o in customer.orders],      }      # Save person      database["persons"][str(customer.person.id)] = customer.person.dict()      # Save orders      for order in customer.orders:        database["orders"][str(order.id)] = order.dict()      file.seek(0)      json.dump(database, file, indent=4, default=str)


É claro que essa classe poderia (e talvez devesse) fazer muito mais, mas não pretende ser um ORM multifuncional perfeito. Deve dar uma ideia sobre as responsabilidades do repositório, que neste caso são armazenamento e recuperação do agregado do cliente no arquivo JSON . Também é importante observar como o repositório lida com as entidades associadas ao agregado. Como os dados e pedidos pessoais estão intimamente ligados ao ciclo de vida do cliente, eles devem ser gerenciados precisamente quando o agregado está sendo processado.

Serviço de domínio

Outro caso a ser considerado é quando existe uma lógica de negócio que simplesmente não se encaixa no agregado ou em qualquer uma de suas entidades ou objetos de valor. Pode ser uma lógica dependente de vários agregados ou do estado do armazenamento de dados. Nesses casos, uma estrutura conhecida como Domain Service pode ser útil. O Serviço de Domínio deve ser capaz de gerenciar agregações, por exemplo, usando o repositório e, em seguida, pode armazenar a lógica de domínio que não pertence à agregação. Por exemplo, um cliente pode exigir lógica para evitar a perda de muitos pedidos:


 import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol):  """Customer repository interface."""  def get(self, customer_id: uuid.UUID) -> Customer:    ...  def save(self, customer: Customer) -> None:    ... class CustomerService:  """Customer service."""  def __init__(self, repository: CustomerRepository) -> None:    self.repository = repository  def get_customer(self, customer_id: uuid.UUID) -> Customer | None:    try:      return self.repository.get(customer_id)    except IndexError:      return None  def save_customer(self, customer: Customer) -> None:    existing_customer = self.get_customer(customer.id)    # If customer is already in the database and has more than 2 orders,    # he cannot end up with half of them after a single save.    if (      existing_customer is not None      and len(existing_customer.orders) > 2      and len(customer.orders) < (len(existing_customer.orders) / 2)    ):      raise ValueError(        "Customer cannot lose more than half of his orders upon single save!"      )    self.repository.save(customer)


Aggregate não pode garantir como seu estado difere daquele no arquivo JSON porque não tem conhecimento do arquivo JSON em primeiro lugar. É por isso que a lógica de comparação deve ser incluída no Serviço de Domínio. Também é importante observar que o Domain Service deve funcionar com a abstração do repositório. Isso simplifica a troca da implementação concreta por uma alternativa usando injeção de dependência.

Juntando tudo

Com todas as peças já cobertas, elas podem ser vistas como um programa de trabalho:


 import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer(  person=entities.Person(    first_name="Peter",    last_name="Tobias",    address=value_objects.Address(      country="Germany",      city="Berlin",      street="Postdamer Platz",      house_number="2/3",    ),  ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)


Tudo tem suas responsabilidades e limites. Aggregate é responsável por gerenciar suas entidades e objetos de valor, bem como impor suas restrições. O serviço de domínio usa o repositório JSON injetado para manter os dados no arquivo JSON e impor limites de domínio adicionais. No final, cada componente tem uma função e significado distintos dentro do domínio especificado.

Resumo

Domain-Driven Design é, sem dúvida, uma ideia complexa de entender. Ele fornece práticas, padrões e ferramentas para ajudar as equipes de software a lidar com os problemas de negócios mais desafiadores, colocando uma forte ênfase no domínio de negócios. DDD é mais do que apenas um conjunto de blocos de construção, no entanto. É uma mentalidade que requer colaboração e comunicação entre as partes interessadas técnicas e de negócios. Uma compreensão compartilhada do domínio, expressa por meio de linguagem onipresente, é fundamental para o sucesso de um projeto DDD. Quando bem feito, o DDD pode levar a um software mais alinhado com as necessidades do negócio e mais eficaz na solução de problemas complexos.

Posfácio e os próximos passos

Este artigo nunca teve a intenção de ser algo como "DDD: From Zero To Hero", mas sim servir como uma introdução ao universo DDD. Eu queria demonstrar os conceitos mais importantes do Domain-Driven Design de uma maneira muito direta e prática. Acredito que aprender Domain-Driven Design é uma excelente maneira de aumentar a experiência em programação. No entanto, você não ouve sobre isso com muita frequência - pelo menos não tanto quanto "11 dicas e truques INSANOS de JavaScript - um tópico 🧵".


De qualquer forma, se você achou isso interessante, pode procurar na seção de fontes por livros e artigos que me inspiraram a escrever este artigo em primeiro lugar. Existem alguns conceitos que não abordei porque pensei que estavam além do escopo desta introdução, mas vale a pena investigá-los:


  • Contextos Delimitados
  • Eventos de domínio
  • Invasão de eventos


Sem dúvida, você os encontrará nas fontes listadas abaixo.

Fontes

Exemplos de código usados no artigo podem ser encontrados aqui: link .


Learning Domain-Driven Design por Vlad Khononov. Um livro incrível que serviu como uma grande fonte de inspiração para mim. Explica todos os conceitos discutidos neste artigo em maior profundidade.


Padrões de Arquitetura em Python por Harry Percival e Bob Gregory. Li o livro há quase dois anos e teve um impacto significativo em mim como desenvolvedor. Voltei a ele enquanto escrevia este artigo e isso me ajudou mais uma vez.


DDD em Python por Przemysław Górecki. Descobri este blog perto do final da redação do artigo, mas despertou meu interesse por causa de seu profissionalismo insano. Curiosidade: trabalhei na mesma empresa que Przemysław e não sabia disso.


Publicado também aqui .