Decomposição e Responsabilidade

Aula 4 — Programação Orientada a Objetos

Autor

Marcos M. Raimundo — Instituto de Computação, UNICAMP

Data de Publicação

30 de agosto de 2026

Slides Lista de aulas

1 Proposta da Aula

Nas aulas anteriores, protegemos o objeto: encapsulamento e invariantes garantem que o estado não se corrompa; a Lei de Demeter garante que um objeto não dependa fragilmente das entranhas de seus vizinhos. Mas essas regras respondem só a uma parte do bom design — elas dizem como um objeto deve interagir, não o que ele deve fazer. Um objeto pode respeitar Demeter à risca, ter o estado perfeitamente encapsulado, e ainda assim ser responsável por absolutamente tudo.

O roteiro, em quatro perguntas:

  1. Como reconhecer, no próprio código, uma classe que “sabe demais” ou “faz demais”?
  2. O que exatamente medem coesão e acoplamento, e por que quase sempre andam juntos?
  3. Quando a solução — dividir responsabilidades — vira ela mesma um problema?
  4. Como a linguagem já distingue, semanticamente, entre “usar”, “ter” e “ser parte de” outro objeto?

2 A Classe Deus

God Class (Classe Deus, ou Objeto Onisciente) é uma classe que sabe demais ou faz demais. É comum, no início do aprendizado, confundir “modelar um objeto do mundo real” com “criar um gerente que centraliza tudo”:

public class Pedido {
    private List<Item> itens;
    private Cliente cliente;

    // Responsabilidade 1: Regra de Negocio (Core)
    public double calcularTotal() { /* soma itens... */ }

    // Responsabilidade 2: Logica de Impostos (Externo)
    public double calcularICMS() {
        if (cliente.getEstado().equals("SP")) return calcularTotal() * 0.18;
        return calcularTotal() * 0.12;
    }

    // Responsabilidade 3: Formatacao e UI (Apresentacao)
    public String gerarReciboTexto() {
        return "Cliente: " + cliente.getNome() + " Total: " + calcularTotal();
    }

    // Responsabilidade 4: Persistencia (Infraestrutura)
    public void salvarNoBanco() { /* JDBC, conexao, transacao... */ }

    // Responsabilidade 5: Comunicacao (Rede)
    public void enviarConfirmacaoEmail() { /* SMTP, credenciais... */ }
}

Quatro razões pelas quais esse design é ruim:

  1. Fragilidade extrema: se a regra de imposto muda, altera-se Pedido; se o banco muda de MySQL para MongoDB, altera-se Pedido; se o layout do recibo muda, altera-se Pedido. Cada alteração arrisca introduzir bugs em partes não relacionadas.
  2. Acoplamento rígido com infraestrutura: Pedido está “sujo” com SQL e SMTP — impossível testar o cálculo do total sem um banco de dados ativo.
  3. Dificuldade de reuso: a lógica de e-mail, enterrada dentro de Pedido, não pode ser reaproveitada em outro lugar (o cadastro de usuários, por exemplo).
  4. Carga cognitiva alta: entender o que um Pedido faz exige ler centenas de linhas de infraestrutura que nada têm a ver com o domínio.

Em suma, a Classe Deus viola a Separação de Preocupações: concentra todas as razões de mudança num único ponto, tornando o sistema rígido.

3 Coesão e Acoplamento

Duas métricas de qualidade caminham de mãos dadas — aumentar a coesão de uma classe costuma reduzir o acoplamento do sistema.

Coesão mede o foco interno: o quanto as responsabilidades de uma classe pertencem juntas. Uma caixa de ferramentas só com chaves de fenda é altamente coesa; a mesma caixa com um sanduíche, um sapato e um controle remoto tem baixa coesão — ela tenta ser várias coisas ao mesmo tempo.

Acoplamento mede o vínculo externo: o quanto uma classe conhece ou depende das entranhas de outra. Se, para ligar a TV da sala, você precisasse primeiro abrir a geladeira e apertar um botão lá dentro, isso seria alto acoplamento — dois sistemas independentes amarrados sem necessidade.

“Alta coesão nos diz que o objeto é uma unidade lógica sólida; baixo acoplamento nos diz que esse objeto é livre para evoluir sem quebrar o mundo ao seu redor.”

O objetivo do design modular é sempre alta coesão e baixo acoplamento — e a ferramenta para chegar lá é o assunto do próximo bloco.

4 SRP e o Teste do “E”

O Princípio da Responsabilidade Única (SRP): uma classe deve ter uma, e apenas uma, razão para mudar. Uma heurística simples e poderosa para diagnosticar violações: descreva o que a classe faz numa única frase. Se você precisar de um “E” (ou “OU”), a classe tem responsabilidades demais.

  • “A classe Usuario valida a senha E salva os dados no banco.”
  • “A classe Relatorio formata os dados em PDF E envia por FTP.”
  • “A classe Pedido calcula o total E processa o pagamento E notifica o cliente.”
public class Pedido {
    private Carrinho carrinho;
    // Faz o calculo E processa o pagamento E envia e-mail
    public void finalizarPedido() {
        double total = carrinho.getTotal();                            // 1. Calculo
        System.out.println("Processando R$ " + total + " no cartao..."); // 2. Pagamento (misturado)
        System.out.println("Enviando e-mail para o cliente...");         // 3. Notificacao (misturado)
    }
}

Imagine tentar testar só o cálculo do total: seria impossível sem acidentalmente disparar um e-mail ou tentar cobrar um cartão de verdade — as três lógicas estão fortemente acopladas no mesmo método.

A solução: extrair especialistas e delegar. Já temos Pagavel (Aula 3); criamos ServicoEmail:

public class Pedido {
    private Carrinho carrinho;
    private Pagavel metodoPagamento;   // Abstracao de Pagamento
    private ServicoEmail emailService; // Nova classe especialista

    // Inversao de Controle: injetamos as dependencias no construtor
    public Pedido(Carrinho carrinho, Pagavel pagavel, ServicoEmail email) {
        this.carrinho = carrinho;
        this.metodoPagamento = pagavel;
        this.emailService = email;
    }

    public void finalizar() { // sempre melhor delegar
        metodoPagamento.processar(carrinho.getTotal());
        emailService.enviarConfirmacao(carrinho.getCliente());
    }
}

O Pedido deixa de ser “faz-tudo” e vira “gerente” — não executa a tarefa, delega para quem sabe. Essa Injeção de Dependência (DI) via construtor traz três ganhos imediatos:

  1. Testabilidade: um teste pode injetar um PagavelMock e um EmailMock — a lógica de finalizar() é verificada sem cobrar cartão de verdade nem enviar spam.
  2. Reuso: ServicoEmail pode ser usado por outras classes (RecuperarSenha, por exemplo), já que não está mais preso dentro de Pedido.
  3. Manutenibilidade: um bug no pagamento fica isolado nas classes de pagamento, sem risco de quebrar o envio de e-mails.
DicaPor que testar Pedido.finalizar() sem Injeção de Dependência exigiria cobrar um cartão de crédito de verdade?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Para ajudar, julgue V ou F nos 4 itens abaixo — a questão só conta se acertar todos:

  • □ Uma classe que exige “E” para ser descrita numa frase provavelmente viola o SRP.
  • □ Injeção de Dependência via construtor garante que o objeto nunca nasça sem suas colaborações essenciais.
  • □ Sem DI, testar a lógica de Pedido exigiria disparar um e-mail real ou cobrar um cartão de verdade.
  • □ Usar um “Mock” no lugar de Pagavel real é uma violação do princípio de Injeção de Dependência.

5 SRP Avançado: a Teoria do Ator e a Métrica LCOM

Robert C. Martin refinou o SRP: “uma razão para mudar” era frequentemente mal interpretada. A verdadeira fonte de mudança não são algoritmos, são pessoas — os atores ou departamentos que pedem a alteração. Se uma classe atende dois departamentos diferentes, ela tem duas razões para mudar:

public class Funcionario {
    private double salarioBase;
    private int horasExtras;

    // O CFO solicita e altera esta logica
    public double calcularPagamento() { return salarioBase + (horasExtras * 50); }

    // O RH solicita alteracoes no formato do relatorio
    public String gerarRelatorioHoras() { return "Total de Horas: " + horasExtras + "h"; }
}

Misturar essas lógicas acopla os ciclos de lançamento do financeiro e do RH — uma mudança de layout de relatório força recompilar e re-testar a folha de pagamento.

Existe até uma prova matemática de falta de coesão: o LCOM (Lack of Cohesion in Methods), que analisa a interseção de uso de atributos pelos métodos:

public class GestorDeUsuario {
    private String nome;        // usado APENAS por salvarNome()
    private String ipConexao;   // usado APENAS por logAcesso()

    public void salvarNome(String n) { this.nome = n; /* persistencia */ }
    public void logAcesso(String ip) { this.ipConexao = ip; /* auditoria */ }
}

Os métodos não compartilham estado algum — a interseção de uso é vazia. Isso é o código implorando para virar duas classes: RepositorioUsuario e AuditoriaAcesso.

6 O Lado Sombrio do SRP

O SRP aplicado sem pragmatismo pode destruir a legibilidade do sistema. Dois riscos:

  • Cirurgia de Espingarda (Shotgun Surgery): a separação é tão excessiva que implementar uma única regra de negócio exige abrir e modificar 15 classes minúsculas diferentes.
  • Modelo Anêmico (revisitado da Aula 2, agora em escala arquitetural): retirar toda a responsabilidade do objeto, reduzindo-o a um contentor passivo:
// RUIM: fragmentacao excessiva (Modelo Anemico)
public class Conta {
    public double saldo; // estado exposto, sem comportamento
}
public class TransferenciaService {
    public void transferir(Conta origem, Conta destino, double valor) {
        origem.saldo -= valor;
        destino.saldo += valor;
    }
}

// BOM: SRP equilibrado (Modelo Rico)
public class Conta {
    private double saldo;
    public void debitar(double valor) { /* valida e deduz */ }
    public void creditar(double valor) { /* adiciona */ }
}

O SRP não proíbe que Conta gerencie seu próprio saldo — pelo contrário, assegurar essa integridade é a responsabilidade primária dela. A regra de equilíbrio: agrupe o que muda junto e pelos mesmos motivos; separe apenas o que muda em ritmos ou por atores diferentes.

7 Tipos de Associação

Proteger o estado e aplicar SRP não basta — é preciso conectar as peças. Três níveis de força estrutural definem quem instancia e quem é coletado junto com quem:

Dependência (“Usa um”) — o vínculo mais fraco: um objeto usa outro temporariamente, sem guardar sua referência.

public class Carrinho {
    // O Carrinho DEPENDE da lista temporaria para conferir,
    // mas nao a "guarda" para si (nao ha this.lista = ...)
    public boolean confereTotal(ArrayList<Produto> itensVerificacao) {
        double soma = 0.0;
        for (Produto p : itensVerificacao) soma += p.precoFinal();
        return soma == this.total;
    }
}

Agregação (“Tem um”) — vínculo todo-parte, mas com ciclos de vida independentes: a parte sobrevive à destruição do todo.

public class Carrinho {
    private ArrayList<Produto> itens; // o Carrinho "tem" Produtos
    public void adicionarItem(Produto p) { this.itens.add(p); }
}
// Se o carrinho for destruido, o Smartphone ainda existe no catalogo!
carrinho = null; // Carrinho morre; Produto sobrevive.

Composição (“É parte de”) — a forma mais forte: a parte pertence unicamente ao todo, e morre com ele (como um documento e suas páginas).

public class Cliente {
    private Cartao cartao;
    // Cliente nasce e gera seu proprio cartao; ninguem mais tem acesso direto a ele
    public Cliente(String nome, double limiteInicial) {
        this.nome = nome;
        this.cartao = new Cartao(limiteInicial); // ciclo de vida atrelado
    }
}

A escolha entre Agregação e Composição é, muitas vezes, uma decisão de domínio de negócio, não só mecânica: se a conta de um cliente é excluída, os dados do cartão de crédito privado devem sumir junto (Composição), não flutuar soltos no sistema (o que uma Agregação permitiria).

DicaPor que a escolha entre Agregação e Composição para Cliente/Cartao depende de uma decisão de negócio, e não só de como o código é escrito?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Para ajudar, julgue V ou F nos 4 itens abaixo — a questão só conta se acertar todos:

  • □ Na Dependência, o objeto usado costuma ser recebido como parâmetro de método, não guardado como atributo.
  • □ Na Agregação, destruir o “todo” implica destruir automaticamente a “parte”.
  • □ Na Composição, a “parte” não tem sentido de existir fora do “todo” — nasce e morre com ele.
  • □ A escolha entre Agregação e Composição pode depender de uma regra de negócio, não só da sintaxe do código.

8 Delegação: o Motor da Composição

Associação define a estrutura (quem contém quem); Delegação define o comportamento (quem faz o quê com o que contém). Relembrando a violação de Demeter da Aula 3:

Cliente joao = new Cliente("Joao");
joao.cadastrarCartao(new Cartao(1000.0));
// O Principal sabe demais! Ele "rouba" o cartao do cliente
// para processar o pagamento por conta propria.
joao.getCartao().processar(500.0);

A cura não é esconder o Cartao atrás de um getter mais discreto — é impedir que qualquer um precise dele diretamente:

public class Cliente {
    private String nome;
    private Cartao cartao;

    public void pagar(double valor) {
        // 1. Defesa da maquina de estados (Fail-Fast)
        if (this.cartao == null) {
            throw new IllegalStateException("Cliente sem cartao.");
        }
        // 2. DELEGACAO: o Cliente nao faz operacoes matematicas.
        // Ele repassa o comando ao especialista financeiro.
        this.cartao.processar(valor);
    }
}

Antes: joao.getCartao().processar(500.0); — depois: joao.pagar(500.0);. O chamador agora desconhece completamente a existência de Cartao. Associação (ter um objeto) mais Delegação (usar o objeto) é a base para trocar componentes sem impacto no sistema — e é o mecanismo que torna a composição, segundo o princípio “favoreça composição em vez de herança”, tão poderosa quanto a herança, porém muito mais flexível.

Dicajoao.pagar(500.0) esconde a existência de Cartao de quem chama. Isso é o mesmo mecanismo da Lei de Demeter (Aula 3) ou algo diferente?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Para ajudar, julgue V ou F nos 4 itens abaixo — a questão só conta se acertar todos:

  • joao.getCartao().processar(500.0) respeita a Lei de Demeter, pois Cartao é um atributo de Cliente.
  • joao.pagar(500.0) é um exemplo de “Tell”: o chamador diz o que quer, sem saber como é feito.
  • □ Depois da refatoração, o código que chama pagar() desconhece completamente a existência de Cartao.
  • □ Associação (ter um objeto) e Delegação (usar esse objeto) juntas permitem trocar o Cartao sem afetar quem chama pagar().

9 Conclusão

Voltando às quatro perguntas da abertura:

  1. Reconhecer uma Classe Deus: ela precisa de “E” para ser descrita numa frase, ou mistura razões de mudança de atores diferentes.
  2. Coesão e acoplamento: coesão é o quanto uma classe é uma unidade lógica sólida; acoplamento é o quanto ela depende de vizinhos — as duas costumam melhorar juntas.
  3. Quando dividir vira problema: quando a fragmentação exige tocar em muitas classes para uma única regra (Cirurgia de Espingarda), ou esvazia objetos de comportamento (Modelo Anêmico).
  4. Usar, ter, ser parte de: Dependência (temporário), Agregação (ciclos de vida independentes), Composição (ciclos de vida atrelados) — e a Delegação é o que torna essas estruturas vivas.

Ponte para a Aula 5

A delegação de hoje usou uma interface já pronta (Pagavel, da Aula 1). A Aula 5 formaliza a interface como contrato de verdade, e introduz uma ferramenta que temos usado implicitamente — testes automatizados com mocks — de forma explícita, com JUnit.

Exercícios Soluções