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
- Observer Pattern - Refactoring Guru — Explicação detalhada com diagramas, exemplos em múltiplas linguagens e casos de uso práticos.
- Strategy Pattern - Refactoring Guru — Guia completo sobre o padrão Strategy, incluindo implementação e quando aplicá-lo.
- Observer Pattern - SourceMaking — Tutorial com exemplos em Java e discussão sobre variações push/pull.
- Strategy Pattern - SourceMaking — Descrição detalhada com exemplos de código e trade-offs entre Strategy e outras abordagens.
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — Livro clássico que define ambos os padrões, com exemplos originais em C++ e Smalltalk.
- Observer vs Strategy - Baeldung — Artigo comparativo com exemplos em Java, destacando diferenças e cenários de uso combinado.
- Event-Driven Architecture - Martin Fowler — Artigo que explora como o Observer evolui para arquiteturas orientadas a eventos em sistemas modernos.