Aula 11 — Programação Orientada a Objetos
2026-09-18
Aula 10 resolveu como o objeto se comporta — o padrão Strategy trocou o if/else de pagamento por uma interface injetável.
O Strategy não foi um golpe de sorte: é um representante de uma família de 23 soluções catalogadas e nomeadas.
Hoje resolvemos como o dado é protegido e como o objeto nasce em segurança — problemas que o Strategy não tocou.
Roteiro: o vocabulário dos padrões → o perigo da mutabilidade → o Value Object → construção segura com Builder → Factory Method e DIP.
Anos 1970: Christopher Alexander, A Pattern Language — catalogou topologias urbanas/arquitetônicas recorrentes.
Engenharia civil não reinventa o formato de uma porta a cada casa; engenharia de software não deveria reinventar como orquestrar objetos dependentes a cada sistema.
Gamma, Helm, Johnson e Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software.
23 padrões catalogados, ainda hoje o vocabulário básico da arquitetura moderna.
Quase todos se apoiam em só dois pilares: Composição e Polimorfismo — ambos já dominados por vocês.
Solução arquitetural testada e documentada para um problema recorrente.
Não é código para copiar e colar — é um gabarito conceitual: as peças, as responsabilidades, a topologia das mensagens.
Não são frameworks: um framework chama seu código; o padrão é a organização que você dá ao seu próprio código.
Não são algoritmos estruturais: não ordenam nem buscam — resolvem acoplamento e coesão.
Não curam código procedural: pressupõem encapsulamento já estabelecido; aplicados sobre variáveis globais, só mascaram o problema.
Em vez de “crie uma interface comum, isole a execução, injete no construtor do orquestrador”…
…um engenheiro sênior diz apenas: “implemente um Strategy aqui.”
Padrões condensam horas de debate estrutural numa única palavra — a ferramenta de escala dos arquitetos.
Comportamento = comunicação no tempo (Strategy, State); Estrutural = topologia (Adapter, Decorator); Criação = gênese (Value Object, Builder, Factory Method).
Fail-Fast e invariantes são vitais (Aula 2) — mas quem executa essa validação complexa?
Espalhar new pelo sistema acopla negócio a alocação de memória e a minúcias de inicialização.
Objetivo criacional: o sistema pede um objeto, a fábrica entrega pronto e validado — é isso que o resto desta aula constrói.
Se um colega descrever uma solução como “uma classe que traduz chamadas de uma biblioteca externa incompatível para o formato que meu sistema espera”, a que família de padrões essa solução pertence — e por que classificá-la pela família certa importa mais do que decorar o nome “Adapter”?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
Classificando pela família certa — Resposta
Voltando à pergunta: a solução descrita é um Adapter, família Estrutural. Classificar pela família importa mais do que o nome porque o propósito (harmonizar topologia) é o que orienta a decisão de projeto — o nome é só a etiqueta que vem depois, útil para comunicação rápida entre engenheiros que já concordam sobre o propósito.
Efeitos colaterais: alterar um atributo aqui reverbera em comportamento inesperado ali.
Acoplamento oculto: objetos que compartilham referência para o mesmo dado mutável na Heap.
Se A e B referenciam o mesmo C, e A altera C, B pode assumir (erradamente) que C não mudou — o aliasing bug.
Retangulor2 = r1 não copia dados — copia o endereço. Um único objeto na Heap, duas portas de entrada na Stack.
O compilador não avisa nada: do ponto de vista da linguagem, nada de errado aconteceu.
Clonar (new Retangulo(r1)) antes de compartilhar evita a corrupção — mas tem custo real.
Sobrecarrega o Garbage Collector; polui o código com lógica de clonagem em toda fronteira.
Trata o sintoma (uma cópia por vez), não a causa (o objeto ainda pode ser mutado por qualquer um).
Se o objeto não permite mutação, a única forma de obter um valor diferente é criar uma instância nova — a antiga fica sempre intocada.
Identidade por ID (RA), não por atributo. É natural que o resto mude — a identidade não.
Dois objetos são a mesma entidade se têm o mesmo ID, mesmo com todos os outros campos diferentes.
Identidade por atributo: sem ID — o objeto é o conjunto dos seus valores.
Imutabilidade absoluta: uma vez criado, o estado nunca muda.
Substituibilidade: iguais se o conteúdo for idêntico (equals() por campo, nunca por referência).
Semântica rica: Dinheiro no lugar de double — o tipo já carrega o significado de domínio.
Dinheiro: um Value Object Completopublic final class Dinheiro {
private final double quantia;
private final String moeda;
public Dinheiro(double quantia, String moeda) {
if (quantia < 0) throw new IllegalArgumentException("Quantia negativa");
this.quantia = quantia;
this.moeda = moeda;
}
public Dinheiro somar(double delta) {
return new Dinheiro(this.quantia + delta, this.moeda);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Dinheiro d)) return false;
return Double.compare(d.quantia, quantia) == 0 && moeda.equals(d.moeda);
}
}pedido.getTotal().somar(desconto) nunca corrompe o total antigo — sempre devolve instância nova.
equals()/hashCode() seguem o mesmo contrato da Aula 2 — agora comparando por valor, não por identidade.
Campos final: referência/valor não reatribuído após o construtor.
Classe final: impede que uma subclasse adicione estado mutável por trás do contrato.
Ausência de setters: caixa preta inalterável após a gênese.
Defesa na cópia: referências internas a objetos mutáveis (Date, List) precisam ser clonadas — senão o mutável “vaza”.
Confiar em double/int/String para Dinheiro, CPF, Email — o tipo diz como é guardado, não o que significa.
O compilador aceita qualquer valor: int de idade aceita -500 sem reclamar.
Validação migra para o construtor (Fail-Fast) — se o objeto Email existe, ele já é válido em qualquer camada.
Thread-safety inerente: sem escrita, não há condição de corrida nem necessidade de locks.
Raciocínio local: ler um método que recebe um VO não exige investigar mutação “à distância”.
Otimização via Flyweight: reutilizar a mesma instância de um valor comum sem risco de corrupção.
Se um sistema de biblioteca representasse cada exemplar físico de um livro como um Value Object (sem ID, igualdade por atributo), o que quebraria no momento em que dois exemplares idênticos do mesmo título precisassem ser rastreados separadamente (um emprestado, outro na prateleira)?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
equals() incapaz de distinguir qual exemplar específico está emprestado e qual está na prateleira.Dinheiro de R$ 50 criado numa parte do sistema é sempre equals() a outro Dinheiro de R$ 50 criado em outra parte completamente diferente do sistema, mesmo sem nenhuma relação de criação entre os dois.Aluno permitisse alterar o valor do campo ra através de um método público após a construção, isso não comprometeria a modelagem de Aluno como Entidade, desde que curso continuasse mutável.O que quebra sem identidade — Resposta
equals() incapaz de distinguir qual exemplar específico está emprestado e qual está na prateleira — é exatamente o preço de trocar identidade por valor: dois VOs com o mesmo conteúdo são, por definição, indistinguíveis.Dinheiro de R$ 50 criado numa parte do sistema é sempre equals() a outro Dinheiro de R$ 50 criado em outra parte completamente diferente do sistema, mesmo sem nenhuma relação de criação entre os dois — é exatamente a substituibilidade por valor: a igualdade depende só do conteúdo, nunca da origem.Aluno permitisse alterar o valor do campo ra através de um método público após a construção, isso não comprometeria a modelagem de Aluno como Entidade, desde que curso continuasse mutável — comprometeria sim: a identidade persistente (o RA) é exatamente o que não pode mudar numa Entidade; permitir sua alteração quebra a garantia central do padrão, independentemente do que acontece com os outros campos.Voltando à pergunta: o rastreio individual de exemplares quebraria — equals() por valor confunde “dois livros com o mesmo conteúdo” com “o mesmo exemplar físico”. A correção é dar a cada exemplar um ID (ex.: número de patrimônio), modelando-o como Entidade; o título do livro (metadados: nome, autor, ISBN), esse sim, pode continuar sendo um Value Object compartilhado por todos os exemplares.
Se algo deve falhar, que falhe o mais cedo possível — o construtor como “segurança” (Aula 2, revisitado).
Lista longa: ordem de argumentos do mesmo tipo vira aposta.
Estado inconsistente: objeto vazio preenchido depois via setters — nasce “mancando”.
Acoplamento rígido: new espalhado vincula negócio à classe concreta.
Objeto intermediário que acumula dados e só ao final produz a instância alvo.
Ideal para objetos imutáveis com muitos parâmetros — legibilidade e segurança sem sacrificar a imutabilidade.
Cada método retorna this — o encadeamento é só açúcar sintático sobre “configure e devolva-se”.
PedidoBuilder e Pedidopublic final class Pedido {
private final String cliente;
private final double valorTotal;
private final boolean urgente;
// Construtor privado: so o Builder tem permissao para instanciar
private Pedido(PedidoBuilder builder) {
this.cliente = builder.getCliente();
this.valorTotal = builder.getTotal() - builder.getDesconto();
this.urgente = builder.isUrgente();
}
}public class PedidoBuilder {
private String cliente;
private double total;
private double desconto;
private boolean urgente = false;
public PedidoBuilder paraCliente(String nome) { this.cliente = nome; return this; }
public PedidoBuilder comItem(String nome, double preco) { this.total += preco; return this; }
public PedidoBuilder comDesconto(double valor) { this.desconto = valor; return this; }
public PedidoBuilder urgente() { this.urgente = true; return this; }
public Pedido build() {
if (cliente == null || cliente.isBlank())
throw new IllegalStateException("Pedido deve ter um cliente.");
if (desconto > total)
throw new IllegalStateException("Desconto maior que o valor total!");
return new Pedido(this);
}
}Construtor de Pedido privado: nenhum código externo instancia sem passar pelo Builder — nem um Pedido “meio pronto” sobrevive.
build() é o portão único de validação cross-field — nem PedidoBuilder, nem Pedido isoladamente conseguiriam checar “desconto vs. total” sozinhos.
O cliente não deveria precisar saber qual classe concreta instanciar.
Interface de criação define o método; a lógica de decisão fica confinada à fábrica.
public class NotificacaoPedidoFactory {
public static NotificacaoPedido criar(String meio) {
return switch (meio.toUpperCase()) {
case "SMS" -> new NotificacaoPedidoSMS();
case "EMAIL" -> new NotificacaoPedidoEmail();
default -> throw new IllegalArgumentException("Meio desconhecido: " + meio);
};
}
}O retorno é sempre a interface NotificacaoPedido — nunca a classe concreta.
Ponto único de manutenção: WhatsApp novo? Só a fábrica muda, não as dezenas de pontos de disparo.
Encapsulamento de infraestrutura: credenciais SMTP, chaves de API — o cliente nunca as vê.
Polimorfismo real: o cliente interage só com o contrato NotificacaoPedido.
new como a Cola Mais Forte do CódigoQuem instancia uma classe concreta passa a depender dela — testar sem banco real, ou trocar de fornecedor, exige editar a lógica de negócio.
Contrato é o rei; a fábrica é injetada; “qual classe instanciar” migra para a periferia do sistema.
Mesma lógica de fronteira do Pagavel na Aula 10 — agora aplicada à criação do colaborador, não ao seu comportamento.
Se um novo requisito pedir notificação por WhatsApp, por que a resposta correta nunca deveria envolver abrir e editar os 50 pontos do sistema que hoje disparam NotificacaoPedidoFactory.criar(...)?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
ServicoDePedidos chamasse diretamente new NotificacaoPedidoEmail() em vez de pedir a NotificacaoPedidoFactory, adicionar um novo meio de notificação ainda seria possível sem alterar nenhum código que já dispara notificações.RelatorioPDF ou RelatorioExcel a partir de uma preferência do usuário, centralizando essa decisão numa única classe fábrica, aplica a mesma lógica estrutural do Factory Method visto em aula.RepositorioPedidosFactory sempre devolvesse a mesma implementação concreta (RepositorioPedidosMySQL), injetar essa fábrica em vez de instanciar diretamente RepositorioPedidosMySQL não traria nenhum benefício arquitetural, mesmo pensando em testes automatizados futuros.ServicoDePedidos que recebe uma RepositorioPedidosFactory no construtor, mas depois usa instanceof RepositorioPedidosMySQL para decidir se aplica uma otimização específica daquele banco, preserva integralmente a Inversão de Dependência que a injeção da fábrica pretendia estabelecer.Por que não editar os 50 pontos de disparo — Resposta
ServicoDePedidos chamasse diretamente new NotificacaoPedidoEmail() em vez de pedir a NotificacaoPedidoFactory, adicionar um novo meio de notificação ainda seria possível sem alterar nenhum código que já dispara notificações — falso: sem a fábrica, ServicoDePedidos está acoplado à classe concreta NotificacaoPedidoEmail; um novo meio exigiria editar todo ponto que faz essa instanciação direta.RelatorioPDF ou RelatorioExcel a partir de uma preferência do usuário, centralizando essa decisão numa única classe fábrica, aplica a mesma lógica estrutural do Factory Method visto em aula — mesmo padrão, domínio diferente.RepositorioPedidosFactory sempre devolvesse a mesma implementação concreta (RepositorioPedidosMySQL), injetar essa fábrica em vez de instanciar diretamente RepositorioPedidosMySQL não traria nenhum benefício arquitetural, mesmo pensando em testes automatizados futuros — falso: mesmo com uma única implementação hoje, a injeção já permite substituir por um mock em teste sem tocar ServicoDePedidos, o que new direto jamais permitiria.ServicoDePedidos que recebe uma RepositorioPedidosFactory no construtor, mas depois usa instanceof RepositorioPedidosMySQL para decidir se aplica uma otimização específica daquele banco, preserva integralmente a Inversão de Dependência que a injeção da fábrica pretendia estabelecer — não preserva: o instanceof reintroduz conhecimento sobre a classe concreta dentro do módulo de alto nível, exatamente o acoplamento que a injeção da fábrica deveria eliminar.Voltando à pergunta: porque nenhum dos 50 pontos conhece a classe concreta — todos conversam só com NotificacaoPedido através da fábrica. Uma nova classe NotificacaoPedidoWhatsApp mais uma linha no switch da fábrica bastam; nenhum código cliente precisa mudar, e nenhum risco de quebrar SMS ou Email no processo.
build() antes de liberar um produto imutávelPróxima aula: o terceiro pilar da estabilidade — o padrão State, contrastado deliberadamente com o Strategy já dominado.
UNICAMP — Instituto de Computação