intermediário65 minutosLição 3 de 7

Entidades e Casos de Uso

Aprenda a projetar entidades como regras de negócio empresariais e casos de uso como regras de negócio específicas da aplicação

Entidades e Casos de Uso

Entidades e Casos de Uso formam as duas camadas mais internas da Arquitetura Limpa. Juntos, eles contêm toda a lógica de negócio. Nada nessas camadas depende de frameworks, bancos de dados ou sistemas externos.

ℹ️Note

Entidades representam regras de negócio de toda a empresa. Casos de Uso representam regras de negócio específicas da aplicação. A distinção é sobre escopo, não tecnologia.

Entidades: Regras de Negócio Empresariais

python
from dataclasses import dataclass, field from decimal import Decimal from enum import Enum, auto from typing import List class OrderStatus(Enum): PENDING = auto() CONFIRMED = auto() SHIPPED = auto() DELIVERED = auto() CANCELLED = auto() @dataclass class Address: street: str city: str state: str zip_code: str country: str def is_valid(self) -> bool: return all([self.street, self.city, self.country]) @dataclass class Customer: customer_id: str name: str email: str shipping_address: Address | None = None def update_email(self, new_email: str) -> None: if "@" not in new_email: raise ValueError("Email inválido") self.email = new_email @dataclass class OrderItem: product_id: str product_name: str quantity: int unit_price: Decimal def total_price(self) -> Decimal: return Decimal(str(self.quantity)) * self.unit_price @dataclass class Order: order_id: str customer: Customer items: List[OrderItem] = field(default_factory=list) status: OrderStatus = OrderStatus.PENDING def add_item(self, item: OrderItem) -> None: if self.status != OrderStatus.PENDING: raise ValueError("Não pode modificar pedido não-pendente") self.items.append(item) def confirm(self) -> None: if self.status != OrderStatus.PENDING: raise ValueError("Apenas pedidos pendentes podem ser confirmados") if not self.items: raise ValueError("Não pode confirmar pedido vazio") self.status = OrderStatus.CONFIRMED def cancel(self) -> None: if self.status in (OrderStatus.SHIPPED, OrderStatus.DELIVERED): raise ValueError("Não pode cancelar pedido enviado ou entregue") self.status = OrderStatus.CANCELLED
💡Tip

Entidades devem ser puras — sem imports de frameworks, sem mapeamentos ORM, sem lógica de serialização. São classes Python simples com métodos de negócio.

Princípios de Design de Entidades

PrincípioDescriçãoExemplo
EncapsulamentoProteger estado internoPropriedades, atributos privados
AutovalidaçãoProteger contra estados inválidosVerificações em setters
Rico em comportamentoMétodos, não getters/settersorder.confirm(), não order.status = "confirmed"
Ignorante de persistênciaSem consciência de BD/ORMSem save(), sem atributo objects
Livre de frameworkSem imports de frameworksSem modelos Django, sem imports Flask

Casos de Uso: Regras de Negócio da Aplicação

python
from abc import ABC, abstractmethod from dataclasses import dataclass from decimal import Decimal @dataclass class PlaceOrderInput: customer_id: str items: list[dict] @dataclass class PlaceOrderOutput: order_id: str total: Decimal status: str class CustomerRepository(ABC): @abstractmethod def find_by_id(self, customer_id: str) -> Customer | None: ... class ProductRepository(ABC): @abstractmethod def find_by_id(self, product_id: str) -> "Product | None": ... class OrderRepository(ABC): @abstractmethod def save(self, order: Order) -> None: ... class PlaceOrderUseCase: def __init__(self, customer_repo: CustomerRepository, product_repo: ProductRepository, order_repo: OrderRepository): self._customer_repo = customer_repo self._product_repo = product_repo self._order_repo = order_repo def execute(self, input_data: PlaceOrderInput) -> PlaceOrderOutput: customer = self._customer_repo.find_by_id(input_data.customer_id) if customer is None: raise ValueError(f"Cliente {input_data.customer_id} não encontrado") items = [] for item_data in input_data.items: product = self._product_repo.find_by_id(item_data["product_id"]) if product is None: raise ValueError(f"Produto {item_data['product_id']} não encontrado") items.append(OrderItem(product.product_id, product.name, item_data["quantity"], product.price)) order = Order(order_id=self._generate_id(), customer=customer, items=items) order.confirm() self._order_repo.save(order) return PlaceOrderOutput(order_id=order.order_id, total=order.calculate_total(), status=order.status.name) def _generate_id(self) -> str: import uuid return str(uuid.uuid4())
100%

Um Caso de Uso = Uma Classe

💡Tip

Um padrão comum é um caso de uso por classe. Isso dá a cada caso de uso uma única responsabilidade e os torna testáveis independentemente.

python
# FAÇA ASSIM: Classes separadas para casos de uso separados class RegisterUserUseCase: def execute(self, input_data) -> User: ... class UpdateUserProfileUseCase: def execute(self, user_id, profile_data) -> User: ... # NÃO ASSIM: Classe god com tudo class UserManager: def register(self, ...): ... def update_profile(self, ...): ... def deactivate(self, ...): ... def send_welcome_email(self, ...): ...

DTOs para Casos de Uso

python
from dataclasses import dataclass from decimal import Decimal from typing import List, Optional @dataclass class CreateProductInput: name: str description: str price: Decimal category_id: str tags: List[str] @dataclass class ProductOutput: product_id: str name: str price: Decimal

Testando Casos de Uso

python
import pytest from decimal import Decimal def test_place_order_successfully(): customer = Customer(customer_id="C1", name="Alice", email="alice@test.com") product = Product(product_id="P1", name="Widget", price=Decimal("10.00"), stock=5, category="Gadgets") customer_repo = InMemoryCustomerRepository([customer]) product_repo = InMemoryProductRepository([product]) order_repo = InMemoryOrderRepository() use_case = PlaceOrderUseCase(customer_repo, product_repo, order_repo) output = use_case.execute(PlaceOrderInput(customer_id="C1", items=[{"product_id": "P1", "quantity": 3}])) assert output.status == "CONFIRMED" assert output.total == Decimal("30.00") def test_place_order_fails_for_nonexistent_customer(): use_case = PlaceOrderUseCase(InMemoryCustomerRepository([]), InMemoryProductRepository(), InMemoryOrderRepository()) with pytest.raises(ValueError, match="não encontrado"): use_case.execute(PlaceOrderInput(customer_id="FAKE", items=[]))

Entidade vs Caso de Uso

PerguntaColocar na EntidadeColocar no Caso de Uso
Esta regra muda por aplicação?NãoSim
É uma regra de negócio universal?SimNão
Depende de dados externos?NãoSim
Orquestra múltiplas entidades?NãoSim
Envolve I/O (BD, API, arquivo)?NãoSim

Composição de Casos de Uso

python
class CheckoutUseCase: def __init__(self, place_order: PlaceOrderUseCase, process_payment: ProcessPaymentUseCase, send_confirmation: SendConfirmationUseCase, update_inventory: UpdateInventoryUseCase): self._place_order = place_order self._process_payment = process_payment self._send_confirmation = send_confirmation self._update_inventory = update_inventory def execute(self, input_data: CheckoutInput) -> CheckoutOutput: order_output = self._place_order.execute(PlaceOrderInput(customer_id=input_data.customer_id, items=input_data.items)) payment_output = self._process_payment.execute(ProcessPaymentInput(customer_id=input_data.customer_id, amount=order_output.total)) for item in input_data.items: self._update_inventory.execute(UpdateInventoryInput(product_id=item["product_id"], quantity_reduced=item["quantity"])) self._send_confirmation.execute(SendConfirmationInput(customer_id=input_data.customer_id, order_id=order_output.order_id)) return CheckoutOutput(order_id=order_output.order_id, transaction_id=payment_output.transaction_id, total=order_output.total)

Antipadrões Comuns

AntipadrãoDescriçãoCorreção
Entidade AnêmicaEntidade é um saco de dados sem métodosMover lógica relacionada para a entidade
Caso de Uso GordoCaso de uso faz coisas demaisDividir em casos de uso focados
Pular EntidadesToda lógica em casos de usoExtrair regras compartilhadas para entidades
Repositório na EntidadeEntidade chama banco de dadosMover persistência para caso de uso via repositório

Exercícios Práticos

  1. Construa uma Entidade: Crie uma entidade BankAccount com métodos deposit(amount), withdraw(amount), transfer_to(target, amount).

  2. Crie um Caso de Uso: Escreva um TransferMoneyUseCase que depende de AccountRepository (interface) e usa a entidade BankAccount.

  3. Entidade vs Caso de Uso: A regra "Pedidos acima de R$100 têm frete grátis" deve estar na entidade ou no caso de uso? Justifique.

  4. Teste um Caso de Uso: Escreva testes pytest para CancelOrderUseCase. Inclua testes para cancelamento bem-sucedido e falha.

  5. Anêmico para Rico: Refatore esta entidade anêmica para ser rica em comportamento:

    python
    @dataclass class Booking: id: str; room_id: str; guest_name: str; check_in: str; check_out: str; status: str; total: float
  6. Caso de Uso Composto: Escreva um BookRoomUseCase que compõe: ValidateRoomAvailabilityUseCase, CreateBookingUseCase, ChargeDepositUseCase.

  7. Padrão Repositório In-Memory: Implemente InMemoryBookingRepository com save, find_by_id, find_by_room_and_date.

  8. Design de DTO: Projete DTOs de entrada e saída para um SearchFlightsUseCase.

Success

Entidades guardam o o quê e Casos de Uso guardam o como. Mantenha entidades livres de framework e casos de uso focados em orquestração.

O Fluxo de um Caso de Uso

Cada caso de uso segue um padrão padrão:

  1. Receber entrada do controlador
  2. Validar a entrada (regras de negócio)
  3. Buscar entidades necessárias dos repositórios
  4. Orquestrar métodos das entidades para realizar a operação
  5. Persistir mudanças através dos repositórios
  6. Retornar saída para o apresentador
python
class CancelOrderUseCase: def __init__(self, order_repo: OrderRepository, notification_service: NotificationService): self._order_repo = order_repo self._notification = notification_service def execute(self, order_id: str, reason: str) -> None: order = self._order_repo.find_by_id(order_id) if order is None: raise ValueError("Pedido não encontrado") order.cancel() self._order_repo.save(order) self._notification.send( recipient=order.customer.email, subject="Pedido Cancelado", body=f"Seu pedido {order_id} foi cancelado. Motivo: {reason}", )

Quando Usar Entidade vs Caso de Uso

A distinção entre entidade e caso de uso é crucial. Aqui está uma regra prática:

python
# ENTIDADE: Regra universal — aplica-se em qualquer contexto class Money: def __init__(self, amount: Decimal, currency: str = "BRL"): if amount < 0: raise ValueError("Valor não pode ser negativo") self.amount = amount self.currency = currency def __add__(self, other: "Money") -> "Money": if self.currency != other.currency: raise ValueError("Moedas diferentes não podem ser somadas") return Money(self.amount + other.amount, self.currency) # CASO DE USO: Regra específica — varia por aplicação class CalculateShippingCostUseCase: def __init__(self, shipping_provider: ShippingProvider): self._provider = shipping_provider def execute(self, order: Order, zip_code: str) -> Money: weight = sum(item.quantity * 0.5 for item in order.items) return self._provider.calculate(weight, zip_code)

Testes de Entidades

Entidades são as unidades mais fáceis de testar porque não têm dependências externas:

python
import pytest from decimal import Decimal class TestOrderEntity: def test_initial_status_is_pending(self): customer = Customer("C1", "Alice", "a@test.com") order = Order("ORD-1", customer) assert order.status == OrderStatus.PENDING def test_confirm_changes_status(self): customer = Customer("C1", "Alice", "a@test.com") order = Order("ORD-1", customer) order.add_item(OrderItem("P1", "Widget", 1, Decimal("10.00"))) order.confirm() assert order.status == OrderStatus.CONFIRMED def test_confirm_empty_order_raises_error(self): customer = Customer("C1", "Alice", "a@test.com") order = Order("ORD-1", customer) with pytest.raises(ValueError, match="vazio"): order.confirm() def test_cancel_pending_order(self): customer = Customer("C1", "Alice", "a@test.com") order = Order("ORD-1", customer) order.cancel() assert order.status == OrderStatus.CANCELLED def test_cancel_shipped_order_raises_error(self): customer = Customer("C1", "Alice", "a@test.com") order = Order("ORD-1", customer) order.status = OrderStatus.SHIPPED with pytest.raises(ValueError, match="cancelar"): order.cancel() def test_calculate_total(self): customer = Customer("C1", "Alice", "a@test.com") order = Order("ORD-1", customer) order.add_item(OrderItem("P1", "Widget", 2, Decimal("10.00"))) order.add_item(OrderItem("P2", "Gadget", 1, Decimal("25.00"))) assert order.calculate_total() == Decimal("45.00") def test_add_item_to_confirmed_order_fails(self): customer = Customer("C1", "Alice", "a@test.com") order = Order("ORD-1", customer) order.add_item(OrderItem("P1", "Widget", 1, Decimal("10.00"))) order.confirm() with pytest.raises(ValueError, match="pendente"): order.add_item(OrderItem("P2", "Gadget", 1, Decimal("25.00")))
ℹ️Note

Os testes de entidade são puros, rápidos e não requerem configuração. Eles são a primeira linha de defesa contra bugs de lógica de negócio.

Bônus: Checklist para Projetar Entidades e Casos de Uso

Para Entidades:

  • A entidade contém apenas regras de negócio universais?
  • A entidade não importa nada de frameworks?
  • A entidade não tem métodos de persistência (save, delete)?
  • A entidade valida seu próprio estado?
  • Os métodos revelam intenção de negócio (confirm, cancel, ship)?

Para Casos de Uso:

  • O caso de uso tem uma única responsabilidade?
  • O caso de uso depende apenas de abstrações (interfaces/protocols)?
  • Os DTOs de entrada/saída são objetos simples (dataclasses)?
  • O caso de uso orquestra entidades sem conter lógica de entidade?
  • O caso de uso é testável sem infraestrutura real?
Success

Entidades bem projetadas e casos de uso focados são o coração de um sistema sustentável.

Progresso43%