É difícil aceitar a verdade. Que sua aplicação Java com mais de 2000 linhas está toda escrita em uma única classe... mas você já sabe disso, ou pelo menos espero que saiba. Eu sei que você já tentou torná-la mais compacta, mas as coisas simplesmente não estão fazendo sentido.
É aqui que entra um princípio que transcende tempo e espaço, que nos ajuda em todos os aspectos da nossa vida: Separation of Concerns (Separação de Responsabilidades) ou SoC.
Eu sei que pode parecer intimidante, mas não se preocupe - o Tio T está aqui e vou tornar tudo o mais simples possível para o seu prazer.
Então hoje vamos passar por estes pontos:
- O que é Separation of Concerns?
- Coesão e Acoplamento
- Por que funciona?
- Exemplos práticos em Java
- Conclusão
Antes de começarmos, fique confortável, pegue sua bebida favorita e coloque seus óculos inteligentes. Vamos começar!
O que é Separation of Concerns?
Como todo bom post técnico precisa de uma citação, aqui está algo que copiei da Wikipedia:
"Em ciência da computação, separation of concerns é um princípio de design para separar um programa de computador em seções distintas, de modo que cada seção aborde uma preocupação separada."
Ok, agora que as formalidades acabaram, deixe-me explicar de forma simples.
Separation of Concerns é um princípio universal que praticamente todas as pessoas aplicam. Não estou falando apenas de desenvolvimento de software - ele se aplica a muitos outros campos também.
Um Exemplo do Mundo Real
Imagine sua cozinha. Você não guarda talheres no mesmo lugar que produtos de limpeza, certo? Você tem gavetas separadas para:
- Talheres
- Panelas
- Temperos
- Produtos de limpeza
Por quê? Porque cada categoria tem uma responsabilidade diferente, e misturá-las tornaria sua vida um caos.
Como SoC se Aplica ao Java?
Em Java, aplicamos SoC em vários níveis:
1. Nível de Arquitetura - Padrão MVC
// Model - Responsável pelos dados
public class Usuario {
private Long id;
private String nome;
private String email;
// getters e setters
}
// View - Responsável pela apresentação (JSP exemplo)
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<body>
<h1>Bem-vindo, ${usuario.nome}!</h1>
</body>
</html>
// Controller - Responsável pela lógica de controle
@RestController
@RequestMapping("/usuarios")
public class UsuarioController {
@Autowired
private UsuarioService service;
@GetMapping("/{id}")
public Usuario buscarUsuario(@PathVariable Long id) {
return service.buscarPorId(id);
}
}
2. Nível de Camadas - Arquitetura em Camadas
// Camada de Apresentação
@RestController
public class PedidoController {
@Autowired
private PedidoService pedidoService;
@PostMapping("/pedidos")
public ResponseEntity<Pedido> criarPedido(@RequestBody PedidoDTO dto) {
Pedido pedido = pedidoService.processar(dto);
return ResponseEntity.ok(pedido);
}
}
// Camada de Negócio
@Service
public class PedidoService {
@Autowired
private PedidoRepository repository;
@Autowired
private EmailService emailService;
public Pedido processar(PedidoDTO dto) {
// Lógica de negócio
Pedido pedido = new Pedido(dto);
pedido.calcularTotal();
pedido = repository.save(pedido);
emailService.enviarConfirmacao(pedido);
return pedido;
}
}
// Camada de Dados
@Repository
public interface PedidoRepository extends JpaRepository<Pedido, Long> {
List<Pedido> findByClienteId(Long clienteId);
}
Coesão e Acoplamento
Dois conceitos fundamentais em SoC:
Alta Coesão (BOM ✅)
Elementos relacionados ficam juntos.
// ALTA COESÃO - Todos os métodos são sobre gerenciamento de conta
public class ContaBancaria {
private BigDecimal saldo;
private String numero;
public void depositar(BigDecimal valor) {
this.saldo = this.saldo.add(valor);
}
public void sacar(BigDecimal valor) {
if (saldo.compareTo(valor) >= 0) {
this.saldo = this.saldo.subtract(valor);
}
}
public BigDecimal consultarSaldo() {
return this.saldo;
}
}
Baixo Acoplamento (BOM ✅)
Módulos dependem minimamente uns dos outros.
// BAIXO ACOPLAMENTO - Usando interfaces
public interface NotificadorService {
void enviar(String mensagem, String destinatario);
}
@Service
public class EmailNotificador implements NotificadorService {
public void enviar(String mensagem, String destinatario) {
// Implementação de email
}
}
@Service
public class SMSNotificador implements NotificadorService {
public void enviar(String mensagem, String destinatario) {
// Implementação de SMS
}
}
// A classe não está acoplada a uma implementação específica
@Service
public class PedidoService {
@Autowired
private NotificadorService notificador; // Interface, não implementação!
public void finalizarPedido(Pedido pedido) {
// processar pedido...
notificador.enviar("Pedido confirmado!", pedido.getCliente().getEmail());
}
}
Por Que SoC Funciona?
1. Manutenibilidade 🔧
Quando você precisa fazer uma mudança, você sabe exatamente onde ir:
// Antes de SoC - Tudo misturado 😱
public class SistemaPedido {
public void processarPedido(String item, int quantidade, String email) {
// Validação
if (item == null || quantidade <= 0) {
throw new IllegalArgumentException("Dados inválidos");
}
// Cálculo de preço
double preco = quantidade * 10.0;
if (quantidade > 100) {
preco = preco * 0.9; // desconto
}
// Salvar no banco
Connection conn = DriverManager.getConnection("jdbc:...");
PreparedStatement stmt = conn.prepareStatement("INSERT INTO pedidos...");
// ...
// Enviar email
Properties props = new Properties();
Session session = Session.getInstance(props);
MimeMessage message = new MimeMessage(session);
// ...
}
}
// Depois de SoC - Tudo organizado 😊
@Service
public class PedidoService {
@Autowired private ValidadorPedido validador;
@Autowired private CalculadoraPreco calculadora;
@Autowired private PedidoRepository repository;
@Autowired private EmailService emailService;
public void processarPedido(PedidoDTO dto) {
validador.validar(dto);
BigDecimal preco = calculadora.calcular(dto);
Pedido pedido = new Pedido(dto, preco);
repository.save(pedido);
emailService.enviarConfirmacao(pedido);
}
}
2. Testabilidade 🧪
Com SoC, testar fica muito mais fácil:
@Test
public void testCalculadoraPreco() {
CalculadoraPreco calc = new CalculadoraPreco();
PedidoDTO dto = new PedidoDTO("Item", 150);
BigDecimal preco = calc.calcular(dto);
// Testando apenas a lógica de cálculo, isoladamente!
assertEquals(new BigDecimal("1350.00"), preco); // 150 * 10 * 0.9
}
@Test
public void testPedidoService() {
// Usando mocks para isolar o teste
ValidadorPedido validador = mock(ValidadorPedido.class);
CalculadoraPreco calculadora = mock(CalculadoraPreco.class);
PedidoRepository repository = mock(PedidoRepository.class);
EmailService emailService = mock(EmailService.class);
when(calculadora.calcular(any())).thenReturn(new BigDecimal("100"));
PedidoService service = new PedidoService(validador, calculadora, repository, emailService);
service.processarPedido(new PedidoDTO());
verify(repository).save(any());
verify(emailService).enviarConfirmacao(any());
}
3. Reutilização ♻️
Componentes bem separados podem ser reutilizados:
// Este serviço de email pode ser usado em qualquer lugar!
@Service
public class EmailService {
public void enviar(Email email) {
// implementação
}
}
// Usado no PedidoService
@Service
public class PedidoService {
@Autowired EmailService emailService;
// ...
}
// Usado no UsuarioService também!
@Service
public class UsuarioService {
@Autowired EmailService emailService;
// ...
}
Exemplos Práticos do Mundo Java
Exemplo 1: Sistema de E-commerce
// ❌ RUIM - Violando SoC
public class Produto {
private String nome;
private BigDecimal preco;
// Métodos de negócio - OK
public BigDecimal calcularPrecoComDesconto(BigDecimal percentual) {
return preco.multiply(BigDecimal.ONE.subtract(percentual));
}
// Método de persistência - NÃO DEVERIA ESTAR AQUI!
public void salvarNoBanco() {
Connection conn = DriverManager.getConnection("...");
// código SQL...
}
// Método de apresentação - NÃO DEVERIA ESTAR AQUI!
public String toHTML() {
return "<div>" + nome + " - R$ " + preco + "</div>";
}
}
// ✅ BOM - Respeitando SoC
public class Produto {
private String nome;
private BigDecimal preco;
// Apenas lógica de negócio
public BigDecimal calcularPrecoComDesconto(BigDecimal percentual) {
return preco.multiply(BigDecimal.ONE.subtract(percentual));
}
}
@Repository
public class ProdutoRepository {
public void salvar(Produto produto) {
// código de persistência
}
}
@Component
public class ProdutoView {
public String renderizar(Produto produto) {
return "<div>" + produto.getNome() + " - R$ " + produto.getPreco() + "</div>";
}
}
Exemplo 2: Processamento de Pagamento
// Separando responsabilidades em um sistema de pagamento
// 1. Validação
@Component
public class ValidadorPagamento {
public void validar(DadosPagamento dados) {
if (dados.getValor().compareTo(BigDecimal.ZERO) <= 0) {
throw new ValorInvalidoException();
}
if (!isCartaoValido(dados.getNumeroCartao())) {
throw new CartaoInvalidoException();
}
}
private boolean isCartaoValido(String numero) {
// Algoritmo de Luhn
return true; // simplificado
}
}
// 2. Processamento
@Component
public class ProcessadorPagamento {
@Autowired
private GatewayPagamento gateway;
public RespostaPagamento processar(DadosPagamento dados) {
return gateway.cobrar(dados);
}
}
// 3. Notificação
@Component
public class NotificadorPagamento {
@Autowired
private EmailService emailService;
@Autowired
private SMSService smsService;
public void notificarSucesso(Pagamento pagamento) {
emailService.enviar(criarEmailSucesso(pagamento));
if (pagamento.getCliente().temCelular()) {
smsService.enviar(criarSMSSucesso(pagamento));
}
}
}
// 4. Orquestração
@Service
public class PagamentoService {
@Autowired private ValidadorPagamento validador;
@Autowired private ProcessadorPagamento processador;
@Autowired private NotificadorPagamento notificador;
@Autowired private PagamentoRepository repository;
@Transactional
public Pagamento realizarPagamento(DadosPagamento dados) {
// Cada componente faz sua parte
validador.validar(dados);
RespostaPagamento resposta = processador.processar(dados);
Pagamento pagamento = new Pagamento(dados, resposta);
repository.save(pagamento);
notificador.notificarSucesso(pagamento);
return pagamento;
}
}
SoC em Diferentes Níveis
Nível de Método
// ❌ RUIM - Método fazendo muitas coisas
public void processarRelatorio(List<Venda> vendas) {
// Filtrar vendas
List<Venda> vendasValidas = new ArrayList<>();
for (Venda v : vendas) {
if (v.getValor() > 0 && v.getData() != null) {
vendasValidas.add(v);
}
}
// Calcular total
BigDecimal total = BigDecimal.ZERO;
for (Venda v : vendasValidas) {
total = total.add(v.getValor());
}
// Formatar relatório
StringBuilder sb = new StringBuilder();
sb.append("Relatório de Vendas\n");
sb.append("Total: ").append(total).append("\n");
// Salvar arquivo
Files.write(Paths.get("relatorio.txt"), sb.toString().getBytes());
// Enviar email
emailService.enviar("relatorio@empresa.com", sb.toString());
}
// ✅ BOM - Cada método com uma responsabilidade
public void processarRelatorio(List<Venda> vendas) {
List<Venda> vendasValidas = filtrarVendasValidas(vendas);
BigDecimal total = calcularTotal(vendasValidas);
String relatorio = formatarRelatorio(vendasValidas, total);
salvarRelatorio(relatorio);
enviarRelatorioPorEmail(relatorio);
}
private List<Venda> filtrarVendasValidas(List<Venda> vendas) {
return vendas.stream()
.filter(v -> v.getValor().compareTo(BigDecimal.ZERO) > 0)
.filter(v -> v.getData() != null)
.collect(Collectors.toList());
}
private BigDecimal calcularTotal(List<Venda> vendas) {
return vendas.stream()
.map(Venda::getValor)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private String formatarRelatorio(List<Venda> vendas, BigDecimal total) {
return String.format("Relatório de Vendas\nTotal: %s\nQuantidade: %d",
total, vendas.size());
}
Nível de Pacotes
com.empresa.app/
├── controller/ # Responsável por receber requisições
│ ├── UsuarioController.java
│ └── ProdutoController.java
├── service/ # Lógica de negócio
│ ├── UsuarioService.java
│ └── ProdutoService.java
├── repository/ # Acesso a dados
│ ├── UsuarioRepository.java
│ └── ProdutoRepository.java
├── model/ # Entidades do domínio
│ ├── Usuario.java
│ └── Produto.java
├── dto/ # Objetos de transferência
│ ├── UsuarioDTO.java
│ └── ProdutoDTO.java
└── util/ # Utilitários
├── EmailUtil.java
└── ValidadorUtil.java
Aplicando SoC com Spring Boot
Spring Boot naturalmente encoraja SoC através de suas anotações:
@Configuration // Configuração separada
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Component // Componente reutilizável
public class CacheManager {
private Map<String, Object> cache = new HashMap<>();
// ...
}
@Service // Lógica de negócio
public class ClienteService {
// ...
}
@Repository // Acesso a dados
public interface ClienteRepository extends JpaRepository<Cliente, Long> {
// ...
}
@RestController // Controlador REST
public class ClienteController {
// ...
}
@Aspect // Aspectos transversais (logging, segurança)
@Component
public class LoggingAspect {
@Around("@annotation(Loggable)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
logger.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
return proceed;
}
}
Padrões de Design que Implementam SoC
1. Strategy Pattern
// Separando algoritmos de cálculo de frete
public interface CalculadoraFrete {
BigDecimal calcular(Pedido pedido);
}
@Component
public class FretePAC implements CalculadoraFrete {
public BigDecimal calcular(Pedido pedido) {
return pedido.getPeso().multiply(new BigDecimal("5.00"));
}
}
@Component
public class FreteSedex implements CalculadoraFrete {
public BigDecimal calcular(Pedido pedido) {
return pedido.getPeso().multiply(new BigDecimal("15.00"));
}
}
@Service
public class FreteService {
private Map<TipoFrete, CalculadoraFrete> calculadoras;
public BigDecimal calcularFrete(Pedido pedido, TipoFrete tipo) {
return calculadoras.get(tipo).calcular(pedido);
}
}
2. Observer Pattern
// Separando notificações de eventos
public interface EventListener {
void onPedidoCriado(Pedido pedido);
}
@Component
public class EmailListener implements EventListener {
public void onPedidoCriado(Pedido pedido) {
// Enviar email
}
}
@Component
public class EstoqueListener implements EventListener {
public void onPedidoCriado(Pedido pedido) {
// Atualizar estoque
}
}
@Component
public class NotaFiscalListener implements EventListener {
public void onPedidoCriado(Pedido pedido) {
// Gerar nota fiscal
}
}
@Service
public class PedidoService {
@Autowired
private List<EventListener> listeners;
public void criarPedido(PedidoDTO dto) {
Pedido pedido = new Pedido(dto);
// salvar pedido...
// Notificar todos os interessados
listeners.forEach(l -> l.onPedidoCriado(pedido));
}
}
Microserviços: SoC em Escala
// Serviço de Usuários
@SpringBootApplication
public class UserServiceApplication {
// Responsável apenas por usuários
}
// Serviço de Pedidos
@SpringBootApplication
public class OrderServiceApplication {
// Responsável apenas por pedidos
}
// Serviço de Notificações
@SpringBootApplication
public class NotificationServiceApplication {
// Responsável apenas por notificações
}
// Comunicação via REST ou mensageria
@FeignClient(name = "user-service")
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id);
}
Armadilhas Comuns e Como Evitá-las
1. God Class (Classe Deus)
// ❌ RUIM - Classe fazendo tudo
public class SistemaCompleto {
// 50 atributos
// 100 métodos
// Validação, cálculo, persistência, apresentação...
}
// ✅ BOM - Responsabilidades distribuídas
public class Usuario { /* modelo */ }
public class UsuarioService { /* lógica */ }
public class UsuarioRepository { /* persistência */ }
public class UsuarioController { /* API */ }
2. Anemic Domain Model
// ⚠️ CUIDADO - Modelo anêmico (sem comportamento)
public class Conta {
private BigDecimal saldo;
// apenas getters e setters
}
public class ContaService {
public void sacar(Conta conta, BigDecimal valor) {
// toda lógica aqui
}
}
// ✅ MELHOR - Modelo rico
public class Conta {
private BigDecimal saldo;
public void sacar(BigDecimal valor) {
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException();
}
this.saldo = saldo.subtract(valor);
}
public void depositar(BigDecimal valor) {
this.saldo = saldo.add(valor);
}
}
Conclusão
Separation of Concerns não é apenas um princípio teórico - é uma ferramenta prática que torna nosso código:
- Mais fácil de entender 🧠
- Mais fácil de manter 🔧
- Mais fácil de testar 🧪
- Mais fácil de escalar 📈
- Mais fácil de reutilizar ♻️
Lembre-se:
- Cada classe deve ter uma razão para mudar
- Cada método deve fazer uma coisa bem feita
- Cada camada deve ter uma responsabilidade clara
- Prefira composição sobre herança
- Use interfaces para reduzir acoplamento
Próximos Passos
- Estude os princípios SOLID (SoC é parte do S - Single Responsibility)
- Explore padrões de design como MVC, MVP, MVVM
- Pratique Domain-Driven Design (DDD)
- Aprenda sobre Arquitetura Hexagonal
- Experimente com Microserviços
Call to Action
Agora olhe para seu código Java. Encontre aquela classe com 1000 linhas. Você sabe qual é.
Comece pequeno:
- Extraia um método
- Depois extraia uma classe
- Depois separe em camadas
- Continue refatorando
Roma não foi construída em um dia, e seu código limpo também não será. Mas cada passo na direção certa conta!
💬 Tem alguma dúvida ou quer compartilhar como aplica SoC em seus projetos Java? Deixe um comentário abaixo!
⭐ Se este artigo foi útil, não esqueça de dar uma estrela e compartilhar com seus colegas desenvolvedores Jav
Top comments (0)