Padrões comportamentais: Observer e Strategy

1. Introdução aos Padrões Comportamentais na Arquitetura de Software

1.1. O papel dos padrões comportamentais no design de interações entre objetos

Padrões comportamentais tratam da comunicação e da delegação de responsabilidades entre objetos. Na arquitetura de software, esses padrões definem como os componentes trocam mensagens, como reagem a mudanças de estado e como algoritmos podem ser selecionados dinamicamente. Diferentemente dos padrões criacionais (que focam na instanciação) ou estruturais (que focam na composição), os comportamentais modelam o fluxo de controle e a lógica de tomada de decisão.

1.2. Panorama geral: Observer (notificação) vs. Strategy (algoritmos intercambiáveis)

O Observer estabelece um mecanismo de notificação um-para-muitos: quando um objeto (subject) muda de estado, todos os seus dependentes (observers) são automaticamente notificados. É ideal para sistemas reativos e orientados a eventos.

O Strategy encapsula famílias de algoritmos relacionados, tornando-os intercambiáveis dentro de um contexto. Em vez de condicionais espalhadas pelo código, delega-se a escolha do algoritmo a uma interface polimórfica.

1.3. Quando cada padrão se aplica em sistemas de grande escala

Observer é recomendado quando múltiplos componentes precisam ser informados sobre mudanças de estado sem acoplamento rígido. Strategy é indicado quando um sistema precisa variar seu comportamento em tempo de execução, como em mecanismos de precificação, validação ou ordenação.

2. Padrão Observer: Mecanismo de Notificação e Reatividade

2.1. Definição e estrutura: Subject, Observer e o contrato de atualização

O padrão define três papéis principais:
- Subject: mantém o estado e uma lista de observers, oferecendo métodos para anexar, desanexar e notificar.
- Observer: interface com um método update() que o subject invoca.
- ConcreteObserver: implementa a interface e reage às notificações.

2.2. Variações arquiteturais: push vs. pull, eventos assíncronos e filas

No modelo push, o subject envia dados detalhados junto com a notificação. No modelo pull, o observer consulta o subject para obter os dados necessários. Em arquiteturas modernas, o Observer é frequentemente implementado com filas de mensagens (como RabbitMQ, Kafka) para suportar assincronia e resiliência.

2.3. Implementação com desacoplamento: lista de observadores e gerenciamento de inscrições

interface Observer {
    method update(data: any): void
}

class Subject {
    private observers: List<Observer> = []

    method attach(obs: Observer): void {
        observers.add(obs)
    }

    method detach(obs: Observer): void {
        observers.remove(obs)
    }

    method notifyAll(data: any): void {
        for each obs in observers {
            obs.update(data)
        }
    }
}

3. Aplicações Arquiteturais do Observer

3.1. Sistemas de eventos e notificações em tempo real (ex.: interfaces de usuário)

Em frameworks como React ou Vue.js, o Observer é a base do sistema reativo: quando um estado muda, os componentes que dependem dele são automaticamente re-renderizados. Em sistemas de trading, notificações de preço disparam múltiplas estratégias de compra/venda.

3.2. Integração com barramento de mensagens e arquitetura orientada a eventos

Em arquiteturas baseadas em eventos (Event-Driven Architecture), o Observer evolui para o padrão Event Bus ou Message Broker. Microsserviços publicam eventos em um barramento central, e outros serviços consomem esses eventos de forma assíncrona, promovendo desacoplamento total.

3.3. Riscos e mitigação: vazamento de memória, cascata de notificações e consistência

  • Vazamento de memória: observers não removidos causam referências órfãs. Solução: usar referências fracas (WeakReference) ou garantir remoção explícita.
  • Cascata de notificações: observers que modificam o subject podem gerar loops infinitos. Solução: usar flags de reentrância ou filas de eventos.
  • Consistência: notificações podem ocorrer em estado inconsistente. Solução: notificar apenas após a transação ser concluída.

4. Padrão Strategy: Encapsulamento de Algoritmos e Políticas

4.1. Definição e estrutura: Context, Strategy interface e implementações concretas

O Strategy separa o "o quê" (contexto) do "como" (estratégia). A estrutura é:
- Context: mantém uma referência para uma estratégia e delega a execução.
- Strategy: interface que define o método do algoritmo.
- ConcreteStrategy: implementações específicas.

4.2. Princípio Open/Closed em ação: adição de novas estratégias sem modificar o contexto

O contexto permanece fechado para modificação, mas aberto para extensão: novas estratégias podem ser adicionadas sem alterar o código existente. Isso reduz drasticamente o risco de regressão.

4.3. Exemplos clássicos: estratégias de ordenação, validação, cálculo de frete

Sistemas de e-commerce frequentemente usam Strategy para calcular frete: Correios, transportadora privada, retirada na loja. Cada modalidade é uma estratégia concreta, e o contexto (carrinho) simplesmente invoca calcularFrete().

5. Aplicações Arquiteturais do Strategy

5.1. Configuração dinâmica de comportamentos via injeção de dependência

Em frameworks como Spring ou .NET Core, a injeção de dependência permite trocar estratégias em tempo de execução com base em configuração (arquivos YAML, variáveis de ambiente). Isso é especialmente útil em sistemas multi-tenant.

5.2. Uso em pipelines de processamento e seleção de políticas de negócio

Em pipelines ETL, cada etapa pode ser uma estratégia: extração, transformação, carga. Em sistemas de recomendação, diferentes algoritmos (colaborativo, baseado em conteúdo, híbrido) são estratégias selecionadas por perfil de usuário.

5.3. Trade-offs: complexidade adicional vs. flexibilidade e testabilidade

Strategy adiciona classes extras e indireção, mas em compensação:
- Testabilidade: cada estratégia pode ser testada isoladamente.
- Flexibilidade: novos comportamentos são introduzidos sem tocar em código estável.
- Manutenibilidade: condicionais complexas desaparecem.

6. Comparação e Sinergia entre Observer e Strategy

6.1. Diferenças fundamentais: notificação reativa vs. seleção de algoritmo

Observer é reativo: a mudança em um objeto dispara ações em outros. Strategy é seletivo: um contexto escolhe qual algoritmo executar. Observer resolve "quem precisa saber", Strategy resolve "como fazer".

6.2. Cenários de uso combinado: Strategy como observador ou contexto reativo

É possível combinar ambos: um sistema de notificações (Observer) pode usar diferentes estratégias de formatação (Strategy) para apresentar a notificação em JSON, XML ou HTML. O observer recebe os dados, mas delega a formatação a uma estratégia injetada.

6.3. Impacto na arquitetura: acoplamento, escalabilidade e manutenibilidade

Observer tende a reduzir o acoplamento direto, mas pode aumentar o acoplamento temporal (todos os observers são notificados simultaneamente). Strategy reduz acoplamento condicional, mas exige gerenciamento explícito de estratégias. Ambos melhoram a manutenibilidade quando bem aplicados.

7. Exemplos de Código (pseudocódigo em text)

7.1. Observer: Estrutura básica com Subject e interface Observer

// Observer interface
interface EventListener {
    method onEvent(eventType: String, data: any): void
}

// Subject
class EventManager {
    private listeners: Map<String, List<EventListener>> = {}

    method subscribe(eventType: String, listener: EventListener): void {
        if not listeners.contains(eventType) {
            listeners[eventType] = []
        }
        listeners[eventType].add(listener)
    }

    method unsubscribe(eventType: String, listener: EventListener): void {
        listeners[eventType].remove(listener)
    }

    method notify(eventType: String, data: any): void {
        for each listener in listeners[eventType] {
            listener.onEvent(eventType, data)
        }
    }
}

// Concrete Observer
class EmailNotifier implements EventListener {
    method onEvent(eventType: String, data: any): void {
        print("Enviando email para evento: " + eventType + " com dados: " + data)
    }
}

7.2. Strategy: Contexto com interface Strategy e implementações concretas

// Strategy interface
interface ShippingStrategy {
    method calculate(weight: Float, distance: Float): Float
}

// Concrete Strategies
class CorreiosStrategy implements ShippingStrategy {
    method calculate(weight: Float, distance: Float): Float {
        return weight * 0.5 + distance * 0.1
    }
}

class TransportadoraStrategy implements ShippingStrategy {
    method calculate(weight: Float, distance: Float): Float {
        return weight * 0.3 + distance * 0.2 + 10.0
    }
}

// Context
class ShippingCalculator {
    private strategy: ShippingStrategy

    method setStrategy(strategy: ShippingStrategy): void {
        this.strategy = strategy
    }

    method calculate(weight: Float, distance: Float): Float {
        return strategy.calculate(weight, distance)
    }
}

7.3. Combinação: Sistema de notificações que utiliza estratégias de formatação

// Strategy para formatação
interface FormatStrategy {
    method format(data: any): String
}

class JsonFormatter implements FormatStrategy {
    method format(data: any): String {
        return JSON.stringify(data)
    }
}

class HtmlFormatter implements FormatStrategy {
    method format(data: any): String {
        return "<div>" + data + "</div>"
    }
}

// Observer que usa Strategy
class FormattedNotifier implements EventListener {
    private formatter: FormatStrategy

    method setFormatter(formatter: FormatStrategy): void {
        this.formatter = formatter
    }

    method onEvent(eventType: String, data: any): void {
        String formatted = formatter.format(data)
        print("Notificação formatada: " + formatted)
    }
}

8. Considerações Finais e Melhores Práticas

8.1. Quando evitar Observer: dependências cíclicas e desempenho crítico

Evite Observer quando o número de observers for muito grande e as notificações forem frequentes — o custo de notificação linear pode degradar o desempenho. Também evite se houver risco de dependências cíclicas entre subject e observers.

8.2. Quando evitar Strategy: número fixo de algoritmos ou overhead desnecessário

Se você tem apenas um ou dois algoritmos e não há perspectiva de novos, o Strategy adiciona complexidade desnecessária. Nesse caso, um simples if ou switch é mais adequado.

8.3. Relação com outros padrões da série (Command, Chain, Iterator) e evolução arquitetural

Observer e Strategy frequentemente trabalham com outros padrões comportamentais:
- Command: pode ser usado como estratégia (encapsular uma ação como objeto).
- Chain of Responsibility: pode ser combinado com Strategy para pipelines de processamento.
- Iterator: usado para percorrer coleções de observers ou estratégias.

Em arquiteturas modernas, ambos os padrões evoluem para conceitos mais amplos: Observer para Event Sourcing e CQRS, Strategy para Policy-based Design e Feature Toggles.


Referências