Refatoração de sistema legado de pedidos de e-commerce aplicando encapsulamento real, objetos de domínio, Hide Delegate e redistribuição de responsabilidades conforme SRP
- Sobre o Projeto
- Bad Smells Identificados
- Métricas Antes e Depois
- Arquitetura Após Refatoração
- Estrutura de Pacotes
- Como Executar
- Testes
- Decisões Técnicas
- Documentação Completa
- Referências
Sistema legado responsável por gerar faturas e enviar e-mails de confirmação de pedidos. O código original concentrava dados do cliente, lógica de cálculo, formatação e chamada a serviço externo em uma única classe Order com atributos públicos e listas paralelas sem garantia de consistência.
A refatoração aplica sete técnicas de Fowler para eliminar os problemas estruturais sem alterar a lógica de negócio: o comportamento do sistema após a refatoração é idêntico ao original, verificado pelos testes automatizados.
| Bad Smell | Localização | Impacto |
|---|---|---|
| Atributos públicos sem controle | Order.clientName, clientEmail, discountRate |
Qualquer código externo altera o estado sem validação |
| Listas paralelas | products, quantities, prices em Order |
Nenhuma garantia de que os três índices permanecem sincronizados |
| Dados do cliente misturados com pedido | clientName e clientEmail em Order |
SRP violado — Order tem duas razões para mudar |
| Lógica de cálculo embutida em método de exibição | printInvoice() calcula e imprime ao mesmo tempo |
Impossível testar o cálculo sem efeito colateral de I/O |
| Acoplamento direto com infraestrutura | Order.sendEmail() chama EmailService diretamente |
Trocar o mecanismo de envio exige alterar a classe de domínio |
| Raw types sem segurança de tipo | List products, List quantities, List prices |
Erros de tipo são detectados apenas em runtime |
| Métrica | Antes | Depois |
|---|---|---|
| Classes de domínio | 1 (Order) |
4 (Client, Item, Order, DiscountPolicy) |
| Atributos públicos de domínio | 6 | 0 |
| Listas paralelas | 3 | 0 |
Responsabilidades em Order |
4 (dados, cálculo, formatação, notificação) | 1 (gerenciar itens e expor totais) |
Acoplamento Order → EmailService |
Direto | Eliminado |
| Testes automatizados | 0 | 17 (JUnit 5 + Jqwik) |
| Cobertura de domínio (JaCoCo) | — | > 90% |
App (main)
│
├── Order ──────────────── Client
│ │ (nome, e-mail encapsulados)
│ │
│ ├── List<Item>
│ │ (produto, qtd, preço, subtotal)
│ │
│ └── DiscountPolicy
│ (taxa, discountAmount, finalTotal)
│
├── InvoicePrinter ──────── Order
│ (formatação da fatura)
│
└── EmailNotifier ────────── EmailService (infrastructure)
(notificação de pedido)
Cada camada depende apenas das camadas internas. Order não conhece nem InvoicePrinter nem EmailNotifier. EmailNotifier é o único ponto de acesso ao EmailService de infraestrutura.
src/
└── main/java/com/andrebecker/ecommerce/
│ ├── App.java # entrada principal com avaliação de design
│ ├── original/
│ │ └── App.java # código legado preservado intacto
│ ├── domain/
│ │ ├── Client.java # identidade do comprador
│ │ ├── Item.java # produto, quantidade, preço e subtotal
│ │ ├── Order.java # agregação de itens e cálculo de totais
│ │ └── DiscountPolicy.java # política de desconto isolada
│ ├── service/
│ │ ├── InvoicePrinter.java # formatação e impressão da fatura
│ │ └── EmailNotifier.java # delegação ao serviço de e-mail
│ └── infrastructure/
│ └── EmailService.java # serviço externo (inalterado)
└── test/java/com/andrebecker/ecommerce/domain/
├── ItemTest.java # 7 testes determinísticos de Item
├── OrderTest.java # 7 testes determinísticos de Order
└── OrderPropertyTest.java # 3 propriedades Jqwik
Pré-requisitos: JDK 21+, Maven 3.9+
# Build completo + cobertura JaCoCo
mvn clean verify
# Apenas testes
mvn test
# Compilar sem testes
mvn clean compileScripts interativos:
# Windows
run.bat
# Linux / macOS
chmod +x run.sh && ./run.shRelatório JaCoCo gerado em target/site/jacoco/index.html.
17 testes no total — 0 falhas.
| Teste | Cobertura |
|---|---|
subtotalIsQuantityTimesPrice |
subtotal = qty × price |
subtotalWithUnitQuantityEqualsPrice |
qty = 1 → subtotal = price |
subtotalWithDecimalPrice |
precisão com valores decimais |
constructorRejectsZeroQuantity |
Fail-Fast: quantidade zero |
constructorRejectsNegativeQuantity |
Fail-Fast: quantidade negativa |
constructorRejectsNegativePrice |
Fail-Fast: preço negativo |
constructorRejectsBlankProduct |
Fail-Fast: produto em branco |
grossTotalSumsAllItemSubtotals |
total bruto com múltiplos itens |
discountAmountIsRateAppliedToGrossTotal |
desconto = total × taxa |
finalTotalIsGrossTotalMinusDiscount |
total final = subtotal − desconto |
finalTotalCoherentWithGrossTotalAndDiscountRate |
coerência total final × taxa |
itemsListIsImmutable |
coleção interna não é modificável externamente |
constructorRejectsNullClient |
Fail-Fast: cliente nulo |
constructorRejectsNullDiscountPolicy |
Fail-Fast: política nula |
| Propriedade | Invariante |
|---|---|
grossTotalEqualsSum |
totalBruto == Σ(qty × price) para qualquer conjunto de itens válidos |
finalTotalLessThanGrossTotalWhenDiscountPositive |
totalFinal < subtotal quando discountRate > 0 |
itemSubtotalNeverNegative |
subtotal de Item nunca negativo para valores válidos |
Exercício 1 e 2 — Encapsulamento e objetos de domínio
As três listas paralelas (products, quantities, prices) foram substituídas por List<Item>. Item encapsula os três valores com validação no construtor e calcula o próprio subtotal(). A eliminação das listas paralelas remove a possibilidade de inconsistência de índice, que era estruturalmente impossível de detectar pelo compilador.
Exercício 3 — Hide Delegate
Order.sendEmail() chamava EmailService diretamente, criando acoplamento entre domínio e infraestrutura. EmailNotifier encapsula essa chamada. Order não conhece mais EmailService — trocar o canal de envio não exige nenhuma alteração nas classes de domínio.
Exercício 4 — Move Method e SRP
printInvoice() calculava e formatava no mesmo método, misturando cálculo (responsabilidade de Order) com apresentação (responsabilidade de InvoicePrinter). Os métodos foram redistribuídos: grossTotal(), discountAmount() e finalTotal() pertencem a Order e DiscountPolicy; InvoicePrinter apenas formata.
Exercício 5 — Extract Class (Client)
clientName e clientEmail eram atributos de Order, violando SRP. Client encapsula esses dados com validação própria. Order passa a receber um Client, tornando o modelo mais expressivo e eliminando strings soltas no construtor do pedido.
Exercício 6 — Extract Method
printInvoice() original iterava, calculava e imprimia em sequência linear. InvoicePrinter decompõe em printHeader(), printLineItems(), printLineItem() e printTotals() — cada método com intenção única e nome descritivo.
Exercício 7 — Fail-Fast e código morto
Validações no construtor de todas as classes de domínio garantem que objetos inválidos não chegam ao domínio. O padrão DiscountPolicy.calculateDiscount() estático foi convertido em métodos de instância com estado real (rate), eliminando o anti-padrão de classe utilitária sem identidade.
Composição sobre herança
Todas as classes são final. Não há hierarquia de herança, o que elimina qualquer possibilidade de violação de LSP. A extensibilidade é obtida por substituição de DiscountPolicy ou EmailNotifier sem alterar as classes que os recebem.
Análise detalhada de cada refatoração, trechos antes/depois e justificativa técnica:
- MARTIN, Robert C. Clean Code: A Handbook of Agile Software Craftsmanship. 2. ed. Prentice Hall, 2008.
- FOWLER, Martin. Refactoring: Improving the Design of Existing Code. 2. ed. Addison-Wesley, 2018.
- BECK, Kent. Test Driven Development: By Example. Addison-Wesley, 2002.
- JUnit 5 User Guide
- Jqwik Documentation
- JaCoCo Documentation