Padrões e Práticas de Domain-Driven Design
Domain-Driven Design (DDD) é uma abordagem de desenvolvimento de software que se concentra na modelagem de domínios de negócio complexos através de padrões estratégicos e táticos. O DDD ajuda as equipes a criar software que se alinha estreitamente com as necessidades de negócio enquanto mantém excelência técnica e adaptabilidade.
Domain-Driven Design Estratégico
Contextos Limitados
Contextos limitados definem os limites dentro dos quais um modelo de domínio particular se aplica:
Padrões de Mapeamento de Contexto:
Sales Context ──► Customer Context ──► Support Context
│ │ │
└─ Shared Kernel ────┼─ Customer ─────────┘
│
└─ Separate Ways
Relacionamentos de Contexto:
- Shared Kernel: Subconjunto comum de modelo de domínio compartilhado entre contextos
- Customer-Supplier: Um contexto depende de outro
- Conformist: Segue o modelo de outro contexto
- Anti-Corruption Layer: Traduz entre diferentes modelos
Linguagem Ubíqua
Colaboração com Especialista de Domínio:
Business Analyst ──► Domain Expert ──► Developer ──► Code
│ │ │ │
└─ Business Terms ─────┼─ Ubiquitous ──┼─ Domain ─┘
│ Language │ Objects
└───────────────┘
Evolução da Linguagem:
- Comece com terminologia de negócio
- Refine através de conversas
- Reflita mudanças no código imediatamente
- Use os mesmos termos na documentação, testes e discussões
Domain-Driven Design Tático
Padrões da Camada de Domínio
Entidades
Entidades representam objetos de domínio com identidade e ciclo de vida:
Características da Entidade:
class Customer {
private readonly id: CustomerId;
private name: PersonName;
private email: EmailAddress;
private status: CustomerStatus;
private addresses: Address[];
constructor(id: CustomerId, name: PersonName, email: EmailAddress) {
this.id = id;
this.name = name;
this.email = email;
this.status = CustomerStatus.ACTIVE;
this.addresses = [];
}
changeName(newName: PersonName): void {
if (this.status !== CustomerStatus.ACTIVE) {
throw new DomainError('Cannot change name of inactive customer');
}
this.name = newName;
this.addDomainEvent(new CustomerNameChanged(this.id, newName));
}
equals(other: Customer): boolean {
return this.id.equals(other.id);
}
}
Objetos de Valor
Objetos de valor são imutáveis e definidos por seus atributos:
Implementação de Objeto de Valor:
class Money {
private readonly amount: number;
private readonly currency: Currency;
constructor(amount: number, currency: Currency) {
if (amount < 0) {
throw new DomainError('Amount cannot be negative');
}
this.amount = amount;
this.currency = currency;
}
add(other: Money): Money {
if (!this.currency.equals(other.currency)) {
throw new DomainError('Cannot add different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency.equals(other.currency);
}
}
Agregados
Agregados definem limites de consistência e encapsulam regras de negócio:
Padrão Aggregate Root:
class Order {
private readonly id: OrderId;
private readonly customerId: CustomerId;
private status: OrderStatus;
private items: OrderItem[];
private totalAmount: Money;
constructor(id: OrderId, customerId: CustomerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.DRAFT;
this.items = [];
this.totalAmount = Money.zero(Currency.USD);
}
addItem(productId: ProductId, quantity: number, unitPrice: Money): void {
if (this.status !== OrderStatus.DRAFT) {
throw new DomainError('Cannot modify confirmed order');
}
const existingItem = this.items.find(item => item.productId.equals(productId));
if (existingItem) {
existingItem.increaseQuantity(quantity);
} else {
this.items.push(new OrderItem(productId, quantity, unitPrice));
}
this.recalculateTotal();
}
confirm(): void {
if (this.items.length === 0) {
throw new DomainError('Cannot confirm empty order');
}
this.status = OrderStatus.CONFIRMED;
this.addDomainEvent(new OrderConfirmed(this.id, this.totalAmount));
}
}
Serviços de Domínio
Serviços contêm lógica de negócio que não se encaixa naturalmente em entidades ou objetos de valor:
Exemplo de Serviço de Domínio:
class PricingService {
constructor(
private readonly discountRepository: DiscountRepository,
private readonly taxCalculator: TaxCalculator
) {}
calculateTotal(order: Order, customer: Customer): Money {
let subtotal = order.getSubtotal();
// Apply customer discounts
const customerDiscounts = this.discountRepository.findByCustomer(customer.id);
for (const discount of customerDiscounts) {
if (discount.appliesTo(subtotal)) {
subtotal = discount.applyTo(subtotal);
}
}
// Calculate taxes
const taxAmount = this.taxCalculator.calculateTax(subtotal, customer.address);
return subtotal.add(taxAmount);
}
}
Eventos de Domínio
Eventos de domínio representam ocorrências significativas de negócio:
Padrão de Evento de Domínio:
abstract class DomainEvent {
public readonly eventId: string;
public readonly aggregateId: string;
public readonly eventVersion: number;
public readonly occurredOn: Date;
constructor(aggregateId: string) {
this.eventId = uuidv4();
this.aggregateId = aggregateId;
this.eventVersion = 1;
this.occurredOn = new Date();
}
}
class OrderPlaced extends DomainEvent {
constructor(
public readonly orderId: OrderId,
public readonly customerId: CustomerId,
public readonly totalAmount: Money
) {
super(orderId.toString());
}
}
Camada de Aplicação
Padrões de Comando e Consulta
Padrão de Comando:
abstract class Command {
public readonly commandId: string;
public readonly timestamp: Date;
constructor() {
this.commandId = uuidv4();
this.timestamp = new Date();
}
}
class PlaceOrderCommand extends Command {
constructor(
public readonly customerId: CustomerId,
public readonly items: OrderItem[]
) {
super();
}
}
class CommandHandler {
constructor(
private readonly orderRepository: OrderRepository,
private readonly domainEventPublisher: DomainEventPublisher
) {}
async handle(command: PlaceOrderCommand): Promise<OrderId> {
const order = Order.create(command.customerId);
for (const item of command.items) {
order.addItem(item.productId, item.quantity, item.unitPrice);
}
await this.orderRepository.save(order);
await this.domainEventPublisher.publish(order.getDomainEvents());
return order.id;
}
}
Serviços de Aplicação
Camada de Serviço de Aplicação:
class OrderApplicationService {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus
) {}
async placeOrder(request: PlaceOrderRequest): Promise<PlaceOrderResponse> {
const command = new PlaceOrderCommand(
new CustomerId(request.customerId),
request.items.map(item => new OrderItem(
new ProductId(item.productId),
item.quantity,
new Money(item.unitPrice, Currency.USD)
))
);
const orderId = await this.commandBus.send(command);
return { orderId: orderId.toString() };
}
async getOrderDetails(orderId: string): Promise<OrderDetailsDto> {
const query = new GetOrderDetailsQuery(new OrderId(orderId));
return await this.queryBus.send(query);
}
}
Camada de Infraestrutura
Padrão Repository
Implementação de Repository:
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: OrderId): Promise<Order | null>;
findByCustomerId(customerId: CustomerId): Promise<Order[]>;
nextIdentity(): OrderId;
}
class SqlOrderRepository implements OrderRepository {
constructor(private readonly db: Database) {}
async save(order: Order): Promise<void> {
const data = this.mapToData(order);
await this.db.orders.upsert({
where: { id: order.id.toString() },
update: data,
create: data
});
}
async findById(id: OrderId): Promise<Order | null> {
const data = await this.db.orders.findUnique({
where: { id: id.toString() }
});
return data ? this.mapToDomain(data) : null;
}
}
Implementação de Mapeamento de Contexto
Camada Anti-Corrupção
Implementação ACL:
class LegacySystemAdapter {
constructor(private readonly legacyApi: LegacyApiClient) {}
async getCustomer(customerId: string): Promise<Customer> {
const legacyCustomer = await this.legacyApi.getCustomer(customerId);
return new Customer(
new CustomerId(legacyCustomer.id),
new PersonName(legacyCustomer.firstName, legacyCustomer.lastName),
new EmailAddress(legacyCustomer.email)
);
}
async updateCustomer(customer: Customer): Promise<void> {
await this.legacyApi.updateCustomer({
id: customer.id.toString(),
firstName: customer.name.firstName,
lastName: customer.name.lastName,
email: customer.email.toString()
});
}
}
Teste de Modelos de Domínio
Teste Unitário de Lógica de Domínio
Teste de Entidade:
describe('Customer', () => {
it('should change name when active', () => {
const customer = new Customer(
new CustomerId('123'),
new PersonName('John', 'Doe'),
new EmailAddress('john@example.com')
);
customer.changeName(new PersonName('Jane', 'Doe'));
expect(customer.name.firstName).toBe('Jane');
});
it('should not change name when inactive', () => {
const customer = new Customer(/* ... */);
customer.deactivate();
expect(() => {
customer.changeName(new PersonName('Jane', 'Doe'));
}).toThrow(DomainError);
});
});
Teste de Integração
Teste de Repository:
describe('OrderRepository', () => {
let repository: OrderRepository;
let db: TestDatabase;
beforeEach(async () => {
db = await createTestDatabase();
repository = new SqlOrderRepository(db);
});
it('should save and retrieve order', async () => {
const order = Order.create(new CustomerId('123'));
order.addItem(new ProductId('456'), 2, new Money(10, Currency.USD));
await repository.save(order);
const retrieved = await repository.findById(order.id);
expect(retrieved?.id).toEqual(order.id);
expect(retrieved?.getTotalAmount()).toEqual(new Money(20, Currency.USD));
});
});
Anti-Padrões Comuns de DDD
Modelo de Domínio Anêmico
Anti-Padrão:
// ❌ Anemic - no business logic
class Order {
id: string;
items: OrderItem[];
status: string;
// Just getters and setters
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
Abordagem Correta:
// ✅ Rich domain model
class Order {
// Business logic encapsulated
calculateTotal(): Money {
return this.items.reduce(
(total, item) => total.add(item.getLineTotal()),
Money.zero(this.currency)
);
}
}
Classes Deus
Anti-Padrão:
// ❌ God class - too many responsibilities
class OrderManager {
createOrder() { /* ... */ }
calculateTax() { /* ... */ }
sendEmail() { /* ... */ }
updateInventory() { /* ... */ }
}
Abordagem Correta:
// ✅ Single responsibility classes
class OrderFactory { /* ... */ }
class TaxCalculator { /* ... */ }
class EmailService { /* ... */ }
class InventoryService { /* ... */ }
Integração com GitScrum
Modelagem de Domínio em Gerenciamento de Tarefas
DDD para Gerenciamento de Projeto:
- Modele projetos como contextos limitados
- Use eventos de domínio para mudanças de estado de tarefas
- Implemente agregados para limites de sprint e projeto
Linguagem Ubíqua na Comunicação da Equipe
Compreensão Compartilhada:
- Defina termos claros de domínio para tarefas e workflows
- Use terminologia consistente entre membros da equipe
- Reflita conceitos de domínio nas configurações do quadro GitScrum