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.

10 Exercícios

10.1 Questões discursivas

  1. A classe Pedido do início desta aula tinha cinco responsabilidades (cálculo, imposto, formatação, persistência, e-mail). Escolha duas delas e explique, para cada uma, qual “ator” (departamento ou stakeholder) provavelmente solicitaria mudanças nela — e por que isso já é evidência de violação do SRP.

  2. Um colega decide aplicar o SRP “ao pé da letra” e separa a classe Pedido em 12 classes de uma linha cada, uma para cada campo. Explique, usando os conceitos de Cirurgia de Espingarda e Modelo Anêmico, por que essa fragmentação pode ser pior do que a Classe Deus original.

  3. Explique a diferença entre Agregação e Composição usando o exemplo de Cliente e Cartao. Por que essa escolha de design depende de uma decisão sobre o domínio de negócio (o que acontece com o cartão quando a conta é excluída), e não apenas de como o atributo é declarado em código?

10.2 Questões de Verdadeiro/Falso

Cada bloco de 4 itens trata do mesmo tema. A questão só é considerada correta se todos os 4 itens forem julgados corretamente (deixar em branco tem penalidade de 20% da nota da questão).

NotaA Classe Deus (God Class)
  • □ É identificada por centralizar lógicas de múltiplos atores (financeiro, infraestrutura, UI) num único arquivo.
  • □ O “Teste do E” ajuda a diagnosticá-la: se a classe faz X E Y, provavelmente tem responsabilidades demais.
  • □ Uma Classe Deus facilita a manutenção, pois toda a lógica fica concentrada num só lugar.
  • □ Testar isoladamente uma responsabilidade de uma Classe Deus costuma exigir efeitos colaterais indesejados (e-mail, banco).
NotaCoesão e Acoplamento
  • □ Se duas classes com métodos completamente distintos (sem nenhum atributo em comum) fossem fundidas numa única classe apenas para reduzir o número de arquivos do projeto, essa fusão aumentaria a coesão do sistema.
  • □ No limite em que uma classe A depende de tantos detalhes internos de uma classe B que qualquer mudança em B obriga a reescrever A, dizemos que A e B têm acoplamento muito baixo.
  • □ Numa oficina mecânica organizada em bancadas por especialidade (uma para motor, uma para elétrica, uma para funilaria), cada bancada guardando só as ferramentas da sua especialidade é uma analogia de alta coesão, assim como a caixa de ferramentas só com chaves de fenda desta aula.
  • □ Duas classes podem ter baixo acoplamento entre si mesmo que cada uma, isoladamente, tenha baixíssima coesão.
NotaSRP e o Teste do “E”
  • □ Se uma classe tiver apenas um único método público, isso já garante que ela respeita o SRP.
  • □ Se a descrição de uma classe exige a conjunção “E”, ela provavelmente viola o SRP.
  • □ Aplicar o SRP significa que uma classe só pode ter um único método público.
  • Pedido que calcula total, processa pagamento e envia e-mail no mesmo método viola o SRP.
NotaInjeção de Dependência
  • □ Se Pedido continuasse criando new CartaoDeCredito() e new ServicoEmail() dentro do próprio construtor, mas guardasse essas instâncias em atributos privados corretamente encapsulados, isso já seria suficiente para obter os ganhos de testabilidade da Injeção de Dependência.
  • □ A injeção via construtor garante que o objeto nunca exista sem suas dependências essenciais.
  • □ A DI aumenta o acoplamento, pois obriga o objeto a conhecer quem o instanciou.
  • □ Sem DI, testar Pedido.finalizar() isoladamente, sem efeitos colaterais reais, é muito mais difícil.
NotaA Teoria do Ator no SRP
  • □ Numa fintech em que o mesmo microsserviço calcula o score de crédito (usado pelo time de Risco) e também formata esse score para exibição no app do cliente (usado pelo time de Produto), esse serviço, pela Teoria do Ator, tem apenas uma razão para mudar, pois lida com um único conceito: “score de crédito”.
  • □ Misturar lógica solicitada pelo RH e pelo Financeiro na mesma classe é seguro, pois são setores diferentes.
  • □ O conflito de atores é uma fonte comum de “quebras colaterais” em classes com múltiplas razões de mudança.
  • □ No limite em que uma empresa tem um único departamento responsável por todas as decisões de negócio (sem separação entre financeiro, RH, produto etc.), a Teoria do Ator deixaria de fazer qualquer distinção útil entre classes, pois haveria apenas um ator possível para todo o sistema.
NotaA Métrica LCOM
  • □ No limite em que todo método de uma classe usa exatamente os mesmos atributos que todos os outros métodos, o LCOM dessa classe seria o mais alto possível.
  • □ Um LCOM alto indica uma classe muito coesa e bem projetada.
  • □ Se metade dos métodos usa o atributo A e a outra metade usa só o atributo B, a classe deveria ser dividida.
  • □ LCOM é uma forma de provar matematicamente que uma classe é, na prática, duas classes disfarçadas.
NotaO Lado Sombrio do SRP
  • □ Num sistema de e-commerce em que adicionar um novo campo obrigatório (“CPF do comprador”) ao formulário de checkout exige editar 20 classes de uma linha cada (uma para nome, uma para e-mail, uma para CPF etc.), esse cenário ilustra a Cirurgia de Espingarda, e a solução recomendada pela aula é sempre criar ainda mais classes minúsculas para isolar o novo campo.
  • □ O Modelo Anêmico é uma consequência possível da aplicação fanática e sem pragmatismo do SRP.
  • □ O SRP proíbe que uma classe como Conta gerencie seu próprio saldo internamente.
  • □ Como o SRP recomenda alta coesão, uma classe Conta que gerencia tanto debitar() quanto creditar() sobre o mesmo saldo já é, por si só, uma violação leve do SRP, pois tem dois métodos que alteram o mesmo estado.
NotaAssociação: Dependência (“Usa um”)
  • □ Se um método recebesse um objeto como parâmetro e o armazenasse permanentemente num atributo da classe logo na primeira linha do método, essa relação continuaria sendo classificada como Dependência.
  • □ O objeto usado numa dependência costuma ser guardado como atributo de longo prazo.
  • □ Um método de uma API de pagamentos que recebe um objeto ContextoDeAuditoria só para registrar um log da chamada atual, sem guardar essa referência para uso posterior, exemplifica o mesmo tipo de vínculo fraco (Dependência) visto em Carrinho.confereTotal().
  • Carrinho.confereTotal(ArrayList<Produto> itens) recebendo a lista como parâmetro é um exemplo de dependência.
NotaAssociação: Agregação (“Tem um”)
  • □ Se, no exemplo do Carrinho e dos Produto, destruir o carrinho (carrinho = null) também apagasse os Produto do catálogo do sistema, essa relação ainda seria corretamente chamada de Agregação.
  • □ Se um Carrinho for destruído, os Produto que ele referenciava também são destruídos.
  • □ Vários objetos “todo” diferentes podem referenciar o mesmo objeto “parte” simultaneamente.
  • □ É uma forma de posse mais fraca do que a Composição.
NotaAssociação: Composição (“É parte de”)
  • □ No limite em que um objeto “parte” de uma Composição pudesse ser compartilhado por dois objetos “todo” diferentes ao mesmo tempo, a relação continuaria sendo, por definição, uma Composição.
  • □ Num sistema de RH em que cada Funcionario tem seu próprio HistoricoDisciplinar, criado apenas quando o funcionário é contratado e apagado quando o funcionário é desligado do sistema, essa relação seria mais bem classificada como Composição do que como Agregação.
  • □ Um Documento e suas Pagina são um exemplo clássico de Composição.
  • □ A escolha entre Agregação e Composição é sempre puramente sintática, nunca uma decisão de negócio.
NotaDelegação
  • □ Se um método pagar() delegasse ao Cartao a operação de processar o valor, mas antes disso navegasse por this.cartao.getBanco().getGerente() só para registrar um log, essa navegação extra ainda seria uma delegação limpa, sem qualquer violação de Demeter.
  • joao.getCartao().processar(500.0) é um exemplo correto de delegação.
  • joao.pagar(500.0), delegando internamente ao Cartao, corrige a violação de Demeter do exemplo anterior.
  • □ A delegação é o mecanismo que torna a composição uma alternativa flexível à herança.
NotaTell, Don’t Ask e a Síntese da Aula
  • □ Num sistema de estoque em que, antes de vender um produto, o código lê produto.getQuantidade() e produto.getQuantidadeMinima() para decidir por fora se a venda é permitida, esse padrão está alinhado com “Tell, Don’t Ask”, da mesma forma que pedido.clientePodePagar() está.
  • □ Associação, SRP e Delegação são princípios independentes, sem nenhuma relação entre si.
  • □ Um sistema bem decomposto reduz a carga cognitiva ao permitir raciocinar sobre uma responsabilidade por vez.
  • □ Delegar ao especialista certo permite trocar sua implementação sem que o orquestrador precise mudar.