Pular para o conteúdo

SRP - Single Responsibility Principle

Robert C. Martin enuncia o princípio como: “A module should have one, and only one, reason to change.” A frase “um motivo para mudar” foi frequentemente mal interpretada como “uma classe deve fazer apenas uma coisa”. Em Clean Architecture, Martin refina a definição: um módulo deve ser responsável perante um único ator. Reason to change traduz-se em fonte da mudança — e fontes de mudança são pessoas/papéis: o time financeiro, o time de RH, o DBA, o time de operações.

Portanto, SRP é essencialmente um princípio de coesão de acoplamento social: código que muda pelas mesmas razões (mesmos atores) deve ficar junto; código que muda por razões diferentes (atores diferentes) deve ser separado.

Quando uma classe atende a múltiplos atores, mudanças solicitadas por um ator podem quebrar o comportamento esperado por outro. Martin ilustra com o clássico exemplo Employee que possui calcularSalario() (dono: financeiro), reportarHoras() (dono: RH) e salvar() (dono: DBA). Se calcularSalario e reportarHoras compartilham um método utilitário horasRegulares(), uma mudança pedida pelo financeiro pode alterar silenciosamente o número que o RH reporta. Dois atores, uma classe, colisão inevitável.

Os sintomas concretos de violação de SRP:

  • Colisões de merge frequentes no mesmo arquivo por times diferentes.
  • Classes “faz-tudo” (UserManager, OrderService com 1500 linhas) que agregam persistência, validação, formatação e regra de negócio.
  • Um bug numa funcionalidade força reteste de funcionalidades não relacionadas.

Levar SRP ao extremo produz fragmentação anêmica: dezenas de classes de uma linha, cada uma com um método, e uma teia de colaboradores difícil de navegar. O custo de SRP é o aumento do número de peças e da indireção. A coesão que você ganha pode ser anulada pela dispersão que você cria.

  • Não separe responsabilidades que sempre mudam juntas pelo mesmo ator — isso é acoplamento coeso legítimo, e reuni-las é correto.
  • Em código com baixíssima taxa de mudança (scripts, protótipos descartáveis), o investimento em separação raramente se paga.
  • SRP se aplica a módulos e classes, mas também a funções: uma função gigante com muitos níveis de abstração viola o espírito do princípio.

Uma única classe mistura três atores: regra de cálculo (financeiro), persistência (DBA) e notificação (marketing/comunicação).

@Service
public class RelatorioPedidoService {
private final DataSource dataSource;
private final JavaMailSender mailSender;
public RelatorioPedidoService(DataSource dataSource, JavaMailSender mailSender) {
this.dataSource = dataSource;
this.mailSender = mailSender;
}
public BigDecimal calcularTotalComImpostos(Pedido pedido) {
BigDecimal subtotal = pedido.itens().stream()
.map(ItemPedido::valorTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal imposto = subtotal.multiply(new BigDecimal("0.17"));
return subtotal.add(imposto);
}
public void salvar(Pedido pedido) {
try (Connection conn = dataSource.getConnection()) {
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO pedido (id, total) VALUES (?, ?)");
ps.setString(1, pedido.id());
ps.setBigDecimal(2, calcularTotalComImpostos(pedido));
ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void enviarConfirmacao(Pedido pedido) {
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo(pedido.emailCliente());
msg.setSubject("Pedido confirmado");
msg.setText("Total: " + calcularTotalComImpostos(pedido));
mailSender.send(msg);
}
}
public record Pedido(String id, String emailCliente, List<ItemPedido> itens) {}
public record ItemPedido(String descricao, BigDecimal valorTotal) {}
@Component
public class CalculadoraImpostoPedido {
private static final BigDecimal ALIQUOTA = new BigDecimal("0.17");
public BigDecimal calcularTotalComImpostos(Pedido pedido) {
BigDecimal subtotal = pedido.itens().stream()
.map(ItemPedido::valorTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return subtotal.add(subtotal.multiply(ALIQUOTA));
}
}
public interface PedidoRepository {
void salvar(Pedido pedido, BigDecimal total);
}
@Repository
public class JdbcPedidoRepository implements PedidoRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcPedidoRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void salvar(Pedido pedido, BigDecimal total) {
jdbcTemplate.update("INSERT INTO pedido (id, total) VALUES (?, ?)",
pedido.id(), total);
}
}
public interface NotificadorPedido {
void notificarConfirmacao(Pedido pedido, BigDecimal total);
}
@Component
public class EmailNotificadorPedido implements NotificadorPedido {
private final JavaMailSender mailSender;
public EmailNotificadorPedido(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void notificarConfirmacao(Pedido pedido, BigDecimal total) {
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo(pedido.emailCliente());
msg.setSubject("Pedido confirmado");
msg.setText("Total: " + total);
mailSender.send(msg);
}
}
@Service
public class ProcessadorPedido {
private final CalculadoraImpostoPedido calculadora;
private final PedidoRepository repository;
private final NotificadorPedido notificador;
public ProcessadorPedido(CalculadoraImpostoPedido calculadora,
PedidoRepository repository,
NotificadorPedido notificador) {
this.calculadora = calculadora;
this.repository = repository;
this.notificador = notificador;
}
public void processar(Pedido pedido) {
BigDecimal total = calculadora.calcularTotalComImpostos(pedido);
repository.salvar(pedido, total);
notificador.notificarConfirmacao(pedido, total);
}
}

Cada classe agora responde a um único ator. Uma mudança na regra fiscal toca apenas CalculadoraImpostoPedido; um novo canal de notificação não arrisca a lógica de cálculo.

classDiagram
    class RelatorioPedidoService {
        +calcularTotalComImpostos(Pedido)
        +salvar(Pedido)
        +enviarConfirmacao(Pedido)
    }
    note for RelatorioPedidoService "ANTES: 3 atores em 1 classe"

    class ProcessadorPedido {
        +processar(Pedido)
    }
    class CalculadoraImpostoPedido {
        +calcularTotalComImpostos(Pedido)
    }
    class PedidoRepository {
        <<interface>>
        +salvar(Pedido, BigDecimal)
    }
    class NotificadorPedido {
        <<interface>>
        +notificarConfirmacao(Pedido, BigDecimal)
    }

    ProcessadorPedido --> CalculadoraImpostoPedido
    ProcessadorPedido --> PedidoRepository
    ProcessadorPedido --> NotificadorPedido
  1. Qual é a definição refinada de “motivo para mudar” que Martin adota em Clean Architecture?
Resposta

“Motivo para mudar” significa um único ator — o grupo de pessoas ou stakeholder que solicita a mudança. Coesão em SRP é sobre agrupar código que muda pelas mesmas razões sociais/organizacionais.

  1. Por que separar calcularSalario() e reportarHoras() de uma classe Employee?
Resposta

Porque respondem a atores diferentes (financeiro vs. RH). Se compartilharem lógica, uma mudança pedida por um ator pode alterar silenciosamente o comportamento esperado pelo outro.

  1. Qual o principal custo de aplicar SRP de forma dogmática?
Resposta

Fragmentação e indireção excessivas: muitas classes minúsculas e uma teia de colaboradores difícil de navegar, além de dispersar código que muda junto.

  1. Separar um sistema em Controller/Service/Repository garante SRP?
Resposta

Não. É uma boa separação técnica, mas não garante SRP se o Service continuar atendendo a múltiplos atores. SRP é sobre razões de mudança, não sobre camadas técnicas.