Pular para o conteúdo

Exercícios Práticos

  1. Resolva no editor, com código compilando — ler a solução não treina o músculo.
  2. Só depois abra o gabarito comentado (callout recolhido).
  3. Compare decisões, não sintaxe: o valor está no porquê de cada corte.

Exercício 1.1 — Três atores e uma cadeia de ifs

Seção intitulada “Exercício 1.1 — Três atores e uma cadeia de ifs”

O serviço abaixo fecha faturas. Identifique todas as violações de SOLID e refatore.

@Service
public class FechamentoFaturaService {
private final JdbcTemplate jdbcTemplate;
private final JavaMailSender mailSender;
public FechamentoFaturaService(JdbcTemplate jdbcTemplate, JavaMailSender mailSender) {
this.jdbcTemplate = jdbcTemplate;
this.mailSender = mailSender;
}
public void fechar(String faturaId, String tipoCliente) {
BigDecimal total = jdbcTemplate.queryForObject(
"SELECT SUM(valor) FROM item_fatura WHERE fatura_id = ?", BigDecimal.class, faturaId);
BigDecimal desconto;
if (tipoCliente.equals("VAREJO")) {
desconto = total.multiply(new BigDecimal("0.05"));
} else if (tipoCliente.equals("ATACADO")) {
desconto = total.multiply(new BigDecimal("0.12"));
} else if (tipoCliente.equals("PARCEIRO")) {
desconto = total.multiply(new BigDecimal("0.20"));
} else {
desconto = BigDecimal.ZERO;
}
jdbcTemplate.update("UPDATE fatura SET total = ?, desconto = ? WHERE id = ?",
total, desconto, faturaId);
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo(buscarEmail(faturaId));
msg.setText("Fatura fechada: " + total.subtract(desconto));
mailSender.send(msg);
}
private String buscarEmail(String faturaId) {
return jdbcTemplate.queryForObject(
"SELECT email FROM cliente c JOIN fatura f ON f.cliente_id = c.id WHERE f.id = ?",
String.class, faturaId);
}
}

Perguntas-guia: quantos atores mudam esta classe? O que acontece quando surgir o tipo de cliente “GOVERNO”? Como você testaria a regra de desconto sem banco e sem SMTP?

Gabarito comentado
  • SRP: três atores — financeiro (desconto), persistência (SQLs), comunicação (e-mail). No mínimo três peças: uma política de desconto, um repositório, um notificador.
  • OCP: a cadeia if/else sobre tipoCliente cresce a cada tipo novo. Extraia uma PoliticaDesconto com implementações por tipo, injetadas como List pelo Spring (o mesmo desenho do exemplo de OCP - Open-Closed Principle).
  • DIP: a regra depende de JdbcTemplate e JavaMailSender concretos. Inverta: interfaces FaturaRepository e NotificadorFatura definidas junto à regra, implementações na infraestrutura.
  • Resultado esperado: um FecharFaturaService fino orquestrando PoliticaDesconto, FaturaRepository e NotificadorFatura — e a regra de desconto testável com um construtor e um assert.

Exercício 1.2 — A subclasse que não sustenta o contrato

Seção intitulada “Exercício 1.2 — A subclasse que não sustenta o contrato”

Um agendador percorre List<Relatorio> chamando os dois métodos de exportação e agora explode em produção:

public abstract class Relatorio {
public abstract byte[] exportarPdf();
public abstract byte[] exportarExcel();
}
public class RelatorioGerencial extends Relatorio {
@Override
public byte[] exportarPdf() {
return new byte[] {1};
}
@Override
public byte[] exportarExcel() {
return new byte[] {2};
}
}
public class RelatorioAuditoria extends Relatorio {
@Override
public byte[] exportarPdf() {
return new byte[] {3};
}
@Override
public byte[] exportarExcel() {
throw new UnsupportedOperationException("Auditoria so sai em PDF");
}
}

Diagnostique pelo vocabulário de LSP e ISP e proponha a estrutura correta.

Gabarito comentado

A exceção em override é o cheiro duplo: RelatorioAuditoria não é substituível (LSP) porque foi forçado a implementar um contrato gordo (ISP). Segregue por papel: interfaces ExportavelPdf e ExportavelExcel separadas. O agendador de Excel passa a depender de List<ExportavelExcel> e o problema desaparece em compilação — quem não exporta Excel simplesmente não entra na lista.

Sem código no primeiro momento: papel, caneta ou Excalidraw.

Regras do negócio:

  1. Um título tem vários exemplares; empresta-se o exemplar, não o título.
  2. Empréstimo dura 14 dias; devolução atrasada gera multa de R$ 1,00 por dia.
  3. Um leitor pode ter no máximo 3 empréstimos ativos.
  4. Leitor com multa pendente não pode fazer novo empréstimo.
  5. Se todos os exemplares de um título estão emprestados, o leitor pode reservar; quando um exemplar volta, a reserva mais antiga é notificada.

Tarefas:

  1. Liste Entidades e Value Objects e escolha as raízes de Agregados com suas fronteiras.
  2. Diga onde vive cada invariante (regras 1 a 4) e por quê.
  3. Identifique os Domain Events e quem reage a eles.
  4. Marque o que viraria outro Bounded Context se o sistema crescesse.
Gabarito comentado (uma modelagem defensável — não a única)
  • VOs: Prazo, Multa (valor + dias de atraso), Isbn. Entidades: Leitor, Exemplar, Emprestimo, Reserva, Titulo.
  • Agregados: Emprestimo como raiz própria, referenciando LeitorId e ExemplarId por identidade — a invariante 2 (prazo/multa) vive nele. Leitor como raiz que guarda contadores e multas pendentes — invariantes 3 e 4 vivem nele.
  • A tensão didática: as invariantes 3 e 4 cruzam Leitor × Emprestimo. Se exigirem consistência imediata, criar empréstimo deve passar pelo agregado Leitor (ex.: leitor.emprestar(...) fabrica o Emprestimo); se tolerarem uma corrida rara, valida-se por consulta e compensa-se depois — exatamente a discussão de Agregados e Domain Events.
  • Eventos: EmprestimoRealizado, ExemplarDevolvido (dispara cálculo de multa e atendimento de Reserva), ReservaDisponivel (a notificação em si é um adapter).
  • Contextos futuros: Catálogo (título, ISBN, dados bibliográficos) separado de Circulação (empréstimos e reservas); Cobrança de multas seria candidato a um terceiro.

O controller abaixo funciona — e concentra tudo:

@RestController
@RequestMapping("/emprestimos")
public class EmprestimoController {
private final JdbcTemplate jdbcTemplate;
public EmprestimoController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostMapping
public ResponseEntity<String> emprestar(@RequestBody Map<String, String> body) {
Integer ativos = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM emprestimo WHERE leitor_id = ? AND devolvido = false",
Integer.class, body.get("leitorId"));
if (ativos >= 3) {
return ResponseEntity.unprocessableEntity().body("Limite de emprestimos atingido");
}
jdbcTemplate.update(
"INSERT INTO emprestimo (leitor_id, exemplar_id, prazo) VALUES (?, ?, current_date + 14)",
body.get("leitorId"), body.get("exemplarId"));
return ResponseEntity.ok("Emprestimo registrado");
}
}

Tarefas:

  1. Desenhe as Ports deste caso de uso (driving e driven).
  2. Reestruture em domain / application / infrastructure, movendo a regra “máximo 3” para o domínio.
  3. Escreva o teste do caso de uso com um fake em memória, sem Spring (molde em Testando o Núcleo).
  4. Aponte onde a tradução do erro para 422 deve acontecer.
Gabarito comentado
  • Driving port EmprestarExemplarUseCase (comando com leitorId/exemplarId); driven port LeitorRepository ou EmprestimoRepository, conforme sua modelagem no Bloco 2 — interfaces no núcleo, na linguagem do domínio.
  • A regra “máximo 3” vira invariante de agregado (ex.: leitor.emprestar(...) lança LimiteDeEmprestimosExcedido), não um if de controller com SQL de contagem.
  • O teste instancia o application service com o fake e cobre dois cenários em testes separados: limite atingido e limite livre.
  • O 422 nasce num @RestControllerAdvice (adapter primário) traduzindo LimiteDeEmprestimosExcedido — o núcleo nunca menciona HTTP (ver a seção de tradução de erros em Exemplo Hexagonal com Spring).

Bloco 4 — Patterns & Refactoring: do smell ao padrão

Seção intitulada “Bloco 4 — Patterns & Refactoring: do smell ao padrão”

O cálculo de frete abaixo cresceu por acúmulo:

public BigDecimal calcularFrete(Pedido pedido, String transportadora,
boolean seguro, boolean embalagemPresente) {
BigDecimal base;
if (transportadora.equals("CORREIOS")) {
base = pedido.pesoTotal().multiply(new BigDecimal("1.20"));
} else if (transportadora.equals("TRANSPORTADORA_X")) {
base = pedido.pesoTotal().multiply(new BigDecimal("0.95")).add(new BigDecimal("12"));
} else if (transportadora.equals("RETIRADA")) {
base = BigDecimal.ZERO;
} else {
throw new IllegalArgumentException(transportadora);
}
if (seguro) {
base = base.add(pedido.valorTotal().multiply(new BigDecimal("0.01")));
}
if (embalagemPresente) {
base = base.add(new BigDecimal("5"));
}
return base;
}

Tarefas:

  1. Nomeie os smells e os princípios violados (Code Smells e SOLID).
  2. Separe os dois eixos de variação e diga qual padrão cabe em cada um.
  3. Refatore com passos pequenos (Técnicas de Refactoring), testes verdes a cada passo.
Gabarito comentado
  • O if/else sobre transportadora é Repeated SwitchesOCPReplace Conditional with Polymorphism, chegando em Strategy (uma PoliticaFrete por transportadora, injetadas como List — o mesmo desenho do exemplo de OCP).
  • Os flags seguro/embalagemPresente são responsabilidades cumulativas e combináveis — caso de Decorator sobre a interface de frete (new ComSeguro(new ComEmbalagem(politica))), não de mais estratégias: estratégias são alternativas, decorators se empilham.
  • Flags booleanas na assinatura eram o cheiro de que dois conceitos (escolha do transporte × extras) estavam fundidos numa função só.

Bloco 5 — Sistemas Distribuídos: pedido em três serviços

Seção intitulada “Bloco 5 — Sistemas Distribuídos: pedido em três serviços”

Pedido, Pagamento e Estoque são serviços com bancos próprios. O fluxo de negócio: criar pedido → reservar estoque → cobrar → confirmar.

Tarefas:

  1. Desenhe a SAGA orquestrada: passos, replies e as compensações de cada falha possível (inclusive falha depois da cobrança).
  2. Aplique Message Outbox: em quais dos três serviços a tabela de outbox é necessária e por quê?
  3. Especifique a idempotência do consumidor de Pagamento: o que é a chave da mensagem, o que é o id de deduplicação (Event-Driven Architecture)?
  4. Em qual borda entra o API Gateway e o que ele não deve fazer nesse fluxo?
Gabarito comentado
  • Compensações em ordem inversa: falha na cobrança → liberar estoque → cancelar pedido; falha após a cobrança (confirmação rejeitada) → estornar pagamento → liberar estoque — os estados COMPENSATING_* do exemplo de SAGA.
  • Outbox nos três: todo serviço que muda estado local e publica reply/evento tem o problema do dual write.
  • Chave = id do pedido (ordem por agregado na partição); deduplicação = eventId do payload registrado em tabela de processados na mesma transação do efeito.
  • O gateway roteia, autentica e aplica rate limit na entrada; ele não orquestra a saga nem contém regra de negócio — isso é o orquestrador, dentro do contexto dono do fluxo.

Bloco 6 — Frontend: extraia a porta do componente

Seção intitulada “Bloco 6 — Frontend: extraia a porta do componente”

Um componente React faz fetch, calcula elegibilidade de desconto e renderiza — tudo no corpo. Tarefas:

  1. Separe domínio (regra pura), porta (ClienteRepository), adapter HTTP e hook de caso de uso, no desenho de Arquitetura no Frontend.
  2. Escreva dois testes: o hook com fake da porta (sem rede) e o componente integrado com MSW (Testes e TDD no Frontend).
  3. Justifique quando cada um dos dois testes é o certo.
Gabarito comentado
  • A regra de elegibilidade vira função pura em domain/ (testável com assert direto); o fetch vira adapter da porta; o componente só renderiza o que o hook entrega.
  • Fake via porta: rápido, sem rede — ideal para variações da regra. MSW: exercita o adapter HTTP real e a integração — ideal para o caminho feliz e erros de rede. São seams complementares, não concorrentes: regra no fake, fronteira no MSW.

Para um serviço Spring Boot com testes de integração via Testcontainers:

  1. Escreva o workflow de CI (CI - GitHub Actions): etapas, cache, o que roda em cada job.
  2. Onde os testes de Testcontainers rodam e qual pré-requisito o runner precisa ter?
  3. Que permissão o GITHUB_TOKEN precisa para push no ghcr.io?
  4. O pipeline termina no push da imagem. Liste o que falta para virar CD e onde mora o risco de cada item.
Gabarito comentado
  • Dois jobs: build-and-test (checkout, JDK com cache, ./mvnw verify) e build-image com needs: — imagem só nasce de suíte verde.
  • Testcontainers roda no job de testes e exige Docker disponível no runner (os runners ubuntu-latest já o têm).
  • permissions: packages: write — o token é read-only por padrão em repositórios novos.
  • Para CD: promoção da imagem por ambiente, migrações de banco versionadas, estratégia de deploy (rolling/blue-green), rollback automatizável e observabilidade para saber se o deploy deu certo — cada um é um modo de falha que o pipeline atual não cobre.

Especificação mínima:

  1. Assinante escolhe um plano (Básico: 1 livro/mês; Premium: 3 livros/mês) com cobrança mensal.
  2. Upgrade e downgrade valem a partir do próximo ciclo; upgrade no meio do ciclo libera os livros extras imediatamente.
  3. Assinatura pode ser pausada por até 2 meses por ano; pausada, não cobra nem envia.
  4. Três falhas de cobrança consecutivas suspendem a assinatura e publicam um fato para o time de retenção.
  5. A seleção do mês não pode repetir livro já enviado àquele assinante.

Entregáveis:

  1. Context map — quais contextos (Assinaturas, Cobrança, Envio, Catálogo?) e os padrões de integração entre eles (Design Estratégico e Tático).
  2. Modelo tático do contexto de Assinaturas: agregados, VOs, invariantes e eventos.
  3. Um caso de uso ponta a ponta no estilo de Exemplo Hexagonal com Spring ou Exemplo Clean com Spring.
  4. Testes: domínio puro + caso de uso com fakes + uma regra ArchUnit (Testando o Núcleo).

Checklist de autoavaliação:

  • Alguma classe do domínio importa Spring ou JPA? (deveria ser zero)
  • Toda invariante tem um teste que a exercita por cenário de negócio?
  • Os eventos têm nome no passado e payload mínimo?
  • Dá para trocar o banco escrevendo apenas um adapter novo?
  • O caso de uso se lê como a frase que o especialista falaria?