Composição de Sistemas: Contratos e Estabilidade
Aula 3 — Programação Orientada a Objetos
1 Proposta da Aula
Nas Aulas 1 e 2, a preocupação foi quase puramente anatômica: como um objeto nasce, como se diferencia de outro na Heap, como protege seus próprios atributos. Aprendemos a fabricar peças de alta qualidade, cada uma funcionando isoladamente. Mas peças perfeitas que não se encaixam são o caminho mais rápido para o fracasso.
O salto desta aula é o de fabricante de peças para arquiteto de sistemas. Um arquiteto não se importa, neste nível, com a liga metálica de um parafuso — importa o encaixe e o torque que ele suporta para conectar a viga ao pilar. Um sistema orientado a objetos é uma rede de colaboradores: o design não trata só do que um objeto sabe ou faz internamente, mas de com quem ele fala e de que forma. Se um objeto sabe demais sobre a estrutura interna do vizinho, qualquer mudança nesse vizinho o quebra — é o que chamamos de acoplamento por “intimidade técnica”. O objetivo desta aula é substituir essa intimidade por um acoplamento baseado em contratos.
O roteiro, em quatro perguntas:
- Como várias classes especialistas colaboram sem que uma precise conhecer os detalhes internos da outra?
- O que exatamente separa a interface (o contrato) da implementação (o detalhe), e por que essa fronteira é a mais importante da Orientação a Objetos?
- Como reconhecer, no próprio código, quando um objeto está “fofocando” demais sobre os vizinhos dos seus vizinhos?
- O que muda quando um sistema é escrito para aceitar qualquer objeto que cumpra um contrato, em vez de uma classe específica?
2 Especialistas Colaborando: Produto, ItemCarrinho, Carrinho
O erro de um programador iniciante ao projetar um sistema é concentrar toda a inteligência numa única classe “gerente”, reduzindo as demais a repositórios de dados — o Modelo Anêmico da Aula 2, agora em escala de sistema. A alternativa: cada classe é um especialista autônomo.
public class Produto {
private String nome;
private double precoBase;
public Produto(String n, double p) { this.nome = n; this.precoBase = p; }
public double getPreco() { return this.precoBase; }
}
public class ItemCarrinho {
private Produto produto;
private int quantidade;
public ItemCarrinho(Produto p, int q) { this.produto = p; this.quantidade = q; }
public double subtotal() {
// MENSAGEM: "Produto, quanto voce custa?"
return produto.getPreco() * quantidade;
}
}Produto é inteiramente autônomo — não conhece o Carrinho nem nenhuma interface de usuário. Isso é baixo acoplamento: o mesmo Produto serve numa vitrine de e-commerce, num sistema de estoque ou num relatório fiscal, sem qualquer alteração. ItemCarrinho não acessa precoBase diretamente (o que seria violação de encapsulamento); ele “pergunta” ao produto, via getPreco().
public class Carrinho {
private List<ItemCarrinho> itens = new ArrayList<>();
public void adicionar(Produto p, int qtd) { this.itens.add(new ItemCarrinho(p, qtd)); }
public double calcularTotal() {
double total = 0;
for (ItemCarrinho item : itens) {
// DELEGACAO: "Item, quanto voce vale no total?"
total += item.subtotal();
}
return total;
}
}O Carrinho não calcula nada sozinho — ele orquestra, usando delegação: percorre a lista e diz a cada item “calcule seu valor”. Ele não conhece preços nem regras de imposto; confia apenas no contrato de que ItemCarrinho tem um método subtotal(). Dividir responsabilidades assim evita os Objetos Deus (God Objects), que crescem sem limite e se tornam impossíveis de manter — e permite testar subtotal() isoladamente, sem montar um carrinho inteiro.
3 Interface vs. Implementação: a Fronteira da Estabilidade
A distinção entre Interface (o contrato: o “O Quê”) e Implementação (o detalhe técnico: o “Como”) é talvez o pilar mais transformador da Orientação a Objetos. A falha de design mais recorrente em sistemas complexos é o acoplamento físico — uma classe que conhece detalhes excessivos das entranhas de outra. Quando essa fronteira é rompida, uma mudança de baixo nível (trocar uma consulta SQL por uma chamada de API) provoca falhas em cascata em módulos que não deveriam nem saber que essa troca aconteceu.
// O CLIENTE (Quem usa) so enxerga a promessa do contrato
public class Compra {
public void processar(CartaoDeCredito cartao) {
ValidadorFinanceiro validador = new ValidadorFinanceiro();
// Confiamos no contrato: se retornar true, o cartao e valido.
// Nao perguntamos COMO ele valida, apenas o resultado.
if (validador.isValido(cartao)) {
System.out.println("Compra processada com sucesso!");
}
}
}public class ValidadorFinanceiro {
// A INTERFACE publica: simples e direta (O "QUE")
public boolean isValido(CartaoDeCredito c) {
return verificarAlgoritmoLuhn(c.getNumero()) && consultarGatewayExterno(c);
}
// A IMPLEMENTACAO: detalhes privados (O "COMO") — o mundo externo nem sabe que existem
private boolean verificarAlgoritmoLuhn(String numero) { /* ... */ return true; }
private boolean consultarGatewayExterno(CartaoDeCredito c) { /* ... */ return true; }
}Compra é agnóstica quanto à complexidade da validação — não sabe se há consulta a banco de dados, cálculo de checksum, ou chamada de rede. Essa separação permite que ValidadorFinanceiro evolua (passar a usar IA antifraude, por exemplo) sem que Compra precise sequer ser recompilada. A interface funciona como o painel de controle de uma máquina complexa: você opera os botões (o “Quê”), a fiação interna (o “Como”) fica atrás da parede.
4 Encapsulamento e Evolução sem Regressão
A prova real de um bom design aparece quando a regra de negócio muda. Se Produto expõe a intenção, não o dado:
public class Produto {
private double precoCusto;
// A INTERFACE: expressa uma INTENCAO (o que), nao o acesso ao dado (como)
public double calcularPrecoDeVenda() {
double margemLucro = 1.8;
return this.precoCusto * margemLucro;
}
}Carrinho não “pega” o custo para calcular por fora — ele pede o serviço calcularPrecoDeVenda(). Quando a loja decide que produtos acima de R$ 1000 têm margem menor, a mudança fica inteiramente contida:
// VERSAO 2: a regra de negocio ficou mais complexa
public double calcularPrecoDeVenda() {
// a logica mudou drasticamente, mas o contrato (assinatura) e o mesmo!
if (this.precoCusto > 1000.0) return this.precoCusto * 1.75;
return this.precoCusto * 1.90;
}Se tivéssemos usado um getter de precoCusto e feito a conta no Carrinho, seria preciso alterar toda classe que exibe preços. Com o design de caixa preta, só a implementação interna do Produto muda — a assinatura pública é a mesma, então o impacto no resto do sistema é zero.
Carrinho tivesse um getter para precoCusto e calculasse a margem por fora, o que exatamente quebraria quando a regra de margem mudasse?
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:
- □ A interface define o “O Quê” um objeto promete fazer; a implementação é o “Como”, escondido do cliente.
- □ Trocar a implementação interna de um método, mantendo sua assinatura, não deveria exigir mudanças no código cliente.
- □
ItemCarrinho.subtotal()deveria ler diretamente o atributoprecoBasedeProdutopara ser mais eficiente. - □ Encapsular
calcularPrecoDeVenda()dentro deProdutocentraliza a política de margem num único lugar do sistema.
5 A Lei de Demeter
A Lei de Demeter (Princípio do Menor Conhecimento) não é uma lei da natureza — é uma regra de etiqueta: um método de um objeto \(O\) deve invocar apenas métodos do próprio \(O\), de argumentos recebidos, de objetos criados dentro do método, ou de componentes diretos de \(O\). Fale só com amigos próximos, nunca com estranhos.
A violação é visualmente reconhecível pelo “naufrágio de código” (train wreck):
double saldo = pedido.getCliente().getCarteira().getSaldo(); // VIOLACAOO problema não é a quantidade de pontos — é a invasão de fronteiras. É como um caixa de supermercado que, em vez de pedir o pagamento ao cliente, abre a mochila dele, procura a carteira e conta as notas. Se o cliente decidir carregar dinheiro num aplicativo em vez de uma carteira física, o caixa não sabe mais operar. Cada ponto extra na cadeia é uma promessa de que o código vai quebrar quando a estrutura intermediária mudar.
A cura é a delegação em cadeia, guiada por Tell, Don’t Ask (Diga, não Pergunte):
// CORRETO: o Checkout fala apenas com o Pedido (amigo proximo)
if (pedido.clientePodePagar()) { ... }
// Dentro da classe Pedido: delega a decisao para o proximo
public boolean clientePodePagar() {
return this.cliente.temSaldoSuficiente(this.total);
}Checkout agora é cego para a existência de uma Carteira — se o Cliente trocar a carteira por um ApplePay, Checkout permanece intocado. Essa é a solução técnica para o que a literatura chama de Feature Envy (inveja de funcionalidade): uma classe interessada demais nos dados de outra, em vez das suas próprias responsabilidades. A cura é sempre a mesma — mover o comportamento para onde os dados residem.
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:
- □ A Lei de Demeter recomenda que um objeto interaja apenas com seus “amigos próximos”.
- □
pedido.getCliente().getCarteira().getSaldo()é um exemplo saudável de reuso de getters. - □ Feature Envy é o sintoma de uma classe mais interessada nos dados de outra do que nos próprios.
- □ A cura técnica para o naufrágio de código é criar métodos de delegação, como
pedido.clientePodePagar().
6 Programando para Abstrações: Sistemas Plug-and-Play
O estágio final desta aula é programar para o tipo genérico, não para a classe concreta:
public interface Pagavel {
boolean isPagamentoValido(); // a promessa abstrata
}
public class Boleto implements Pagavel {
public boolean isPagamentoValido() { /* logica de codigo de barras */ return true; }
}
public class CartaoDeCredito implements Pagavel {
public boolean isPagamentoValido() { /* comunicacao com operadora */ return true; }
}
public class Checkout {
// FLEXIBILIDADE: aceito qualquer um que assine o contrato!
public void finalizar(Pagavel metodo) {
if (metodo.isPagamentoValido()) System.out.println("Transacao concluida!");
}
}Se amanhã surgir o Pix, basta criar Pix implements Pagavel — Checkout não precisa ser alterado nem recompilado. Compare com o código rígido:
public void processar(CartaoDeCredito cartao) { ... }
processar(meuBoleto); // ERRO DE COMPILACAO: Boleto cannot be converted to CartaoEsse erro de compilação não é só uma falha sintática — é um diagnóstico de acoplamento forte: o sistema exige uma identidade específica em vez de uma capacidade funcional. A cura é elevar o nível de abstração: em vez de pedir um objeto de uma classe específica, pedir um objeto que saiba responder à mensagem certa. A interface é o “padrão USB-C” do software — um encaixe universal, indiferente ao que está do outro lado, desde que o contrato seja respeitado.
Checkout.finalizar recebesse CartaoDeCredito em vez de Pagavel, o que exatamente o erro de compilação de um novo Pix estaria diagnosticando sobre o design?
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:
- □
Checkout.finalizar(Pagavel metodo)aceita qualquer classe futura que implementePagavel, sem recompilação. - □ Um erro de compilação ao tentar passar
Boletoonde se esperaCartaoDeCreditoé sempre culpa do compilador, não do design. - □ Programar para o tipo concreto (
CartaoDeCredito) em vez da interface (Pagavel) aumenta o acoplamento do sistema. - □ Uma interface bem desenhada funciona como um “encaixe universal”, indiferente à identidade concreta de quem a implementa.
7 Conclusão
Voltando às quatro perguntas da abertura:
- Como especialistas colaboram sem intimidade? Delegando: cada objeto pergunta ao outro só o que precisa, nunca acessa o interior dele.
- Interface vs. implementação: o contrato é estável e público; o detalhe é volátil e pode mudar livremente por trás dele.
- Reconhecer a fofoca: qualquer cadeia de
.get().get().get()que atravessa fronteiras de objetos que não são amigos diretos. - Programar para abstrações: o sistema aceita qualquer objeto que cumpra o contrato — e cresce por adição de novas classes, não por modificação das antigas.
Ponte para a Aula 4
Esta aula tratou de como classes diferentes se conectam sem se conhecer intimamente. A Aula 4 volta um nível de abstração para dentro de uma única classe: como saber quando uma classe está fazendo coisas demais, e como decompô-la em unidades menores e mais coesas (o Princípio da Responsabilidade Única).
8 Exercícios
8.1 Questões discursivas
Explique a metáfora “Fabricante de Peças vs. Arquiteto de Sistemas”. Por que a preocupação puramente anatômica (sintaxe,
this,private) é insuficiente para construir sistemas sustentáveis a longo prazo?O código
pedido.getCliente().getEndereco().getCEP()é um “Naufrágio de Código” (train wreck). Explique tecnicamente por que essa linha representa um risco arquitetural, e como um método de delegação resolveria o problema.Explique como a interface atua como um “escudo” para quem usa um objeto, usando a analogia do controle remoto ou do padrão USB-C para justificar por que não é preciso conhecer a “fiação interna” de um colaborador.
8.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).