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).
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).
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).
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).
- ( ) O foco do “arquiteto” está na composição e na rede de colaboradores, não só na sintaxe isolada da classe.
- ( ) Um sistema robusto pode ser visto como um amontoado de classes que expõem seus dados livremente.
- ( ) Projetar por “encaixes” (interfaces) permite ignorar o “como” interno de cada peça no rascunho do sistema.
- ( ) A maturidade de design é atingida ao definir as mensagens entre objetos antes de escrever a lógica interna dos métodos.
- ( ) Um objeto especialista deve, preferencialmente, não conhecer a existência de seu orquestrador.
- ( )
ItemCarrinho.subtotal()deve acessar diretamente o atributoprecoBasedeProduto. - ( )
Carrinho.calcularTotal()usa delegação, sem conhecer a lógica interna deItemCarrinho. - ( ) Dividir responsabilidades entre especialistas evita a criação de “Objetos Deus”.
- ( ) A interface define o “O Quê” um objeto faz, ocultando o “Como”.
- ( ) Alterar a implementação de um método, mantendo sua assinatura, não deveria exigir recompilar o cliente.
- ( ) O acoplamento físico — conhecer as entranhas de outra classe — é uma das maiores causas de fragilidade.
- ( ) Uma interface bem desenhada deve expor o máximo possível da estrutura interna, para facilitar o reuso.
- ( ) Atributos privados garantem que a interface pública seja a única via de interação com o objeto.
- ( ) Ocultar a implementação permite que regras de negócio voláteis mudem sem efeito cascata.
- ( ) Proteger o estado interno é dispensável se o programador garantir que ninguém chamará os métodos errado.
- ( )
calcularPrecoDeVenda()pode mudar de fórmula internamente sem afetar quem já chama esse método.
- ( ) Estabelece que um objeto deve interagir apenas com seus “amigos próximos” (atributos e argumentos diretos).
- ( ) O objetivo é impedir que o conhecimento sobre a hierarquia interna do sistema se espalhe sem controle.
- ( ) “Falar com estranhos” (objetos obtidos navegando por outros objetos) aumenta a resiliência do código.
- ( ) Um método de delegação, como
getCEPDeEntrega(), é uma forma correta de respeitar a lei.
- ( ) É identificado por longas cadeias de chamadas, como
a.getB().getC().getD(). - ( ) O problema real é puramente estético — o excesso de pontos polui a leitura do código.
- ( ) Cada nível de navegação profunda é uma fronteira de objeto invadida.
- ( ) Se a estrutura intermediária (ex.:
Carteira) mudar, o código que a navegou diretamente quebra.
- ( ) Em vez de investigar o saldo do cliente para autorizar a venda, o sistema deve pedir ao
Pedidoque se valide. - ( ) “Perguntar” por dados para decidir por fora é uma prática que fortalece o encapsulamento.
- ( ) Mover a lógica para onde os dados residem evita que classes se tornem “fofoqueiras”.
- ( )
pedido.clientePodePagar()é um exemplo de “Tell”: oPedidodecide internamente como verificar isso.
- ( ) A delegação ocorre quando um objeto repassa uma mensagem a um colaborador, sem expor a identidade dele.
- ( ) Um “orquestrador” como
Carrinhodeve calcular pessoalmente o subtotal de cada item, para garantir precisão. - ( ) Feature Envy é o sintoma de uma classe mais interessada nos dados de outra do que nos próprios.
- ( ) A cura para Feature Envy é mover o método para a classe que de fato detém os dados necessários.
- ( ) Programar para um tipo genérico (
Pagavel) em vez de uma classe concreta (Boleto) aumenta a rigidez. - ( ) O uso de tipos genéricos permite que o sistema aceite novos componentes sem alterar o código já existente.
- ( ) Referenciar objetos por suas interfaces é uma recomendação central para promover flexibilidade.
- ( ) Um
Checkoutque recebePagavelfunciona com qualquer classe futura que implemente esse contrato.
- ( ) Um erro de compilação ao passar
Boletoonde se esperaCartaoDeCreditoé sempre um bug do compilador. - ( ) Esse erro pode revelar que o sistema exige uma identidade específica em vez de uma capacidade funcional.
- ( ) A solução típica é generalizar o parâmetro para uma interface que ambas as classes implementem.
- ( ) Elevar o nível de abstração do parâmetro reduz o acoplamento entre
Checkoute os métodos de pagamento.
- ( ) O design deve ser Transparente, Razoável, Usável e Exemplar (TRUE).
- ( ) Um código é “Razoável” quando o custo de uma mudança é proporcional ao benefício que ela traz.
- ( ) O design de caixa preta aumenta o custo de mudança, pois esconde erros atrás de interfaces.
- ( ) Contratos estáveis e baixo acoplamento evitam que o custo de uma nova funcionalidade cresça exponencialmente.
- ( )
Carrinhoatua como “maestro”: coordena especialistas em vez de fazer o trabalho de cada um. - ( ) A fronteira entre
CarrinhoeItemCarrinhoé respeitada quando oCarrinhosoma subtotais já calculados. - ( ) Se
ItemCarrinhomudar sua fórmula de subtotal,Carrinhoprecisa ser reescrito para acompanhar. - ( ) Um bom orquestrador permanece estável mesmo quando a lógica interna de um especialista evolui.