Aula 12 — Programação Orientada a Objetos
2026-09-18
Aula 11 fechou dois pilares: o dado (Value Object) e a gênese (Builder, Factory Method) do objeto.
Falta o terceiro: o fluxo — como o objeto muda de comportamento ao longo do seu ciclo de vida.
O Pedido ainda decide o que fazer verificando uma String status no topo de cada método — Obsessão por Primitivos aplicada ao comportamento.
A solução, o State, tem uma topologia de classes quase idêntica à do Strategy — o contraste entre os dois é o fio condutor da primeira metade da aula.
Na segunda metade, viramos a câmera para fora: como o sistema conversa com uma biblioteca externa cuja interface não fala a língua do nosso domínio? Entra o Adapter.
Roteiro: o antipadrão do status → a estrutura do State → onde mora a transição → a fronteira externa e o Adapter.
Pedido Ainda Preso ao Statuspublic class Pedido {
private String status = "NOVO"; // Obsessao por Primitivos, de volta
public void pagar() {
if (status.equals("NOVO")) {
processarPagamento();
status = "PAGO";
} else if (status.equals("PAGO")) {
throw new IllegalStateException("Pedido ja esta pago!");
} else if (status.equals("ENVIADO")) {
throw new IllegalStateException("Pedido ja foi enviado!");
}
}
}Cada método começa perguntando “em que status eu estou?” — a lógica de negócio real só aparece depois, aninhada no if certo.
A ordem das chamadas importa (não cancela antes de existir, não paga duas vezes) — mas a regra que garante essa ordem está espalhada em condicionais repetidas.
Fragilidade de evolução: um estado novo (“EM_ANALISE_DE_FRAUDE”) exige abrir pagar(), cancelar() e enviar() — viola o OCP da Aula 10.
Testabilidade: testar “cancelar um pedido pago” exige primeiro montar o pedido no estado certo — o mesmo problema que o Strategy já resolveu para os algoritmos.
Obsessão por Primitivos (Aula 11), de volta: status como String/int não impede "PGO" por engano — nada no tipo garante um valor válido.
O State converte esse controle de fluxo em estrutura de objetos — a mesma virada que a Aula 11 fez para o dado (double → Dinheiro), agora aplicada ao comportamento ao longo do tempo.
Se um novo requisito pedir um estado “EM_ANALISE_DE_FRAUDE” entre “NOVO” e “PAGO” no Pedido do antipadrão acima, quantos métodos já existentes precisam ser abertos e editados — e o que esse número revela sobre a real causa da fragilidade do design?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
Pedido do antipadrão, inserir um novo estado exige editar apenas o método pagar(), já que é o único que decide a transição de “NOVO” para “PAGO”.cancelar(), montar um Pedido cujo campo status já esteja manualmente ajustado para "PAGO".status fosse trocado de String para um enum Java, isso sozinho eliminaria a necessidade de abrir pagar(), cancelar() e enviar() ao adicionar um estado novo.Pedido do antipadrão nasce do fato de a lógica de transição de estado estar fisicamente distribuída dentro de vários métodos que também contêm lógica de negócio não relacionada ao fluxo.Quantos métodos precisam mudar — Resposta
Pedido do antipadrão, inserir um novo estado exige editar apenas o método pagar(), já que é o único que decide a transição de “NOVO” para “PAGO” — falso: cancelar() e enviar() também verificam status e também precisariam de um novo else if para tratar (ou rejeitar) o novo estado.cancelar(), montar um Pedido cujo campo status já esteja manualmente ajustado para "PAGO" — exatamente o custo de testabilidade do antipadrão: o teste depende de reconstruir manualmente um estado interno antes de exercitar o comportamento.status fosse trocado de String para um enum Java, isso sozinho eliminaria a necessidade de abrir pagar(), cancelar() e enviar() ao adicionar um estado novo — falso: um enum elimina o risco de um valor inválido ("PGO"), mas os três métodos continuariam precisando de um novo case/if para cada estado novo; o problema estrutural (lógica de transição espalhada) não desaparece.Pedido do antipadrão nasce do fato de a lógica de transição de estado estar fisicamente distribuída dentro de vários métodos que também contêm lógica de negócio não relacionada ao fluxo — é precisamente essa mistura (transição + regra de negócio no mesmo método) que torna cada edição arriscada.Voltando à pergunta: três métodos (pagar(), cancelar(), enviar()) precisariam de edição — e esse número revela que a causa real não é “faltam mais ifs”, é que a responsabilidade de decidir o fluxo está fisicamente espalhada por toda a classe em vez de concentrada num único lugar. O bloco seguinte mostra onde esse lugar deveria ser.
Em vez de um valor guardado numa variável, cada fase do ciclo de vida vira uma classe polimórfica.
O Pedido (Contexto) deixa de saber “como” reagir — passa a perguntar só ao “quem” ele é agora, delegando ao objeto de estado.
Quando o estado muda, o Contexto aponta para uma instância nova de uma classe diferente — o sistema cresce horizontalmente (novas classes), não verticalmente (novos ifs).
Context: a classe que os clientes usam (Pedido) — mantém a referência ao EstadoAtual.
State (interface): o contrato dos métodos que dependem do estado.
Concrete States: uma classe por fase do ciclo de vida.
public interface EstadoPedido {
void pagar(Pedido pedido);
void cancelar(Pedido pedido);
void enviar(Pedido pedido);
}
public class EstadoNovo implements EstadoPedido {
@Override
public void pagar(Pedido pedido) {
System.out.println("Pagamento processado!");
pedido.setEstado(new EstadoPago());
}
@Override
public void cancelar(Pedido pedido) {
pedido.setEstado(new EstadoCancelado());
}
@Override
public void enviar(Pedido pedido) {
throw new IllegalStateException("Pedido ainda nao foi pago!");
}
}EstadoNovo sabe que, após um pagamento bem-sucedido, o próximo passo é EstadoPago — essa inteligência mora fora do Pedido.
public class Pedido {
private EstadoPedido estadoAtual;
private double valor;
public Pedido(double valor) {
this.valor = valor;
this.estadoAtual = new EstadoNovo();
}
protected void setEstado(EstadoPedido novoEstado) {
this.estadoAtual = novoEstado;
}
public void pagar() { estadoAtual.pagar(this); }
public void cancelar() { estadoAtual.cancelar(this); }
public void enviar() { estadoAtual.enviar(this); }
}setEstado é protected: só as classes de estado (mesmo pacote) disparam transições — o cliente externo não pode saltar de “Novo” direto para “Entregue”.
A complexidade ciclomática do Pedido cai a quase zero — a “inteligência” de fluxo foi inteiramente terceirizada para os estados.
PedidoA transição tracejada em vermelho não é um caminho real — é uma operação que EstadoEnviado rejeita com exceção.
Mesma topologia — Contexto, interface, implementações concretas via polimorfismo. Não é coincidência.
Strategy: um agente externo ao Contexto (o Cliente) decide qual ConcretaEstrategia injetar — o Contexto nunca troca sozinho.
State: o próprio estado ativo decide qual é o próximo, como consequência de uma operação de negócio, e dispara setEstado — ninguém de fora injeta manualmente.
O Cliente nem aparece no diagrama do State — ele nunca decide diretamente qual estado vem a seguir.
Se alguém disser que o State Pattern é apenas “um Strategy com nomes diferentes”, por que essa afirmação, apesar de a topologia de classes ser quase idêntica, erra o ponto central da intenção arquitetural de cada padrão?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
CalculadoraDeFrete que usa Strategy, é o próprio objeto FreteExpresso injetado que decide, sozinho e sem intervenção externa, substituir-se por FreteGratis durante a execução do sistema.Pedido usando State, é fisicamente o objeto EstadoNovo — e não um código cliente externo chamando pedido.setEstrategia(new EstadoPago()) — quem invoca a transição para EstadoPago como consequência de pagar() ter sido chamado.ConcreteState do padrão State fosse instanciado e injetado manualmente por código cliente externo antes de qualquer operação de negócio ocorrer no Contexto, essa prática ainda preservaria integralmente a intenção arquitetural original do State Pattern.Por que “mesma topologia” não é “mesmo padrão” — Resposta
CalculadoraDeFrete que usa Strategy, é o próprio objeto FreteExpresso injetado que decide, sozinho e sem intervenção externa, substituir-se por FreteGratis durante a execução do sistema — falso: no Strategy, a troca de implementação é decidida por um agente externo ao Contexto (o cliente que chama setEstrategia); a estratégia concreta nunca se autossubstitui.Pedido usando State, é fisicamente o objeto EstadoNovo — e não um código cliente externo chamando pedido.setEstrategia(new EstadoPago()) — quem invoca a transição para EstadoPago como consequência de pagar() ter sido chamado — exatamente a auto-mutação que define o State: a transição nasce de dentro do próprio fluxo de negócio, não de uma injeção externa.ConcreteState do padrão State fosse instanciado e injetado manualmente por código cliente externo antes de qualquer operação de negócio ocorrer no Contexto, essa prática ainda preservaria integralmente a intenção arquitetural original do State Pattern — falso: isso reintroduziria o mesmo mecanismo de injeção externa do Strategy, esvaziando o ponto central do State, que é o objeto decidir e mudar sua própria “personalidade” como efeito colateral de sua lógica de negócio.Voltando à pergunta: a afirmação erra porque confunde “mesma forma de classes” com “mesma intenção arquitetural”. O Strategy resolve “como variar um algoritmo escolhido de fora”; o State resolve “como um objeto muda de comportamento como consequência do seu próprio ciclo de vida”. A topologia é parecida porque ambos usam polimorfismo para eliminar condicionais — mas o gatilho da troca mora em lugares opostos.
Opção A — nos Estados: cada estado conhece seu sucessor. Fluido, mas acopla os estados entre si.
Opção B — no Contexto: uma tabela central de transições.
A Opção B isola os estados entre si, mas reintroduz condicional no Contexto — exatamente o que o State existe para eliminar.
Use transições nos estados para fluxos de negócio naturais e estáveis — o acoplamento EstadoNovo → EstadoPago é aceitável porque essa sequência já é regra de negócio, não variação livre.
Técnica avançada: o método de estado retorna o próximo estado (this.estado = estado.proximo();) — reduz o acoplamento sem reintroduzir condicional no Contexto.
EstadoPagopublic class EstadoPago implements EstadoPedido {
@Override
public void pagar(Pedido pedido) {
throw new IllegalStateException("Pedido ja esta pago!");
}
@Override
public void cancelar(Pedido pedido) {
realizarEstornoFinanceiro(pedido);
pedido.setEstado(new EstadoCancelado());
}
@Override
public void enviar(Pedido pedido) {
pedido.setEstado(new EstadoEnviado());
}
}Em EstadoPago, cancelar() desencadeia um estorno financeiro antes de mudar de estado — regra de negócio embutida na própria classe.
EstadoEnviadoEm EstadoEnviado, cancelar() é proibido — protege a regra de que um produto em trânsito não pode ser simplesmente cancelado.
Sem um único if (status == ...): o código é auto-documentado — para saber o que acontece com um pedido pago cancelado, basta abrir EstadoPago.
Depois de State: o Pedido está protegido por dentro (comportamento, gênese, fluxo). Falta a fronteira que aponta para fora.
Cenário: toda transição de estado precisa ser registrada num sistema de auditoria — mas é uma biblioteca legada, de outra equipe, com uma interface incompatível.
O dilema: não posso mudar a biblioteca (fechada); não devo mudar meu domínio para se moldar a ela (destrói a arquitetura já construída).
Entra o Adapter — a fronteira arquitetural aponta agora para fora, não mais para dentro do objeto.
O cliente chama uma interface que já conhece e confia; o Adapter implementa essa interface, mas guarda por dentro uma referência ao objeto incompatível (o Adaptee).
“Adapter permite que classes trabalhem juntas mesmo que não pudessem de outra forma, por causa de interfaces incompatíveis.”
public interface LoggerModerno {
void info(String mensagem);
}
public class LogServicoLegado {
public void registrarLogNoArquivo(String msg, int severidade) {
System.out.println("[" + severidade + "] " + msg);
}
}
public class LogAdapter implements LoggerModerno {
private LogServicoLegado legado;
public LogAdapter(LogServicoLegado legado) { this.legado = legado; }
@Override
public void info(String mensagem) {
legado.registrarLogNoArquivo(mensagem, 1);
}
}Sem o Adapter, cada classe de negócio precisaria conhecer o número mágico 1 e o nome registrarLogNoArquivo — o LogAdapter centraliza essa tradução.
Se a equipe de compliance trocar a biblioteca legada amanhã, só LogAdapter muda — nenhuma classe de estado do Pedido sofre impacto.
EstadoPago registra o evento sem jamais conhecer LogServicoLegado — duas fronteiras (State para dentro, Adapter para fora) cooperando na mesma classe.
Class Adapter: acoplamento estático — conhece as entranhas do legado; se o fornecedor lançar uma nova subclasse, não consegue reaproveitá-la.
Object Adapter (o LogAdapter de antes): trata o legado como caixa preta injetada — funciona com qualquer subclasse presente ou futura, sem alteração.
Regra de ouro: prefira sempre Object Adapter para manter as caixas pretas fechadas.
Se o LogAdapter decidisse estender (extends) LogServicoLegado em vez de compô-lo por injeção, que capacidade futura o sistema perderia especificamente quando o fornecedor lançasse uma nova subclasse de LogServicoLegado?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
LogAdapter construído por composição (Object Adapter) continua funcionando corretamente mesmo que o objeto injetado no construtor seja, na verdade, uma subclasse de LogServicoLegado lançada pelo fornecedor depois que o LogAdapter já havia sido escrito.LogAdapter que estendesse LogServicoLegado (Class Adapter) precisaria ser reescrito ou duplicado para conseguir adaptar especificamente uma nova subclasse do legado, mesmo que essa subclasse só adicionasse um método novo sem alterar registrarLogNoArquivo.LogServicoLegado legado por uma injeção via construtor no LogAdapter (em vez de herança) é o que permite substituir, em um teste automatizado, o legado real por uma versão simulada (mock) sem alterar o código do LogAdapter.LogServicoLegado expusesse um método protected usado internamente por suas próprias subclasses, um LogAdapter implementado como Object Adapter teria acesso direto a esse método da mesma forma que um Class Adapter teria.O que se perde ao herdar em vez de compor — Resposta
LogAdapter construído por composição (Object Adapter) continua funcionando corretamente mesmo que o objeto injetado no construtor seja, na verdade, uma subclasse de LogServicoLegado lançada pelo fornecedor depois que o LogAdapter já havia sido escrito — exatamente a flexibilidade da composição: o Adapter trata o legado (e qualquer subclasse dele) como uma caixa preta, via polimorfismo.LogAdapter que estendesse LogServicoLegado (Class Adapter) precisaria ser reescrito ou duplicado para conseguir adaptar especificamente uma nova subclasse do legado, mesmo que essa subclasse só adicionasse um método novo sem alterar registrarLogNoArquivo — o extends prende o Adapter a uma única classe-mãe fixa em tempo de compilação; uma nova subclasse do fornecedor exigiria um novo Class Adapter dedicado.LogServicoLegado legado por uma injeção via construtor no LogAdapter (em vez de herança) é o que permite substituir, em um teste automatizado, o legado real por uma versão simulada (mock) sem alterar o código do LogAdapter — a mesma vantagem de testabilidade já vista com Strategy e Factory Method: composição habilita substituição em tempo de execução.LogServicoLegado expusesse um método protected usado internamente por suas próprias subclasses, um LogAdapter implementado como Object Adapter teria acesso direto a esse método da mesma forma que um Class Adapter teria — falso: só quem herda (extends) enxerga membros protected da classe-mãe; um Object Adapter só acessa a API pública do objeto que compõe, o que é exatamente o ponto — ele nunca vê as entranhas do legado.Voltando à pergunta: o sistema perderia a capacidade de reaproveitar automaticamente a nova subclasse — o Object Adapter continua funcionando sem alteração (a subclasse ainda é um LogServicoLegado válido); o Class Adapter, preso via extends à classe original, exigiria escrever um segundo Adapter dedicado só para a subclasse nova.
String status verificada em cada método é Obsessão por Primitivos aplicada ao comportamento — fragiliza a evolução e explode a complexidade de testePróxima aula: Decorator (composição dinâmica de responsabilidades) e Observer (desacoplamento de eventos um-para-muitos) — as duas peças que faltam antes da síntese final do curso.
UNICAMP — Instituto de Computação