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

Data de Publicação

18 de setembro de 2026

Slides Lista de aulas

1 Proposta da Aula

Na Aula 11, o Pedido ganhou dois terços do “tríptico da estabilidade”: o dado deixou de correr risco de aliasing (Value Object) e a gênese deixou de ser um construtor sujo de parâmetros arriscados (Builder, Factory Method). Falta o terceiro pilar: o fluxo — como um objeto muda de comportamento ao longo do seu próprio ciclo de vida sem recair no labirinto de if/else que o Strategy já baniu do cálculo de pagamento. É exatamente aí que o Pedido ainda esconde um problema: ele controla se pode ser pago, cancelado ou enviado através de uma variável status (uma String ou um int) verificada no topo de cada método — a mesma Obsessão por Primitivos da Aula 11, agora infiltrada no comportamento, não no dado.

O padrão que resolve isso — o State — tem uma peculiaridade notável: sua topologia de classes (um Contexto, uma interface, implementações concretas) é quase indistinguível da topologia do Strategy que já dominamos. E é exatamente por isso que o contraste entre os dois vai ser o fio condutor da primeira metade desta aula: mesma estrutura, intenção arquitetural completamente diferente.

Na segunda metade, a aula muda de direção. Depois de o objeto estar protegido por dentro — dado, gênese e agora fluxo —, resta uma fronteira que nenhum dos padrões anteriores tocou: o ponto onde o sistema conversa com o mundo externo — um SDK de pagamento, uma biblioteca de auditoria de terceiros, um sistema legado que a empresa não pode reescrever. Quando a interface desse mundo externo não bate com a que o domínio espera, surge o padrão Adapter.

O roteiro, em quatro perguntas:

  1. Por que reduzir o ciclo de vida de um objeto a uma String status verificada em cada método é uma bomba-relógio de manutenção, e não apenas um estilo de código menos elegante?
  2. Como o State converte esse controle de fluxo em objetos polimórficos — e, já que sua estrutura de classes parece a do Strategy, por que os dois resolvem problemas arquiteturais diferentes?
  3. Quando um estado decide para onde o objeto vai a seguir, quem deveria guardar essa decisão — o próprio estado, ou o objeto hospedeiro?
  4. Se o sistema precisa conversar com uma biblioteca cuja interface não foi desenhada para o seu domínio, e você não pode alterar nem a biblioteca nem o jeito como o resto do sistema já fala, quem deveria absorver essa tradução?

Considere o nosso Pedido, já purificado pelo Strategy (Aula 10) e nascido em segurança pelo Builder (Aula 11). Ele ainda tem um método pagar() que começa perguntando “em que status eu estou?”, um cancelar() que repete a mesma pergunta, e um enviar() que a repete outra vez. Adicionar um único estado novo — digamos, “EM_ANALISE_DE_FRAUDE” — significa abrir os três métodos e inserir um novo if em cada um. É esse Pedido, comportamentalmente ainda no modelo antigo, que os próximos blocos corrigem — e, feito isso, a aula vira a atenção para a fronteira que liga esse Pedido já robusto ao resto do mundo.

2 O Antipadrão do Status como Primitivo

O método que sempre começa com uma pergunta. Considere o Pedido tal como ele ainda está, depois do Strategy e do Builder — funcional, mas com o ciclo de vida codificado em condicionais:

public 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!");
        }
        // E se adicionarmos o estado "CANCELADO" ou "EM_ANALISE_DE_FRAUDE"?
    }

    public void cancelar() {
        if (status.equals("NOVO")) {
            status = "CANCELADO";
        } else if (status.equals("PAGO")) {
            estornar();
            status = "CANCELADO";
        } else if (status.equals("ENVIADO")) {
            throw new IllegalStateException("Impossivel cancelar: pedido em transito!");
        }
    }

    // enviar() repete a mesma bateria de "if (status.equals(...))"
}

Repare no padrão: a primeira linha de cada método é sempre uma verificação de status, e a lógica de negócio real vem depois, aninhada dentro do if correto. Isso cria um acoplamento temporal — a ordem das chamadas importa (não dá para cancelar antes de existir, nem pagar duas vezes), mas a regra que garante essa ordem está espalhada e escondida atrás de condicionais repetidas em cada método.

Três sintomas concretos. Primeiro, a fragilidade de evolução: adicionar um único estado novo (por exemplo, “EM_ANALISE_DE_FRAUDE” entre “NOVO” e “PAGO”) exige abrir pagar(), cancelar() e enviar() e inserir um else if em cada um — o novo estado age como uma “bomba” que obriga a mexer em todos os métodos que dependem de status, violando diretamente o OCP já visto na Aula 10. Segundo, a complexidade ciclomática: a lógica de transição de estado fica misturada com a lógica de negócio (processar pagamento, estornar), tornando o teste unitário exaustivo — para testar “cancelar um pedido pago” é preciso primeiro montar um pedido no estado certo, exatamente o problema de testabilidade que o Strategy já havia resolvido para os algoritmos de pagamento. Terceiro, e talvez o mais familiar: status — normalmente uma String ou um int — é um caso claro de Obsessão por Primitivos (Aula 11): nada no tipo String impede que alguém escreva status = "PGO" por engano, ou compare com um valor que nunca existiu. O compilador não ajuda em nada.

O padrão State surge exatamente para converter esse controle de fluxo, hoje expresso em condicionais sobre um primitivo, em estrutura de objetos — a mesma virada conceitual que a Aula 11 já fez para o dado (doubleDinheiro), agora aplicada ao comportamento ao longo do tempo.

2.1 O Antipadrão do Status e a Fragilidade de Evolução

DicaSe 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).

  • □ No 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”.
  • □ Um teste unitário que verifica “cancelar um pedido já pago dispara estorno” precisa, antes de chamar cancelar(), montar um Pedido cujo campo status já esteja manualmente ajustado para "PAGO".
  • □ Se o tipo do campo 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.
  • □ A fragilidade de evolução do 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.

3 O Padrão State: Estrutura e Delegação

A motivação central: tratar estados como estratégias de comportamento. A limpeza arquitetural do State vem de uma virada conceitual simples: em vez de tratar cada fase do ciclo de vida como um valor guardado numa variável, tratamos cada fase como uma classe polimórfica. O Pedido (o Contexto) deixa de saber “como” reagir a pagar() ou cancelar() e passa a perguntar apenas ao “quem” ele é no momento — delegando toda a decisão para um objeto de estado. Quando o estado muda, o Contexto simplesmente aponta para uma instância nova de uma classe diferente. O sistema passa a crescer horizontalmente: um novo estado significa uma nova classe, não um novo if espalhado por três métodos.

A estrutura em três papéis. O padrão define:

  • Context (Contexto): a classe que os clientes efetivamente usam — no nosso caso, Pedido. Mantém uma referência para o EstadoAtual e delega todas as operações sensíveis ao estado.
  • State (interface): o contrato dos métodos que dependem do estado.
  • Concrete States (Estados Concretos): uma classe por fase do ciclo de vida, cada uma implementando o comportamento correspondente.
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!");
    }
}

O EstadoNovo sabe que, após um pagamento bem-sucedido, o próximo passo lógico é o EstadoPago — toda essa inteligência de fluxo está encapsulada fora da classe Pedido, que fica livre para representar apenas a entidade de negócio.

A “cegueira” deliberada do Contexto. O Pedido, agora um hospedeiro, limita-se a repassar cada chamada para o objeto de estado atual:

public class Pedido {
    private EstadoPedido estadoAtual;
    private double valor;

    public Pedido(double valor) {
        this.valor = valor;
        this.estadoAtual = new EstadoNovo(); // todo pedido nasce em EstadoNovo
    }

    // Invocado apenas pelos objetos de estado concreto
    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); }

    public double getValor() { return valor; }
}

O ponto-chave é setEstado: marcado como protected, só as classes de estado (que residem no mesmo pacote) podem disparar transições. Isso impede que um cliente externo mude o estado do pedido de forma arbitrária — por exemplo, saltando de “Novo” direto para “Entregue” — preservando a integridade da máquina de estados. Comparando com o Pedido do bloco anterior: a complexidade ciclomática da classe caiu a quase zero, porque a “inteligência” de fluxo foi inteiramente terceirizada. Se você quer saber o que acontece quando um EstadoNovo é cancelado, a resposta mora inteira dentro de EstadoNovo.cancelar() — não espalhada por um método de 100 linhas com 10 ifs.

O diagrama resume o ciclo de vida completo que os próximos parágrafos implementam: de EstadoNovo, o pedido pode ser pago (indo para EstadoPago) ou cancelado diretamente; de EstadoPago, pode ser enviado ou cancelado (com estorno); de EstadoEnviado, o cancelamento deixa de ser permitido — a transição em vermelho tracejado marca uma operação que a classe correspondente rejeita com exceção, não uma transição real.

O contraste que importa: Strategy vs. State. Peguem o diagrama do Strategy da Aula 10 e comparem com o Pedido/EstadoPedido acima — a semelhança estrutural é real, não coincidência: ambos têm um Contexto, uma interface e implementações concretas intercambiáveis via polimorfismo. Mas a intenção arquitetural é oposta em um ponto preciso: quem decide qual implementação está ativa, e quando.

  • No Strategy, um agente externo ao Contexto — o código cliente — decide qual ConcretaEstrategia injetar, normalmente uma única vez (ou trocando por decisão explícita do usuário, como o frete escolhido no carrinho). O Contexto nunca troca de estratégia sozinho.
  • No State, o próprio objeto de estado ativo decide, como consequência de uma operação de negócio, qual será o próximo estado — e dispara a transição chamando setEstado no Contexto. Ninguém de fora precisa (nem deveria) injetar um EstadoPago manualmente; ele nasce como resultado de pagar() ser chamado sobre um EstadoNovo.

Em outras palavras: o Strategy injeta um algoritmo de fora para dentro; o State faz o objeto mudar de “personalidade” de dentro para fora, como consequência do seu próprio ciclo de vida. Dizer que “State é só Strategy com outro nome” ignora exatamente esse ponto — a topologia de classes é parecida porque ambos resolvem “substituir condicionais por polimorfismo”, mas o gatilho da substituição de implementação mora em lugares opostos.

O painel da esquerda mostra a seta de controle vindo de fora (o Cliente injeta a estratégia); o painel da direita mostra a seta de controle saindo do próprio estado ativo em direção ao Contexto (setEstado) — o Cliente sequer aparece no diagrama do State, porque ele nunca decide diretamente qual estado vem a seguir.

3.1 Strategy vs. State: Mesma Topologia, Intenções Diferentes

DicaSe 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).

  • □ Em um 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.
  • □ Num 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.
  • □ Se um 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.
  • □ O fato de tanto Strategy quanto State definirem uma interface implementada por múltiplas classes concretas é suficiente, por si só, para classificá-los como o mesmo padrão de projeto sob nomes diferentes.

4 Onde Colocar a Transição, e o Ciclo de Vida Robusto

O dilema mais difícil do State Pattern. Nos exemplos acima, EstadoNovo.pagar() instancia diretamente new EstadoPago(). Isso funciona, mas cria uma decisão de design que precisa ser feita conscientemente: onde deve morar o conhecimento de “qual é o próximo estado”?

  • Opção A — Transição nos Estados (a que usamos até aqui): cada estado conhece o seu sucessor. Prós: transições dinâmicas e fluidas, sem nenhuma lógica condicional no Contexto. Contras: os estados ficam acoplados entre si — EstadoNovo precisa conhecer a classe EstadoPago.
  • Opção B — Transição no Contexto: o Contexto centraliza a “tabela” de transições.
// Opcao B: o Contexto decide, reintroduzindo condicional
protected void avancarFluxo() {
    if (estadoAtual instanceof EstadoNovo) {
        this.estadoAtual = new EstadoPago();
    } else if (estadoAtual instanceof EstadoPago) {
        this.estadoAtual = new EstadoEnviado();
    }
    // o Contexto volta a ter logica condicional - o que queriamos evitar
}

Prós: os estados ficam totalmente isolados uns dos outros. Contras: o Contexto volta a ter lógica condicional — exatamente o problema que o State foi criado para eliminar.

O veredito prático: use transições nos estados (Opção A) para fluxos de negócio naturais e bem definidos, como o ciclo de vida de um Pedido — o acoplamento entre EstadoNovo e EstadoPago é aceitável porque essa sequência já é uma regra de negócio estável, não uma variação livre. Uma técnica avançada para reduzir esse acoplamento sem voltar à Opção B é fazer o método de estado retornar o próximo estado, deixando para o Contexto apenas a tarefa de atualizar a referência (this.estado = estado.proximo();), ou usar uma fábrica de estados dedicada — sem, ainda assim, reintroduzir condicionais no Contexto.

O ciclo de vida robusto: o polimorfismo cobrindo os casos de borda. A força real do State aparece quando o mesmo método se comporta de forma distinta em cada classe, sem um único if (status == ...):

public 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());
        System.out.println("Pedido estornado e cancelado.");
    }

    @Override
    public void enviar(Pedido pedido) {
        pedido.setEstado(new EstadoEnviado());
    }
}

public class EstadoEnviado implements EstadoPedido {
    @Override
    public void pagar(Pedido pedido) {
        throw new IllegalStateException("Pedido ja foi enviado!");
    }

    @Override
    public void cancelar(Pedido pedido) {
        throw new DomainException("Impossivel cancelar: produto em transito!");
    }

    @Override
    public void enviar(Pedido pedido) {
        throw new IllegalStateException("Pedido ja foi enviado!");
    }
}

O método cancelar() se comporta de forma distinta em cada estado: em EstadoNovo, apenas muda o status; em EstadoPago, desencadeia um estorno financeiro antes de mudar; em EstadoEnviado, proíbe a ação, protegendo a regra de negócio de que um produto em trânsito não pode ser simplesmente cancelado. Tudo isso sem um único if (status == ...) — o código torna-se auto-documentado: para saber o que acontece quando um pedido pago é cancelado, basta abrir a classe EstadoPago, isolada de ruídos de outros estados.

5 Da Fronteira Interna à Fronteira Externa: Motivação do Adapter

Com Strategy, Builder e agora State, o Pedido está protegido por dentro em três frentes: comportamento variável (Strategy), gênese (Builder) e fluxo de vida (State). Mas nenhum desses padrões toca um problema diferente: como o sistema conversa com o que está fora dele. Considere um cenário concreto e comum: a equipe de compliance da empresa exige que toda transição de estado do Pedido seja registrada num sistema de auditoria — mas esse sistema é uma biblioteca legada, mantida por outra equipe, que ninguém tem permissão (nem motivo técnico) para reescrever. A interface que essa biblioteca oferece não é a que o resto do sistema já usa para logar eventos.

O dilema é o mesmo, em espírito, do dilema do legado descrito na Aula 10: alterar o código de terceiros é impossível (biblioteca fechada); alterar o código central do domínio para se moldar ao formato do fornecedor destrói a arquitetura que as últimas três aulas construíram com tanto cuidado. É esse conflito — uma interface externa incompatível com o contrato que o domínio já espera — que o padrão Adapter resolve.

6 O Padrão Adapter: Tradução de Protocolos

O papel do Adapter: um tradutor posicionado na fronteira. O Adapter atua como um intermediário que converte a interface de uma classe incompatível na interface que o cliente já espera. O cliente faz uma chamada baseando-se numa interface abstrata que ele conhece e confia; o Adapter implementa essa interface esperada — o que o torna aceitável aos olhos do cliente — mas, por dentro, guarda uma referência para o objeto do fornecedor externo (o Adaptee). Ele recebe os parâmetros do cliente, faz as conversões necessárias e invoca os métodos nativos do sistema legado. Como o filósofo do design diria: “Adapter permite que classes trabalhem juntas mesmo que não pudessem de outra forma, por causa de interfaces incompatíveis.”

// A fronteira que o resto do sistema ja usa
public interface LoggerModerno {
    void info(String mensagem);
}

// A biblioteca legada de auditoria, incompativel, mantida por outra equipe
public class LogServicoLegado {
    public void registrarLogNoArquivo(String msg, int severidade) {
        System.out.println("[" + severidade + "] " + msg);
    }
}

// O ADAPTER: harmoniza os dois mundos via composicao
public class LogAdapter implements LoggerModerno {
    private LogServicoLegado legado; // Object Adapter: TEM-UM legado

    public LogAdapter(LogServicoLegado legado) {
        this.legado = legado;
    }

    @Override
    public void info(String mensagem) {
        // Traducao: absorve o parametro de severidade que o legado exige
        legado.registrarLogNoArquivo(mensagem, 1);
    }
}

Sem o LogAdapter, cada classe de negócio que precisasse registrar um evento de auditoria teria que conhecer o número mágico 1 (severidade “info”) e o nome exótico registrarLogNoArquivo. Centralizando essa tradução, todo o resto do sistema — incluindo o nosso Pedido — só precisa conhecer LoggerModerno.info(mensagem).

Ligando de volta ao State desta aula: o Pedido pode receber um LoggerModerno por injeção (o mesmo raciocínio de Dependência Injetada já usado com Pagavel na Aula 10), e cada classe de estado concreta passa a registrar sua própria transição sem conhecer a existência do LogServicoLegado:

public class EstadoPago implements EstadoPedido {
    @Override
    public void cancelar(Pedido pedido) {
        realizarEstornoFinanceiro(pedido);
        pedido.setEstado(new EstadoCancelado());
        pedido.getLogger().info("Pedido estornado e cancelado."); // via LogAdapter
    }
}

Se amanhã a equipe de compliance trocar a biblioteca legada por outra (com uma assinatura de método totalmente diferente), o único arquivo tocado é LogAdapter — nem Pedido, nem EstadoPago, nem nenhuma outra classe de estado precisa mudar uma linha.

Object Adapter vs. Class Adapter: por que a composição vence. O GoF descreve duas topologias técnicas para o Adapter. O Class Adapter usa herança: a classe Adapter estende diretamente o Adaptee ao mesmo tempo em que implementa a interface esperada pelo cliente.

// Class Adapter (via heranca) - desencorajado
public class LogAdapterPorHeranca extends LogServicoLegado implements LoggerModerno {
    @Override
    public void info(String mensagem) {
        registrarLogNoArquivo(mensagem, 1); // metodo herdado, nao delegado
    }
}

O problema é o acoplamento estático em tempo de compilação: o Adapter passa a conhecer os detalhes íntimos e o estado interno da classe herdada, violando o encapsulamento. Pior — se o fornecedor lançar uma nova subclasse de LogServicoLegado (por exemplo, uma versão com rate limiting embutido), o LogAdapterPorHeranca não consegue reaproveitá-la: ele está rigidamente preso à classe-mãe original via extends.

Por isso, a indústria adota quase exclusivamente o Object Adapter (o LogAdapter que já vimos), apoiado no princípio “Tem-Um”. Como o LogAdapter trata o legado como uma caixa preta opaca, recebida por injeção, o mesmo Adapter funciona com qualquer subclasse presente ou futura de LogServicoLegado, sem nenhuma alteração — a regra de ouro é “prefira sempre Object Adapter para manter as caixas pretas fechadas”.

6.1 O Dilema da Herança no Adapter

DicaSe 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).

  • □ Um 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.
  • □ Um 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.
  • □ Trocar 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.
  • □ Se 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.

7 Conclusão

Voltando às quatro perguntas da abertura:

  1. Por que a String status é uma bomba-relógio: porque a lógica de transição fica espalhada e misturada com regra de negócio em vários métodos, tornando cada estado novo uma edição obrigatória em toda a classe — a mesma Obsessão por Primitivos da Aula 11, agora aplicada ao comportamento em vez do dado.
  2. Como o State resolve isso, e por que não é “Strategy disfarçado”: convertendo cada fase do ciclo de vida numa classe polimórfica que se autodetermina — a topologia de classes é semelhante à do Strategy, mas o gatilho da troca de implementação mora dentro do próprio fluxo de negócio, não numa injeção externa.
  3. Onde mora a transição: de preferência, dentro dos próprios estados — o acoplamento entre EstadoNovo e EstadoPago é aceitável porque a sequência já é uma regra de negócio estável; centralizar no Contexto reintroduziria a condicional que o padrão existe para eliminar.
  4. Quem traduz uma interface externa incompatível: um Adapter posicionado exatamente na fronteira, tratando o legado como caixa preta via composição (Object Adapter) — nunca via herança, que prenderia o sistema à classe concreta do fornecedor.

O Pedido que começou esta aula perguntando “em que status eu estou?” no topo de cada método termina com o fluxo inteiramente delegado a classes de estado auto-documentadas, e com um canal de comunicação para o mundo externo (auditoria, SDKs, sistemas legados) inteiramente isolado atrás de um Adapter. Strategy, Builder/Factory Method e State fecham o “tríptico da estabilidade” do objeto por dentro; o Adapter abre a primeira fronteira estrutural para fora.

Ponte para a Aula 13

A fronteira externa aberta pelo Adapter não é a única forma de composição estrutural e comportamental que falta explorar. Duas perguntas ficam em aberto: como adicionar responsabilidades a um objeto individual de forma dinâmica e cumulativa, sem recair na explosão combinatória de subclasses que a herança geraria (o padrão Decorator — pensem no sistema de I/O do próprio Java, que empilha buffers e streams exatamente assim); e como um evento no núcleo do sistema pode disparar reações em múltiplos subsistemas periféricos sem que o núcleo sequer saiba quem está ouvindo (o padrão Observer, a base conceitual de arquiteturas orientadas a eventos). A Aula 13 ataca essas duas questões; a síntese final que compara todos os padrões de isolamento vistos ao longo do curso — e estabelece a fronteira definitiva entre o núcleo estável e a periferia volátil de um sistema — fica para o fechamento da disciplina.

Exercícios Soluções