Padrões comportamentais: State e Visitor

1. Introdução aos Padrões Comportamentais State e Visitor

1.1. Contexto e motivação

A arquitetura de software frequentemente enfrenta dois desafios complementares: gerenciar comportamentos que mudam conforme o estado interno de um objeto e executar operações distintas sobre uma hierarquia de classes sem poluí-la. Os padrões State e Visitor surgem como soluções elegantes para esses problemas.

O padrão State permite que um objeto altere seu comportamento quando seu estado interno muda, parecendo ter mudado de classe. Já o Visitor permite definir novas operações sobre uma hierarquia de classes sem modificá-las, separando o algoritmo da estrutura de dados.

1.2. Relação com padrões vizinhos

State e Visitor pertencem ao mesmo grupo comportamental que Command, Chain of Responsibility e Iterator. Enquanto Command encapsula uma solicitação como objeto, State gerencia transições de comportamento. Chain of Responsibility encadeia handlers sequencialmente, enquanto Visitor distribui operações entre elementos. Iterator percorre coleções; Visitor as percorre aplicando operações específicas.

1.3. Diferença fundamental

A distinção essencial: State foca na mudança de comportamento baseada em estado interno, permitindo que um objeto transicione entre diferentes modos de operação. Visitor foca na separação entre estrutura de dados e operações, permitindo adicionar novas funcionalidades sem alterar classes existentes.

2. Padrão State: Máquina de Estados na Arquitetura

2.1. Estrutura central

O padrão State é composto por três elementos:
- Context: mantém uma referência ao estado atual e delega comportamento a ele
- State (interface): declara métodos para comportamentos específicos de cada estado
- Concrete States: implementam comportamentos específicos e gerenciam transições

// Interface State
public interface PedidoState {
    void processar(Pedido pedido);
    void cancelar(Pedido pedido);
    void entregar(Pedido pedido);
}

// Concrete State: Novo
public class NovoState implements PedidoState {
    public void processar(Pedido pedido) {
        System.out.println("Pedido sendo processado");
        pedido.setState(new ProcessandoState());
    }
    public void cancelar(Pedido pedido) {
        System.out.println("Pedido cancelado");
        pedido.setState(new CanceladoState());
    }
    public void entregar(Pedido pedido) {
        System.out.println("Não é possível entregar pedido novo");
    }
}

// Context
public class Pedido {
    private PedidoState state;

    public Pedido() {
        this.state = new NovoState();
    }

    public void processar() { state.processar(this); }
    public void cancelar() { state.cancelar(this); }
    public void entregar() { state.entregar(this); }
}

2.2. Impacto arquitetural

State elimina condicionais complexas (if/else ou switch) espalhadas pelo código, isolando cada comportamento de estado em sua própria classe. Isso reduz o acoplamento e facilita a adição de novos estados sem modificar o Context.

2.3. Exemplo conceitual

Em um sistema de fluxo de pedidos, os estados podem ser: Novo, Processando, Enviado, Entregue, Cancelado. Cada estado define transições válidas, prevenindo operações inválidas como cancelar um pedido já entregue.

3. Padrão Visitor: Operações Desacopladas da Hierarquia

3.1. Estrutura central

O padrão Visitor utiliza:
- Element: interface com método accept(Visitor)
- Concrete Elements: classes que implementam accept
- Visitor (interface): declara métodos visit(ConcreteElement) para cada tipo de elemento
- Concrete Visitors: implementam operações específicas

// Element
public interface Documento {
    void accept(Visitor visitor);
}

// Concrete Elements
public class PDF implements Documento {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

public class Word implements Documento {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// Visitor
public interface Visitor {
    void visit(PDF pdf);
    void visit(Word word);
}

// Concrete Visitor
public class ExportadorJSON implements Visitor {
    public void visit(PDF pdf) {
        System.out.println("Exportando PDF para JSON");
    }
    public void visit(Word word) {
        System.out.println("Exportando Word para JSON");
    }
}

3.2. Impacto arquitetural

Visitor implementa double dispatch: a operação executada depende tanto do tipo do Element quanto do tipo do Visitor. Isso permite adicionar novas operações sem modificar a hierarquia de classes existente, seguindo o Princípio Aberto/Fechado.

3.3. Exemplo conceitual

Em uma camada de dados, diferentes formatos de exportação (JSON, XML, CSV) podem ser implementados como Visitors sobre uma hierarquia de documentos (PDF, Word, Excel). Cada novo formato de exportação requer apenas uma nova classe Visitor.

4. State e a Arquitetura em Camadas

4.1. Integração na camada de negócios

Na arquitetura em camadas, o State atua na camada de domínio, modelando estados como objetos de domínio ricos. Cada estado encapsula regras de negócio específicas, mantendo a lógica de transição centralizada.

public class ContaBancaria {
    private ContaState state;
    private double saldo;

    public void sacar(double valor) {
        state.sacar(this, valor);
    }
}

public class AtivaState implements ContaState {
    public void sacar(ContaBancaria conta, double valor) {
        if (conta.getSaldo() >= valor) {
            conta.debitar(valor);
        } else {
            throw new SaldoInsuficienteException();
        }
    }
}

4.2. Transições e camada de apresentação

A camada de apresentação pode observar mudanças de estado via Observer ou eventos, atualizando a interface do usuário conforme o estado atual.

4.3. Persistência de estados

Para persistir o estado atual, pode-se armazenar um identificador do estado (string ou enum) no banco de dados. Ao carregar, o Context restaura o Concrete State correspondente.

5. Visitor e a Arquitetura Hexagonal

5.1. Visitor como extensão de portas

Na arquitetura hexagonal (ports and adapters), o Visitor atua como mecanismo de extensão das portas. Os Elements representam o domínio, enquanto Visitors representam operações de infraestrutura.

5.2. Separação domínio/infraestrutura

Elements permanecem puramente no domínio, sem conhecimento de formatos de saída. Visitors, na camada de infraestrutura, implementam operações específicas (serialização, persistência, envio de eventos).

5.3. Exemplo prático

// Domínio (Element)
public class EventoVenda implements Evento {
    public void accept(EventVisitor visitor) {
        visitor.visit(this);
    }
}

// Infraestrutura (Visitor)
public class ProcessadorEventoKafka implements EventVisitor {
    public void visit(EventoVenda evento) {
        kafkaProducer.send(new KafkaRecord("vendas", evento.toJson()));
    }
}

6. Comparação e Trade-offs entre State e Visitor

6.1. Quando usar State

  • Comportamentos que variam conforme estado interno
  • Número finito e conhecido de estados
  • Transições bem definidas entre estados
  • Exemplos: workflows, máquinas de estados, conexões de rede

6.2. Quando usar Visitor

  • Hierarquia de classes estável com operações variáveis
  • Necessidade de adicionar operações sem modificar classes existentes
  • Operações que não pertencem naturalmente à hierarquia
  • Exemplos: AST de compiladores, exportação de dados, relatórios

6.3. Desafios comuns

State pode aumentar o número de classes e dificultar a visibilidade do fluxo completo. Visitor quebra o encapsulamento ao exigir que Elements exponham dados internos. Ambos podem introduzir complexidade desnecessária em sistemas simples.

7. Conclusão e Aplicações Práticas

7.1. Benefícios arquiteturais

State e Visitor promovem modularidade, extensibilidade e separação de responsabilidades. State isola comportamentos por estado; Visitor separa operações da estrutura de dados. Ambos seguem o Princípio Aberto/Fechado e reduzem acoplamento.

7.2. Exemplos reais

State é amplamente usado em workflows de aprovação (documentos, pedidos), conexões TCP (estabelecendo, ouvindo, fechado) e máquinas de estados em jogos. Visitor é fundamental em compiladores para percorrer AST (análise semântica, geração de código) e em frameworks de processamento de documentos.

7.3. Recomendações

Considere combinar State com Command para encapsular transições como objetos. Use Template Method para definir o esqueleto de algoritmos nos estados. Visitor pode ser combinado com Composite para percorrer estruturas hierárquicas.

A escolha entre State e Visitor depende do eixo de variação: se o comportamento varia por estado, use State; se as operações variam sobre uma estrutura estável, use Visitor. Ambos são ferramentas poderosas para arquiteturas flexíveis e sustentáveis.

Referências