Organização de código Java OOP: pacotes, coesão, acoplamento e princípios aplicados

Capítulo 12

Tempo estimado de leitura: 11 minutos

+ Exercício

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.java

Note 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Regras de dependência entre pacotes (contrato simples)

Defina e respeite um “mapa” de dependências permitido:

PacotePode depender deNão deve depender de
domainJava padrão (coleções, tempo se fizer sentido), outros pacotes de domínioinfra, IO, banco, HTTP, UI
servicedomain, service.portsimplementações em infra (classes concretas)
infradomain, service.ports, bibliotecas técnicasnada “acima” deve depender dela diretamente
apptudo (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 public apenas 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 domain e 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.ports e 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 public e 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 if por 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: domain não depende de infra? service não depende de classes concretas de infra?
  • API pública mínima: quantas classes public existem 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

  1. Crie os pacotes: domain, service, service.ports, infra, app.

  2. Modele o domínio mínimo em domain: crie tipos para representar a compra (ex.: CheckoutRequest ou Purchase) e item (SKU, quantidade). Evite colocar IO aqui.

  3. Extraia uma política de desconto (interface) em domain ou service (dependendo de onde você considera a regra). Crie uma implementação simples (ex.: desconto por quantidade) e injete no serviço.

  4. Crie uma porta de persistência em service.ports (ex.: CheckoutRepository com método save(...)).

  5. Implemente a porta em infra usando arquivo (FileCheckoutRepository), encapsulando FileWriter e tratamento de exceções.

  6. Crie o caso de uso em service (ex.: RunCheckoutService) que valida entrada, calcula total, aplica desconto e chama o repositório.

  7. Faça o wiring em app (ex.: Main), instanciando repositório e política e chamando o serviço.

  8. Reduza visibilidade: classes auxiliares (validadores, formatadores) devem ser package-private quando possível.

  9. Refatore para legibilidade: extraia métodos como validateRequest, calculateTotal, formatCsvLine (este último deve ficar em infra se for detalhe de persistência).

Critérios de aceitação

  • service não importa java.io nem escreve em arquivo diretamente.
  • domain nã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.

Agora responda o exercício sobre o conteúdo:

Ao reorganizar um projeto Java OOP para reduzir acoplamento e facilitar mudanças, qual estrutura de dependências melhor mantém o caso de uso (service) independente de detalhes técnicos?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao depender de interfaces (ports) em vez de classes concretas, o service não fica preso a IO/banco/HTTP. As implementações ficam em infra e o app faz o wiring, reduzindo acoplamento e facilitando troca de infraestrutura.

Próximo capitúlo

Tratamento de erros em Java OOP: exceções, invariantes e mensagens úteis

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
71%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.