Testar grátis
8 min leitura Guide 856 of 877

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

Soluções Relacionadas