Por que organização importa: coesão, acoplamento e custo de mudança
Organizar um projeto Java orientado a objetos é reduzir o custo de entender, testar e alterar o código. Dois conceitos guiam decisões de estrutura:
- Coesão: o quanto as partes de um pacote/classe/módulo “andam juntas” por um mesmo motivo. Alta coesão significa que mudanças relacionadas tendem a acontecer no mesmo lugar.
- Acoplamento: o quanto um pacote/classe depende de detalhes de outro. Baixo acoplamento significa que você consegue mudar uma parte sem quebrar várias outras.
Uma regra prática: aumente coesão agrupando por responsabilidade e reduza acoplamento evitando dependências desnecessárias (principalmente de infraestrutura e detalhes de persistência/IO).
Estrutura de pacotes por responsabilidade (sem frameworks)
Uma estrutura simples e escalável é separar por camadas de responsabilidade, sem “pastas genéricas” (como util) virarem depósito de código.
Proposta de pacotes
app: ponto de entrada, composição (wiring) manual de dependências, configuração simples.domain: regras e tipos do negócio (entidades, value objects, políticas). Deve depender do mínimo possível.service: casos de uso (orquestração), coordena domínio e portas (interfaces) para fora.infra: detalhes técnicos (repositórios em arquivo/SQL, HTTP, relógio do sistema, UUID, etc.).
Quando necessário, crie subpacotes por contexto: domain.order, service.order, infra.persistence, etc. Evite subpacotes profundos demais sem necessidade.
Exemplo de árvore de projeto
src/main/java/com/loja/ app/ Main.java Wiring.java domain/ money/ Money.java order/ Order.java OrderItem.java OrderId.java DiscountPolicy.java customer/ CustomerId.java service/ order/ PlaceOrderService.java CalculateTotalService.java ports/ OrderRepository.java PaymentGateway.java Clock.java infra/ persistence/ InMemoryOrderRepository.java FileOrderRepository.java payment/ FakePaymentGateway.java time/ SystemClock.javaNote que service/ports (ou service.port) concentra interfaces que representam dependências externas. Implementações ficam em infra. Isso reduz acoplamento do código de casos de uso com detalhes técnicos.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Regras de dependência entre pacotes (contrato simples)
Defina e respeite um “mapa” de dependências permitido:
| Pacote | Pode depender de | Não deve depender de |
|---|---|---|
domain | Java padrão (coleções, tempo se fizer sentido), outros pacotes de domínio | infra, IO, banco, HTTP, UI |
service | domain, service.ports | implementações em infra (classes concretas) |
infra | domain, service.ports, bibliotecas técnicas | nada “acima” deve depender dela diretamente |
app | tudo (para compor) | evitar lógica de negócio |
Essa disciplina evita o problema clássico: domínio chamando banco de dados, serviço instanciando cliente HTTP concreto, ou regras de negócio espalhadas em classes de infraestrutura.
Limitar visibilidade: API pública mínima e encapsulamento de pacote
Além de private/public, Java tem um recurso valioso para organização: visibilidade de pacote (sem modificador). Use para esconder detalhes internos dentro do mesmo pacote.
Diretrizes práticas
- Deixe
publicapenas o que é consumido por outros pacotes. O resto pode ser package-private. - Construtores e fábricas: se um tipo só deve ser criado por um serviço/fábrica no mesmo pacote, torne o construtor package-private.
- Evite getters “por padrão” em objetos de domínio. Exponha operações significativas; dados crus aumentam acoplamento.
Exemplo: esconder detalhes dentro do pacote
package com.loja.service.order; class OrderValidator { // package-private: só o pacote service.order usa void validate(PlaceOrderCommand cmd) { // regras de validação do caso de uso } } public class PlaceOrderService { private final OrderValidator validator = new OrderValidator(); // ... }Isso reduz a superfície pública e evita que outros pacotes passem a depender de classes “auxiliares” por conveniência.
Princípios aplicados na organização (sem repetir teoria)
SRP na prática: um motivo para mudar
Use SRP como critério de reorganização: se uma classe/pacote muda por motivos diferentes (ex.: regra de desconto e persistência), separe.
- Sinal de alerta: classe com métodos de negócio + métodos de IO/log/persistência.
- Ação: extraia portas (interfaces) e mova implementações para
infra.
DRY contextual: evite duplicação “boa” virar acoplamento “ruim”
Nem toda repetição deve virar abstração. DRY é sobre duplicação de conhecimento, não sobre repetir 3 linhas iguais.
- Unifique quando a regra é a mesma e muda junto (ex.: cálculo de taxa).
- Não unifique quando a semelhança é acidental e pode divergir (ex.: validações parecidas em contextos diferentes).
Uma abstração prematura cria acoplamento: dois fluxos passam a depender do mesmo método “genérico” e qualquer mudança vira negociação.
Preferência por composição na estrutura do projeto
Na organização do código, “preferir composição” aparece como: montar casos de uso com colaboradores pequenos (validadores, políticas, repositórios via interface), em vez de heranças profundas ou classes “Deus”.
- Serviços de caso de uso pequenos que coordenam objetos e portas.
- Políticas (ex.: desconto) como dependências substituíveis.
- Infra como plug-in: trocável sem reescrever o serviço.
Passo a passo: reorganizando um projeto existente
Use este roteiro quando o projeto já existe e está “misturado”.
Passo 1 — Identifique o “núcleo” (domínio) e isole
- Liste tipos que representam conceitos do negócio (ex.: Pedido, Item, Dinheiro, Identificadores).
- Mova para
domaine remova dependências de IO, banco, HTTP. - Se o domínio precisa de algo externo (ex.: relógio), represente como abstração (interface) fora do domínio ou como valor passado por parâmetro.
Passo 2 — Separe casos de uso (service) de detalhes técnicos (infra)
- Encontre classes que “fazem acontecer” (ex.: finalizar pedido, registrar pagamento).
- Mova para
service. - Para cada dependência técnica, crie uma interface em
service.portse injete no serviço.
Passo 3 — Crie implementações em infra e faça o wiring em app
- Implemente as interfaces em
infra(ex.: repositório em memória/arquivo). - No
app, instancie implementações e injete nos serviços.
Passo 4 — Reduza visibilidade e estabilize a API
- Revise classes
publice torne package-private quando possível. - Garanta que pacotes exponham poucos pontos de entrada (ex.: serviços e tipos de domínio).
Passo 5 — Refatore para legibilidade (técnicas orientadas a leitura)
- Rename: nomes que explicam intenção (variáveis, métodos, pacotes).
- Extract Method: quebre métodos longos em passos nomeados.
- Extract Class: se uma classe tem duas responsabilidades, separe.
- Move Method/Field: mova comportamento para onde estão os dados que ele usa (aumenta coesão).
- Replace Conditional with Polymorphism (quando fizer sentido): substitua
ifpor estratégia/política.
Exemplo completo: antes e depois (sem frameworks)
Antes: classe “faz tudo” e mistura camadas
package com.loja; import java.io.*; import java.time.*; import java.util.*; public class OrderManager { public double placeOrder(String customerId, List<String> items) throws Exception { // validações if (customerId == null || customerId.isBlank()) throw new Exception("invalid"); if (items == null || items.isEmpty()) throw new Exception("empty"); // regra de negócio + persistência em arquivo + data do sistema double total = 0; for (String item : items) total += 10.0; if (items.size() >= 3) total = total * 0.9; String line = customerId + ";" + total + ";" + LocalDateTime.now(); try (FileWriter fw = new FileWriter("orders.txt", true)) { fw.write(line + System.lineSeparator()); } System.out.println("ORDER PLACED: " + line); return total; } }Depois: pacotes separados, portas e infra plugável
package com.loja.domain.order; import java.util.*; public class Order { private final OrderId id; private final List<OrderItem> items; public Order(OrderId id, List<OrderItem> items) { this.id = Objects.requireNonNull(id); this.items = List.copyOf(items); } public int itemCount() { return items.size(); } public List<OrderItem> items() { return items; } public OrderId id() { return id; } }package com.loja.domain.order; public record OrderItem(String sku, int quantity, int unitPriceCents) { public OrderItem { if (sku == null || sku.isBlank()) throw new IllegalArgumentException("sku"); if (quantity <= 0) throw new IllegalArgumentException("quantity"); if (unitPriceCents < 0) throw new IllegalArgumentException("unitPriceCents"); } }package com.loja.domain.order; public interface DiscountPolicy { int applyDiscountCents(int totalCents, int itemCount); }package com.loja.service.ports; import com.loja.domain.order.*; public interface OrderRepository { void save(Order order, int totalCents); }package com.loja.service.order; import java.util.*; import com.loja.domain.order.*; import com.loja.service.ports.*; public class PlaceOrderService { private final OrderRepository repository; private final DiscountPolicy discountPolicy; public PlaceOrderService(OrderRepository repository, DiscountPolicy discountPolicy) { this.repository = Objects.requireNonNull(repository); this.discountPolicy = Objects.requireNonNull(discountPolicy); } public int placeOrder(Order order) { int total = order.items().stream() .mapToInt(i -> i.unitPriceCents() * i.quantity()) .sum(); int discounted = discountPolicy.applyDiscountCents(total, order.itemCount()); repository.save(order, discounted); return discounted; } }package com.loja.infra.persistence; import java.io.*; import com.loja.domain.order.*; import com.loja.service.ports.*; public class FileOrderRepository implements OrderRepository { private final File file; public FileOrderRepository(File file) { this.file = file; } @Override public void save(Order order, int totalCents) { try (FileWriter fw = new FileWriter(file, true)) { fw.write(order.id().value() + ";" + totalCents + System.lineSeparator()); } catch (IOException e) { throw new UncheckedIOException(e); } } }package com.loja.app; import java.io.File; import java.util.List; import com.loja.domain.order.*; import com.loja.infra.persistence.*; import com.loja.service.order.*; public class Main { public static void main(String[] args) { var repo = new FileOrderRepository(new File("orders.txt")); DiscountPolicy discount = (total, itemCount) -> itemCount >= 3 ? (int)(total * 0.9) : total; var service = new PlaceOrderService(repo, discount); var order = new Order(new OrderId("O-1"), List.of(new OrderItem("SKU1", 1, 1000))); int totalCents = service.placeOrder(order); System.out.println(totalCents); } }O ganho estrutural: o caso de uso não sabe que existe arquivo; o domínio não sabe que existe persistência; a infraestrutura pode ser trocada (arquivo, memória, SQL) sem reescrever o serviço.
Checklist de revisão (coesão, acoplamento e legibilidade)
- Pacotes por responsabilidade: existe separação clara entre
domain,service,infra,app? - Dependências permitidas:
domainnão depende deinfra?servicenão depende de classes concretas deinfra? - API pública mínima: quantas classes
publicexistem por pacote? Dá para reduzir com package-private? - Coesão de pacote: arquivos no mesmo pacote mudam pelos mesmos motivos? Se não, separar.
- Coesão de classe: a classe tem um foco claro? Se mistura validação, regra e IO, separar.
- Acoplamento: há imports desnecessários (ex.: serviço importando
java.io)? - DRY contextual: abstrações foram criadas por regra comum real ou por semelhança superficial?
- Preferência por composição: políticas e dependências externas são injetadas (interfaces) em vez de instanciadas dentro?
- Legibilidade: métodos longos foram quebrados em passos nomeados? Nomes refletem intenção?
- Testabilidade: casos de uso podem ser exercitados com implementações em memória/fakes sem tocar IO?
Exercício: reorganização de um código propositalmente bagunçado
Objetivo: aplicar a estrutura por responsabilidade, reduzir acoplamento e melhorar legibilidade sem adicionar frameworks.
Código inicial (bagunçado)
package com.bagunca; import java.io.*; import java.time.*; import java.util.*; public class Checkout { public static double run(String user, String sku, int qty) { if (user == null || user.isBlank()) throw new RuntimeException("u"); if (sku == null || sku.isBlank()) throw new RuntimeException("s"); if (qty <= 0) throw new RuntimeException("q"); double price = 19.90; double total = price * qty; if (qty > 5) total = total * 0.85; String payload = user + "," + sku + "," + qty + "," + total + "," + LocalDateTime.now(); try (FileWriter fw = new FileWriter("checkout.csv", true)) { fw.write(payload + System.lineSeparator()); } catch (IOException e) { throw new RuntimeException(e); } System.out.println("OK " + payload); return total; } }Tarefas
Crie os pacotes:
domain,service,service.ports,infra,app.Modele o domínio mínimo em
domain: crie tipos para representar a compra (ex.:CheckoutRequestouPurchase) e item (SKU, quantidade). Evite colocar IO aqui.Extraia uma política de desconto (interface) em
domainouservice(dependendo de onde você considera a regra). Crie uma implementação simples (ex.: desconto por quantidade) e injete no serviço.Crie uma porta de persistência em
service.ports(ex.:CheckoutRepositorycom métodosave(...)).Implemente a porta em
infrausando arquivo (FileCheckoutRepository), encapsulandoFileWritere tratamento de exceções.Crie o caso de uso em
service(ex.:RunCheckoutService) que valida entrada, calcula total, aplica desconto e chama o repositório.Faça o wiring em
app(ex.:Main), instanciando repositório e política e chamando o serviço.Reduza visibilidade: classes auxiliares (validadores, formatadores) devem ser package-private quando possível.
Refatore para legibilidade: extraia métodos como
validateRequest,calculateTotal,formatCsvLine(este último deve ficar eminfrase for detalhe de persistência).
Critérios de aceitação
servicenão importajava.ionem escreve em arquivo diretamente.domainnão conhece CSV, arquivo ou console.- O cálculo de desconto é substituível sem alterar o caso de uso (injeção por interface).
- O número de classes
publicé o mínimo necessário para uso entre pacotes.