.NET
- Usando a abordagem DDD : Padrões e práticas - II
![]() |
Hoje veremos como podemos usar a abordagem Domain Driven Design na plataforma .NET apresentando os padrões e práticas relacionadas. |
A camada de domínio é o coração de uma aplicação baseada em DDD, contendo os principais blocos de construção e padrões que modelam o domínio do problema. Vmos a sguir discutir os principais blocos de construção, incluindo Entidades, Objetos de Valor e Eventos de Domínio, bem como padrões de design relevantes, como Padrão Factory, Padrão Repositório e Padrão Specification.
Entidades
Entidades são objetos com uma identidade única que pode mudar com o tempo. Eles
são os principais blocos de construção de um modelo de domínio e encapsulam o
estado e o comportamento.
Na plataforma .NET, as entidades normalmente são implementadas como classes com
uma propriedade de identificador exclusivo (por exemplo, um GUID ou um número
inteiro) e métodos que encapsulam a lógica do domínio.
Usar uma classe abstrata para encapsular propriedades comuns, como Id, Data de Criação, Data de Atualização e Data de Exclusão, é uma ótima prática de DDD e pode ser uma abordagem eficaz para reduzir a duplicação de código em entidades do domínio. Essa classe abstrata pode ser chamada de EntityBase ou algo semelhante e outras entidades podem herdar dela para aproveitar essas propriedades comuns.
public abstract class Entity
{
public int Id { get; protected set; }
public DateTime CreatedAt { get; protected set; }
public DateTime UpdatedAt { get; protected set; }
public DateTime? DeletedAt { get; protected set; }
}
|
A seguir considerando uma entidade Customer contendo Id, Name e Email podemos definir a seguinte entidade :
public class Customer : Entity
{
public string Name { get; private set; }
public string Email { get; private set; }
private Customer() { } // Construtor privado para o ORM
public Customer(string name, string email)
{
// Realize validações aqui, se necessário
Name = name;
Email = email;
CreatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
// Método de domínio para atualizar o e-mail do Customer
public void UpdateEmail(string newEmail)
{
// Realize validações aqui, se necessário
Email = newEmail;
UpdatedAt = DateTime.UtcNow;
}
// Método de domínio para excluir o Customer
public void MarkAsDeleted()
{
DeletedAt = DateTime.UtcNow;
}
}
|
Essa abordagem ajuda a manter a consistência e a reutilização de código, permitindo que várias entidades do domínio herdem as propriedades comuns e sigam as boas práticas de DDD. Além disso, ela facilita a manutenção e a evolução do código, pois as alterações comuns nas propriedades podem ser feitas em um só lugar, na classe Entity.
Value Object ou Objetos de Valor
Um Value Object é um dos principais conceitos do Domain-Driven Design (DDD) e representa um objeto imutável cuja identidade é definida pelos seus atributos. Ao contrário das Entidades, os Value Objects não têm uma identidade única e são comparados com base nos valores dos seus atributos, não em uma chave primária. Eles são usados para representar conceitos no domínio que não têm identidade própria, como endereços, valores monetários, coordenadas geográficas, etc.
Um Value Object é um tipo imutável que é distinguível apenas pelo estado de suas propriedades, ou seja, ele não possui uma identidade, e, de forma simples é óbvia, ele é um objeto que representa um valor.
Vamos considerar o endereço de um cliente, o endereço pode ser um bom candidato para um Value Object, pois sua identidade não é fundamental para o domínio, e muitas vezes é comparado com base em seus atributos.
Vamos criar um exemplo de Value Object Address para o Customer conforme o código seguir:
public class Address
{
public string Street { get; }
public string City { get; }
public string State { get; }
public string ZipCode { get; }
public Address(string street, string city, string state, string zipCode)
{
// Realize validações aqui, se necessário
Street = street;
City = city;
State = state;
ZipCode = zipCode;
}
}
|
Usando C#, os objetos de valor podem ser implementados como classes, structs ou records com propriedades somente leitura, comparação de igualdade com base em valores de propriedade e validação adequada de seu estado.
Agora o código da classe Customer ficaria assim:
public class Customer : Entity
{
public string Name { get; private set; }
public string Email { get; private set; }
|
Esta abordagem permite que o endereço seja tratado como um Value Object imutável e incorporado ao estado do cliente. Você pode criar, comparar e usar o endereço como parte das operações de Customer, seguindo as melhores práticas de DDD.
Uma definição mais refinada para o Value Object Address poderia ser definida da seguinte forma:
public class
Address : IEquatable<Address> { public string Street { get; } public string City { get; } public string State { get; } public string ZipCode { get; } public Address(string street, string city, string state, string zipCode) { // validar inputs Street = street; City = city; State = state; ZipCode = zipCode; } public bool Equals(Address other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; return Street == other.Street && City == other.City && State == other.State && ZipCode == other.ZipCode; } public override bool Equals(object obj) => Equals(obj as Address); public override int GetHashCode() => (Street, City, State, ZipCode).GetHashCode(); } |
Temos aqui a implementação da interface IEquatable<Address>, que é uma boa abordagem e oferece uma maneira eficiente de comparar dois objetos Address quanto à igualdade. Essa implementação é conhecida como uma implementação personalizada do método Equals e GetHashCode.
A implementação do Equals e GetHashCode usada é eficiente e considera todos os atributos do Address ao verificar a igualdade. Isso garante que dois objetos Address sejam considerados iguais apenas se todos os atributos forem iguais.
Usar essa abordagem é uma boa prática, especialmente quando você deseja comparar objetos Address com precisão e de forma eficiente, como quando precisa realizar operações como verificar se dois endereços são iguais em seu domínio.
Portanto, a implementação acima é uma escolha sólida e pode ser preferível quando se trabalha com Value Objects que precisam ser comparados quanto à igualdade de maneira detalhada.
Domain Events ou Eventos de domínio
Os Domain Events (Eventos de Domínio) representam eventos que ocorrem dentro do domínio de negócios e são usados para comunicar mudanças significativas no estado do domínio. Eles são uma maneira de desacoplar a lógica de negócios e permitir que diferentes partes do sistema respondam a eventos sem a necessidade de conhecer detalhes específicos sobre quem gerou o evento.
Vamos considerar para o nosso exemplo evento que ocorre quando o email for alterado.
Para implementar um domain event para isso podemos criar uma interface IDomainEvent e, em seguida, criar uma classe específica, como CustomerEmailChanged, que implementa essa interface. Essa é uma abordagem comum para representar eventos de domínio em um sistema DDD.
Primeiro, defina a interface IDomainEvent:
public interface IDomainEvent
{ }
|
Agora, crie uma classe específica para o evento CustomerEmailChanged que implementa IDomainEvent:
public class CustomerEmailChanged : IDomainEvent
{
public int CustomerId { get; }
public string PreviousEmail { get; }
public string NewEmail { get; }
public CustomerEmailChanged(int customerId, string previousEmail, string newEmail)
{
CustomerId = customerId;
PreviousEmail = previousEmail;
NewEmail = newEmail;
}
}
|
Essa abordagem é uma maneira organizada e flexível de representar eventos de domínio em seu sistema. A interface IDomainEvent serve como um marcador para todos os eventos de domínio e, em seguida, cada evento específico (como CustomerEmailChanged) implementa essa interface para indicar que é um evento de domínio.
Na classe Customer, você pode então despachar um evento de CustomerEmailChanged da mesma forma :
public class
Customer : Entity { ... // Método de domínio para atualizar o e-mail do Customer
public void UpdateEmail(string newEmail)
{
// Realize validações aqui, se necessário
string previousEmail = Email;
Email = newEmail;
UpdatedAt = DateTime.UtcNow;
// Dispara o evento de domínio EmailChanged
DomainEvents.Dispatch(new EmailChangedDomainEvent(Id,
previousEmail, newEmail));
}
...
}
|
Aqui adicionamos o método UpdateEmail, que realiza a atualização do e-mail e, em seguida, despacha o evento de domínio EmailChangedDomainEvent usando um mecanismo chamado DomainEvents.Dispatch. Esse mecanismo é fictício e precisa ser implementado no seu sistema para realmente despachar o evento para os ouvintes apropriados.
O EmailChangedDomainEvent carrega informações sobre a alteração do e-mail, como o ID do cliente, o e-mail anterior e o novo e-mail. Isso permite que outros componentes do sistema, como serviços de notificação por e-mail, reajam à alteração do e-mail do cliente.
Essa abordagem é especialmente útil quando você espera ter vários tipos de eventos de domínio em seu sistema, pois cada tipo de evento pode ser representado por uma classe separada que implementa IDomainEvent. Isso torna o sistema mais flexível e facilita a manutenção e a extensão do código à medida que novos eventos são introduzidos.
Aqui você pode questionar de onde vem esse tal de DomainEvents.Dispatch ????
Calma , vou
explicar...
O DomainEvents.Dispatch é uma parte de implementação fictícia que não faz parte da plataforma .NET. Você precisará criar seu próprio mecanismo de despacho de eventos de domínio em seu sistema.
Aqui está uma explicação de como você pode criar um mecanismo simples de despacho de eventos de domínio:
using System;
using System.Collections.Generic;
namespace SeuNamespace.Domain
{
public static class DomainEventDispatcher
{
private static readonly List<WeakReference> _handlers = new List<WeakReference>();
public static void Register<T>(Action<T> handler) where T : IDomainEvent
{
_handlers.Add(new WeakReference(handler));
}
public static void Dispatch<T>(T domainEvent) where T : IDomainEvent
{
foreach (var handlerRef in _handlers)
{
if (handlerRef.Target is Action<T> handler)
{
handler(domainEvent);
}
}
}
}
}
|
Esta classe DomainEventDispatcher permite registrar ouvintes (handlers) para eventos de domínio e, em seguida, despacha os eventos registrados para os ouvintes apropriados.
DomainEventDispatcher.Register<CustomerEmailChanged>(HandleCustomerEmailChanged);
|
private void HandleCustomerEmailChanged(CustomerEmailChanged @event)
{
// Supondo que você tenha um serviço de log para registrar eventos de auditoria
var auditLogService = new AuditLogService();
var customerId = @event.CustomerId;
var previousEmail = @event.PreviousEmail;
var newEmail = @event.NewEmail;
// Crie uma mensagem de log para registrar a alteração de e-mail
var logMessage = $"E-mail do cliente com ID {customerId} alterado de '{previousEmail}' para '{newEmail}'";
// Registre a mensagem de log no serviço de log de auditoria
auditLogService.Log(logMessage);
// Você também pode adicionar mais lógica relacionada à auditoria aqui, se necessário
}
|
Você pode personalizar a implementação de HandleCustomerEmailChanged para atender às necessidades específicas do seu sistema. Por exemplo, em vez de registrar em um log de auditoria, você pode notificar outros serviços, enviar e-mails de confirmação ao cliente ou executar outras ações relevantes à sua aplicação.
DomainEventDispatcher.Dispatch(new CustomerEmailChanged(Id, previousEmail, newEmail));
|
Lembre-se de que esta é apenas uma implementação simples e básica para ilustrar o conceito de despacho de eventos de domínio. Em sistemas reais, você pode usar bibliotecas mais avançadas ou estruturas de mensagens para implementar um mecanismo de despacho de eventos mais robusto e escalável.
O padrão Factory
O Factory Pattern é usado para criar objetos complexos ou agregados enquanto encapsula a lógica de criação do objeto. Em C#, as fábricas podem ser implementadas como métodos estáticos, classes separadas ou até mesmo como parte do próprio objeto de domínio.
O padrão Factory (ou fábrica) é uma ótima abordagem para criar instâncias de objetos quando a criação envolve lógica complicada ou validações. Neste caso, você pode criar uma classe CustomerFactory responsável por criar novas instâncias de Customer após validar os dados de entrada. Aqui está uma possível implementação:
public class CustomerFactory
{
public static Customer CreateCustomer(string name, string email, Address address)
{
// Realize validações aqui antes de criar o Customer
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("O nome do cliente não pode estar em branco.");
}
if (!IsValidEmail(email))
{
throw new ArgumentException("O e-mail não é válido.");
}
// Você pode adicionar mais validações aqui, se necessário
// Crie e retorne uma nova instância de Customer
return new Customer(name, email, address);
}
private static bool IsValidEmail(string email)
{
// Implemente validação de e-mail de acordo com seus requisitos
// Isso é apenas um exemplo simples
return !string.IsNullOrWhiteSpace(email) && email.Contains("@");
}
}
|
Agora, você pode usar a CustomerFactory para criar instâncias de Customer em seu código, garantindo que os dados de entrada sejam validados antes da criação da entidade.
Por exemplo:
var customer = CustomerFactory.CreateCustomer("Josias Silva", "josias@examplo.com",
new Address("Rua Projetada", "City", "State", "12345"));
|
Essa abordagem ajuda a manter a criação de objetos consistente e a centralizar a lógica de validação em um local, facilitando a manutenção e a evolução do código.
O padrão Repository
O padrão Repositório (Repository Pattern) é um padrão de projeto de software que abstrai o acesso a dados, fornecendo uma interface para interagir com as entidades do domínio sem expor os detalhes de como os dados são armazenados e recuperados. Ele ajuda a separar as preocupações de acesso a dados do resto do código de negócios, promovendo a manutenção do código, testabilidade e flexibilidade.
Este´padrão é usado para abstrair o mecanismo de persistência, permitindo que a camada de domínio se concentre na lógica de negócios, ao mesmo tempo em que é independente dos detalhes de armazenamento de dados. Ele pode ser implementado usando interfaces que definem as operações de persistência necessárias para um agregado específico.
Aqui está um exemplo de definição de uma interface ICustomerRepository para interagir com a entidade Customer realizando as operações básicas :
public interface ICustomerRepository
{
Customer GetById(int customerId);
List<Customer> GetAll();
void Add(Customer customer);
void Update(Customer customer);
void Delete(int customerId);
}
|
A implementação real desses métodos dependerá da fonte de dados subjacente, como um banco de dados, um repositório em memória ou qualquer outra fonte de armazenamento. A ideia é que a camada de domínio não precisa saber como os dados são realmente armazenados; essa responsabilidade é delegada à implementação concreta do repositório, que estará em uma camada de infraestrutura separada.
Você criaria uma classe concreta, CustomerRepository, que implementa a interface ICustomerRepository para interagir com sua fonte de dados real (por exemplo, um banco de dados SQL ou NoSQL). Essa classe concreta conteria a lógica real de acesso a dados e consultas para manipular objetos Customer.
Um exemplo básico de implementação de CustomerRepository seria:
using System;
using System.Collections.Generic;
using System.Linq;
namespace SeuNamespace.Infrastructure
{
public class CustomerRepository : ICustomerRepository
{
private List<Customer> _customers = new List<Customer>();
public Customer GetById(int customerId)
{
return _customers.FirstOrDefault(c => c.Id == customerId);
}
public List<Customer> GetAll()
{
return _customers;
}
public void Add(Customer customer)
{
// Gere o ID do cliente (pode ser automático em um banco de dados real)
customer.Id = GenerateCustomerId();
_customers.Add(customer);
}
public void Update(Customer customer)
{
var existingCustomer = _customers.FirstOrDefault(c => c.Id == customer.Id);
if (existingCustomer != null)
{
// Atualize os dados do cliente existente com os novos dados
existingCustomer.Name = customer.Name;
existingCustomer.Email = customer.Email;
existingCustomer.CustomerAddress = customer.CustomerAddress;
}
else
{
throw new InvalidOperationException("Cliente não encontrado para atualização.");
}
}
public void Delete(int customerId)
{
var customerToRemove = _customers.FirstOrDefault(c => c.Id == customerId);
if (customerToRemove != null)
{
_customers.Remove(customerToRemove);
}
else
{
throw new InvalidOperationException("Cliente não encontrado para exclusão.");
}
}
// Métodos adicionais do repositório, se necessário...
private int GenerateCustomerId()
{
// Implemente a lógica para gerar IDs de cliente
// Isso pode envolver consultar um banco de dados para obter um novo ID único.
// Neste exemplo simples, estamos apenas aumentando um contador.
int nextId = _customers.Count + 1;
return nextId;
}
}
}
|
Esta é uma implementação muito simples e simplificada usando a abordagem síncrona e um repositório específico para fins de exemplo. Em um ambiente real, você precisaria lidar com a persistência de dados, transações, tratamento de erros e outras considerações específicas do seu sistema.
O padrão Specification
O Padrão Specification é usado para encapsular lógica de consulta complexa e dissociá-la do restante do modelo de domínio. Na plataforma .NET, este padrão ser implementado usando classes que representam critérios de consulta específicos e podem ser combinados usando operadores lógicos.
Este padrão permite criar uma representação de consulta específica do domínio para consultas complexas ou filtragem de objetos em um sistema. Ele é frequentemente usado para encapsular regras de consulta em objetos reutilizáveis e compreensíveis, reduzindo assim a complexidade e aumentando a manutenibilidade do código.
No contexto do nosso exemplo com a entidade Customer, o value object Address, e o repositório CustomerRepository, podemos usar o padrão Specification para criar consultas reutilizáveis que encapsulem critérios de filtragem específicos do domínio. Isso pode ser útil quando você precisa realizar consultas complexas com base em vários critérios.
Aqui está um exemplo de como você pode usar o padrão Specification para consultar clientes com base em critérios específicos:
Primeiro, defina uma interface ISpecification<T> que representará as especificações:
public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
}
|
Agora, crie uma classe concreta que implementa ISpecification<Customer> para representar uma especificação de consulta para clientes que atendem a determinados critérios. Por exemplo, uma especificação para clientes com um determinado e-mail:
public class CustomerEmailSpecification : ISpecification<Customer>
{
private readonly string _email;
public CustomerEmailSpecification(string email)
{
_email = email;
}
public bool IsSatisfiedBy(Customer customer)
{
return customer.Email == _email;
}
}
|
Agora, você pode usar essa especificação para consultar clientes que atendem a esse critério específico em seu repositório CustomerRepository:
public class
CustomerRepository : ICustomerRepository { // Simulação de uma fonte de dados private List<Customer> _customers = new List<Customer>(); // ... Outros métodos do repositório ... public List<Customer> FindCustomersBySpecification(ISpecification<Customer> specification) { // Implemente a lógica de consulta usando a especificação return _customers.Where(specification.IsSatisfiedBy).ToList(); } // Restante da implementação do repositório... } |
Neste exemplo fictício, estamos usando uma lista _customers como uma simulação de uma fonte de dados. Você substituiria isso pela implementação real de acesso a dados, como o acesso a um banco de dados.
Aqui está como você usaria essa função para buscar clientes com base em um critério específico de e-mail em seu serviço de aplicativo:
public class CustomerService
{
private readonly ICustomerRepository _customerRepository;
public CustomerService(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public List<Customer> GetCustomersWithEmail(string email)
{
// Crie uma especificação com base no critério de e-mail
var emailSpecification = new CustomerEmailSpecification(email);
// Use a especificação para buscar clientes com o e-mail especificado
var customers = _customerRepository.FindCustomersBySpecification(emailSpecification);
return customers;
}
// Restante da lógica do serviço...
}
|
Neste exemplo, criamos uma instância de CustomerEmailSpecification com o critério de e-mail desejado e, em seguida, usamos essa especificação para buscar clientes no repositório CustomerRepository. A lista resultante contém os clientes que atendem ao critério especificado.
Lembre-se de que esse exemplo é simplificado e que a implementação real dependerá da fonte de dados e das especificações específicas do domínio que você precisa. O uso de especificações permite que você adicione critérios de consulta adicionais facilmente, sem modificar a lógica do repositório ou do serviço de aplicativo.
Na próxima parte do artigo vamos apresentar os padrões e práticas usadas na camada Application.
"Finalmente apareceu (Jesus) aos onze, estando eles
assentados à mesa, e lançou-lhes em rosto a sua incredulidade e dureza de
coração, por não haverem crido nos que o tinham visto já ressuscitado."
Marcos 16:14
Referências:
C# - Tasks x Threads. Qual a diferença
DateTime - Macoratti.net
Null o que é isso ? - Macoratti.net
Formatação de data e hora para uma cultura ...
C# - Calculando a diferença entre duas datas
NET - Padrão de Projeto - Null Object Pattern
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis)