Soluções — O Padrão State e o Padrão Adapter: o Fluxo Seguro do Objeto e a Fronteira Externa

Aula 12 — Programação Orientada a Objetos

Autor

Marcos M. Raimundo — Instituto de Computação, UNICAMP

Aula Exercícios

NotaTeste 1 — O Antipadrão do Status como Primitivo
  • □ Um método que verifica if (status.equals("PAGO")) como primeira instrução antes de executar qualquer lógica de negócio caracteriza o mesmo tipo de acoplamento temporal descrito para o antipadrão do Pedido, independentemente de o valor de status ser uma String ou um enum.
  • □ Um sistema de emissão de boletos que usa um campo int codigoSituacao (0 = emitido, 1 = pago, 2 = vencido) verificado em cinco métodos diferentes da mesma classe está estruturalmente exposto ao mesmo padrão de fragilidade de evolução descrito para o Pedido.
  • □ Se um novo estado for adicionado a um sistema que usa o antipadrão de status-como-primitivo, o compilador Java emitirá um erro de compilação em todo método que não trate explicitamente esse novo estado.
  • □ Testar isoladamente a transição “de PAGO para CANCELADO dispara estorno” num sistema que usa o antipadrão de status exige reconstruir manualmente o estado interno do objeto antes de invocar o método sob teste, ao contrário do que ocorreria com uma implementação via State Pattern.
Dica(Resposta) Teste 1 — O Antipadrão do Status como Primitivo
  • ✔ Verdadeiro — O acoplamento temporal (a obrigação de reconstruir corretamente o estado interno antes de invocar um método) nasce de o método verificar sua própria pré-condição via um campo de estado — isso é independente do tipo desse campo. Trocar String por enum só elimina valores inválidos, não a estrutura do problema.
  • ✔ Verdadeiro — É o mesmo antipadrão transferido para outro domínio: a lógica de transição de estado espalhada por vários métodos da mesma classe, em vez de concentrada num único lugar, é o que causa a fragilidade — não importa se o campo se chama status ou codigoSituacao, nem se é String ou int.
  • ✗ Falso — Um campo String/int verificado por cadeias de if/else não oferece nenhuma garantia de exaustividade em tempo de compilação — um método que simplesmente não considera o novo estado compila normalmente e falha silenciosamente em tempo de execução. É exatamente essa ausência de rede de segurança do compilador que agrava a fragilidade do antipadrão.
  • ✔ Verdadeiro — No antipadrão, só existe uma classe Pedido; testar o comportamento de “pago” exige montar um Pedido inteiro com status="PAGO" antes de chamar cancelar(). Com State, EstadoPago.cancelar(pedido) pode ser exercitado isolando a classe de estado diretamente, sem reconstruir o ciclo de vida completo do objeto.
NotaTeste 2 — Estrutura do Padrão State (Context/State/ConcreteState)
  • □ No padrão State, o método responsável por trocar a referência de estado no Contexto (setEstado) deveria ser público, para que qualquer código cliente externo pudesse forçar uma transição arbitrária sempre que necessário.
  • □ Se EstadoNovo.enviar() lançasse uma exceção informando que o pedido ainda não foi pago, essa exceção estaria expressando uma regra de negócio genuína do domínio, e não um defeito de implementação do padrão State.
  • □ Depois de aplicado o State Pattern, a complexidade ciclomática da classe Pedido aumenta em relação à versão com if/else, porque agora ela precisa gerenciar referências para múltiplas classes de estado diferentes.
  • □ Um Pedido que armazena EstadoPedido estadoAtual e delega pagar() para estadoAtual.pagar(this) permite que o comportamento de pagar() mude completamente ao longo da vida do objeto sem que o código-fonte do método pagar() no Pedido seja alterado uma única vez.
Dica(Resposta) Teste 2 — Estrutura do Padrão State (Context/State/ConcreteState)
  • ✗ Falso — Deixar setEstado livremente público para qualquer cliente externo reintroduz exatamente o mecanismo de injeção externa que caracteriza o Strategy, esvaziando a intenção do State — que é o próprio fluxo de negócio, através dos ConcreteState, decidir e executar suas próprias transições.
  • ✔ Verdadeiro — É a mesma lógica de Fail-Fast/Design by Contract já estudada: uma pré-condição de negócio (“não se envia o que não foi pago”) sendo enforced no ponto exato onde é violada. A exceção é o padrão funcionando como esperado, não um sintoma de mau design.
  • ✗ Falso — O efeito é o oposto: a complexidade ciclomática de Pedido cai, porque as cadeias condicionais somem dos seus métodos — Pedido passa a apenas delegar (estadoAtual.pagar(this)). A complexidade não desaparece do sistema, mas se distribui entre classes pequenas e coesas, cada uma com um único caminho de execução.
  • ✔ Verdadeiro — É o Open/Closed Principle em ação via polimorfismo: o comportamento efetivo de pagar() muda conforme estadoAtual muda de referência, mas o código-fonte do método pagar() do Pedido — a linha estadoAtual.pagar(this) — nunca precisa ser reaberto.
NotaTeste 3 — Strategy vs. State: Mesma Topologia, Intenções Diferentes
  • □ Um sistema de checkout onde o cliente escolhe explicitamente, no carrinho, qual EstrategiaFrete usar, e onde o Pedido decide sozinho, como consequência de um pagamento bem-sucedido, transitar de EstadoNovo para EstadoPago, está aplicando Strategy e State no mesmo sistema para dois problemas arquiteturais distintos.
  • □ Se um engenheiro implementasse o State de forma que um código cliente externo chamasse pedido.forcarEstado(new EstadoPago()) livremente a qualquer momento, essa implementação preservaria a intenção arquitetural original do padrão da mesma forma que a implementação vista em aula.
  • □ A razão pela qual Strategy e State não são o mesmo padrão sob nomes diferentes está exclusivamente no número de métodos que a interface de cada um declara, e não em quem decide a troca de implementação.
  • □ Um ConcreteState que, ao processar uma operação de negócio, decide autonomamente instanciar e ativar o próximo estado da máquina está exercendo o mesmo papel decisório que, no Strategy, pertence ao código cliente que injeta a estratégia no Contexto.
Dica(Resposta) Teste 3 — Strategy vs. State: Mesma Topologia, Intenções Diferentes
  • ✔ Verdadeiro — O critério que distingue os dois padrões — quem decide a troca de implementação, e quando — está presente de forma limpa nesse cenário: escolha externa e explícita (Strategy) para o frete, auto-transição interna como efeito colateral de negócio (State) para o ciclo de vida do pedido. Nada impede os dois coexistirem no mesmo sistema, para problemas diferentes.
  • ✗ Falso — Essa implementação reintroduz o mecanismo de injeção externa característico do Strategy, esvaziando o ponto central do State: o objeto decidir e mudar sua própria “personalidade” como efeito colateral do seu próprio fluxo de negócio, não por ordem externa arbitrária.
  • ✗ Falso — É exatamente o oposto: a estrutura de classes (interface + implementações concretas) é necessária, mas não suficiente, para definir o padrão. O que realmente diferencia Strategy de State é quem decide qual implementação está ativa e quando — não a forma sintática da interface.
  • ✔ Verdadeiro — É a inversão exata do lugar da decisão entre os dois padrões: no Strategy, o cliente externo decide e injeta; no State, o próprio ConcreteState assume esse papel decisório internamente, como consequência do processamento de negócio.
NotaTeste 4 — Onde Colocar a Lógica de Transição
  • □ Centralizar a decisão de qual é o próximo estado dentro de um método do Contexto (Opção B) elimina completamente a necessidade de estruturas condicionais em qualquer parte do sistema.
  • □ Fazer com que EstadoNovo conheça diretamente a classe EstadoPago (Opção A) introduz um acoplamento entre classes de estado que é aceitável quando a sequência de transições já reflete uma regra de negócio estável e conhecida.
  • □ Uma técnica em que o método de transição retorna o próximo estado (this.estado = estado.proximo();) permite ao Contexto atualizar sua referência sem conter uma cadeia de if/else comparando o tipo do estado atual.
  • □ Se a ordem de transições de um sistema mudasse com muita frequência por decisão de negócio (por exemplo, um fluxo de aprovação configurável por cliente), a Opção A (transição fixa nos estados) seria estruturalmente mais adequada do que uma tabela de transições configurável mantida fora das classes de estado.
Dica(Resposta) Teste 4 — Onde Colocar a Lógica de Transição
  • ✗ Falso — Centralizar a decisão no Contexto não elimina condicionais — apenas as move para um único lugar (o método do Contexto que decide o próximo estado), em vez de removê-las do sistema por completo.
  • ✔ Verdadeiro — Todo design é uma troca: acoplar EstadoNovo a EstadoPago diretamente custa flexibilidade, mas ganha simplicidade — uma troca aceitável quando a sequência de transições é uma regra de negócio fixa e já conhecida, não algo que mude com frequência.
  • ✔ Verdadeiro — Delegar ao próprio estado a responsabilidade de determinar (e retornar) o próximo estado é o que permite ao Contexto ficar com uma única linha de atribuição polimórfica, sem precisar perguntar “que tipo de estado é esse?” através de uma cadeia condicional.
  • ✗ Falso — É o oposto: quando a sequência de transições muda com frequência por decisão de negócio, fixá-la no código-fonte das classes de estado (Opção A) exige reabrir e recompilar classes a cada mudança — uma tabela de transições configurável, mantida fora do código, se adapta sem exigir nova compilação.
NotaTeste 5 — O Ciclo de Vida Robusto e o Polimorfismo em Ação
  • □ O fato de EstadoPago.cancelar() chamar realizarEstornoFinanceiro() antes de mudar o estado, enquanto EstadoNovo.cancelar() apenas muda o estado sem estornar nada, é uma expressão direta do polimorfismo substituindo uma cadeia de if (status == ...).
  • □ Se EstadoEnviado.cancelar() lança uma exceção proibindo o cancelamento, isso significa que o padrão State, ao contrário do antipadrão original, não é capaz de expressar a regra de negócio “produtos em trânsito não podem ser cancelados”.
  • □ Um desenvolvedor que precisa entender o que acontece quando um pedido pago é cancelado, ao usar o State Pattern, encontra essa resposta isolada dentro da classe EstadoPago, sem precisar ler os outros estados.
  • □ Adicionar um novo estado “EM_ANALISE_DE_FRAUDE” ao sistema baseado em State Pattern exige alterar o código-fonte de EstadoNovo, EstadoPago e EstadoEnviado da mesma forma que exigiria no antipadrão original baseado em String status.
Dica(Resposta) Teste 5 — O Ciclo de Vida Robusto e o Polimorfismo em Ação
  • ✔ Verdadeiro — Cada ConcreteState implementa cancelar() com o comportamento específico que faz sentido para aquele estado — sem que nenhum código precise perguntar “qual é o status atual?” antes de decidir o que fazer. Isso é exatamente o papel do polimorfismo substituindo condicionais.
  • ✗ Falso — É exatamente o oposto: lançar a exceção é a forma como o State expressa essa regra de negócio, de forma isolada e explícita dentro de EstadoEnviado, sem misturar essa lógica com o comportamento de nenhum outro estado.
  • ✔ Verdadeiro — Essa localidade de leitura é uma das vantagens centrais do State sobre o antipadrão: o comportamento de “cancelar quando pago” mora inteiramente em EstadoPago.cancelar(), sem exigir que o leitor percorra um método monolítico com ramos para todos os estados possíveis.
  • ✗ Falso — No antipadrão, os três métodos do Pedido monolítico precisam ser reabertos igualmente. Com State, só as classes diretamente envolvidas na transição para/a partir do novo estado precisam mudar (tipicamente só EstadoNovo, que passaria a poder transitar para o novo estado) — estados sem relação direta, como EstadoEnviado, podem permanecer intocados.
NotaTeste 6 — O Dilema da Incompatibilidade e o Papel do Adapter
  • □ O padrão Adapter resolve especificamente o problema de uma interface externa incompatível com o contrato que o domínio já usa, sem exigir alteração nem do código do fornecedor externo, nem do código cliente que já confia na interface esperada.
  • □ Se a equipe decidisse modificar diretamente as classes de negócio centrais para chamar os métodos nativos do sistema legado de auditoria, essa prática produziria o mesmo nível de isolamento arquitetural que o uso de um Adapter produziria.
  • □ No LogAdapter, o parâmetro de severidade fixo (1) passado para registrarLogNoArquivo é um detalhe de tradução que o Adapter absorve, evitando que ele precise ser conhecido por todas as classes de negócio que disparam eventos de log.
  • □ Um Adapter, por definição do padrão, deve alterar o comportamento interno de pelo menos uma das duas interfaces que ele está harmonizando para que a tradução funcione corretamente.
Dica(Resposta) Teste 6 — O Dilema da Incompatibilidade e o Papel do Adapter
  • ✔ Verdadeiro — É a definição central do padrão: o Adapter absorve a tradução numa terceira classe nova, preservando intacto tanto o código do fornecedor (muitas vezes nem sequer disponível para alteração) quanto o código cliente já escrito contra a interface esperada.
  • ✗ Falso — Modificar as classes de negócio para chamar diretamente a API do legado contamina o modelo de domínio com detalhes de infraestrutura voláteis — exatamente o oposto do isolamento que o Adapter produz ao concentrar essa dependência numa única classe de fronteira.
  • ✔ Verdadeiro — Detalhes de tradução como códigos numéricos fixos exigidos pela API legada são exatamente o tipo de “número mágico” que o Adapter encapsula, evitando que se espalhe por todas as classes de negócio que precisam registrar eventos.
  • ✗ Falso — O Adapter nunca altera o comportamento interno de nenhum dos dois lados — ele só traduz chamadas na fronteira entre eles, delegando para a implementação real depois de adaptar a assinatura/formato da chamada.
NotaTeste 7 — Object Adapter vs. Class Adapter
  • □ Um LogAdapter implementado como Object Adapter continua funcionando sem nenhuma alteração de código se o objeto LogServicoLegado injetado no construtor for substituído por uma subclasse dele lançada pelo fornecedor após o Adapter já estar em produção.
  • □ Um LogAdapter implementado como Class Adapter (via extends LogServicoLegado) consegue adaptar automaticamente qualquer subclasse futura do legado, porque a herança propaga essa capacidade para toda a árvore de classes.
  • □ A principal razão pela qual a indústria prefere o Object Adapter é estética — o código fica mais curto — e não uma diferença estrutural de acoplamento entre as duas abordagens.
  • □ Substituir a composição (private LogServicoLegado legado) por herança (extends LogServicoLegado) no LogAdapter tornaria mais fácil, e não mais difícil, substituir o legado real por um objeto simulado (mock) em um teste automatizado.
Dica(Resposta) Teste 7 — Object Adapter vs. Class Adapter
  • ✔ Verdadeiro — Por composição, o Adapter só depende da API pública declarada pelo tipo LogServicoLegado — qualquer subclasse válida (presente ou futura) satisfaz esse contrato via polimorfismo, sem exigir nenhuma alteração no LogAdapter.
  • ✗ Falso — É o oposto: extends fixa o Adapter, em tempo de compilação, a uma única classe-mãe específica. Uma nova subclasse lançada pelo fornecedor exigiria escrever um Class Adapter dedicado a ela — a herança não propaga essa capacidade automaticamente.
  • ✗ Falso — A preferência é estrutural, não estética: o Object Adapter (composição) desacopla o Adapter da implementação concreta do legado, permite reaproveitar subclasses futuras e facilita testes com mocks — vantagens de acoplamento, não de brevidade de código.
  • ✗ Falso — É o oposto: composição via injeção de construtor é o que permite passar um mock no lugar do objeto real em teste; substituir por herança elimina esse ponto de injeção, tornando a substituição por mock mais difícil, não mais fácil.
NotaTeste 8 — Estudo de Caso: Unificação de Log e a Fronteira com o State
  • □ Se EstadoPago chamar pedido.getLogger().info(...) para registrar uma transição, essa classe de estado permanece livre de qualquer conhecimento sobre a existência de LogServicoLegado.
  • □ Se o fornecedor do sistema de auditoria legado mudar a assinatura de registrarLogNoArquivo de (String, int) para (String, String), um sistema que usa LogAdapter corretamente precisaria alterar apenas a classe LogAdapter, sem tocar em nenhuma classe de estado do Pedido.
  • □ A combinação de State (para o fluxo interno) e Adapter (para a fronteira externa) na mesma classe EstadoPago representa uma violação do princípio de que cada classe deve aplicar exatamente um único padrão de projeto.
  • □ Um sistema onde Pedido recebe um LoggerModerno por injeção de dependência, em vez de instanciar diretamente new LogServicoLegado() dentro de suas classes de estado, permite substituir o logger por um mock em testes automatizados sem alterar as classes de estado.
Dica(Resposta) Teste 8 — Estudo de Caso: Unificação de Log e a Fronteira com o State
  • ✔ Verdadeiro — EstadoPago conhece só a abstração de logging exposta pelo Pedido — o LogAdapter (e o LogServicoLegado que ele encapsula) fica inteiramente escondido atrás dessa fronteira, exatamente o isolamento que o Adapter existe para produzir.
  • ✔ Verdadeiro — Toda a dependência do formato exato da API legada está concentrada dentro do LogAdapter — uma mudança de assinatura do lado do fornecedor se resolve editando só essa classe de fronteira, sem se propagar para as classes de estado que só conhecem a interface abstrata de logging.
  • ✗ Falso — Não existe tal princípio: padrões de projeto resolvem problemas arquiteturais ortogonais entre si, e nada impede uma mesma classe de participar de mais de um padrão simultaneamente — aqui, EstadoPago participa do State (decidindo sua própria transição) e, ao delegar uma chamada de log, se beneficia do Adapter que outra classe implementa.
  • ✔ Verdadeiro — É a mesma vantagem de testabilidade já vista com Strategy, Factory Method e o próprio Adapter: injetar a dependência via construtor, em vez de instanciá-la diretamente, é o que abre espaço para trocar a implementação real por um mock em teste, sem tocar no código das classes de estado.