MÓDULO 15 · CONCEITO 12 DE 12

Design Patterns II — Behavioral

Strategy, Observer, Command, Template Method, Chain of Responsibility, State — relação com SOLID, alternativas funcionais, e o risco de pattern matching sem problema

Tempo de leitura ~22 min Pré-requisito 11 · Design Patterns I Próximo Módulo 01 · Fundamentos II — Sistema

Padrões comportamentais lidam com comunicação e responsabilidade entre objetos — como eles interagem, como responsabilidades são distribuídas, e como algoritmos são encapsulados. Se os padrões estruturais descrevem a forma do código (como objetos se compõem), os comportamentais descrevem o movimento do código (como objetos colaboram). São os padrões mais frequentemente encontrados em código de produção — e os mais frequentemente mal-aplicados, porque suas motivações são mais sutis.

Strategy — algoritmo intercambiável

Strategy define uma família de algoritmos, encapsula cada um, e os torna intercambiáveis. O problema que resolve: um bloco de código com um if ou switch que seleciona entre diferentes comportamentos, e que vai crescer sempre que um novo comportamento for adicionado (violando o Open/Closed Principle). Strategy extrai cada ramo para uma classe separada que implementa uma interface comum.

O exemplo mais natural é política de desconto: em vez de if tier == "gold" then rate = 0.10 else if tier == "silver" then rate = 0.05 dentro do serviço de pedidos, você tem IDiscountStrategy implementado por GoldDiscountStrategy e SilverDiscountStrategy. Adicionar um novo tier é adicionar uma nova implementação, sem tocar no código existente.

Em linguagens funcionais ou com suporte a first-class functions (Python, Go, JavaScript, C# com delegates), Strategy frequentemente não precisa de classes. Uma função passada como parâmetro é Strategy sem o overhead de interface.

Observer — publicar sem conhecer os assinantes

Observer define uma dependência um-para-muitos onde quando um objeto muda de estado, todos os seus dependentes são notificados automaticamente. O problema: um objeto precisa notificar outros sem saber quais são nem quantos. Se você hardcodar as notificações, cria acoplamento entre o publicador e todos os consumidores — e toda vez que um novo consumidor é adicionado, o publicador precisa mudar.

Observer é a base de praticamente todos os sistemas de eventos e mensageria. Domain Events em DDD são Observer em escala de domínio. Pub/sub em sistemas distribuídos é Observer em escala de rede. Event Emitters em Node.js, EventBus em Android, IObservable em C# — são todos Observer.

Command — encapsular requisições

Command encapsula uma requisição como um objeto. O que isso possibilita: enfileirar requisições (queue de comandos), suportar undo/redo (cada comando sabe como desfazer-se), logar operações (o objeto command é serializado), e suportar transações.

No contexto de CQRS e MediatR, o "Command" do padrão arquitetural é literalmente o padrão GoF Command. Um PlaceOrderCommand com seu PlaceOrderHandler é Command + Command Handler. A arquitetura toda de CQRS é construída em cima desse padrão.

Template Method — esqueleto de algoritmo

Template Method define o esqueleto de um algoritmo numa classe base, deixando que subclasses preencham os detalhes específicos sem mudar a estrutura geral. O problema: múltiplas implementações compartilham a mesma sequência de passos, mas diferem nos detalhes de alguns passos.

Exemplo: um importador de dados que sempre faz: abrir arquivo → ler registros → validar → transformar → salvar. A sequência é sempre essa. O que varia é como validar e transformar (diferente para CSV vs JSON vs XML). Template Method coloca a sequência na classe base; subclasses implementam os passos variáveis.

Em linguagens com higher-order functions, Template Method frequentemente vira uma função que recebe callbacks para os passos variáveis — sem herança. É mais flexível e mais testável.

Chain of Responsibility — pipeline de handlers

Chain of Responsibility passa uma requisição ao longo de uma cadeia de handlers, onde cada um decide processar ou passar adiante. O problema: múltiplos objetos podem tratar uma requisição, mas você não sabe qual deles tratará em tempo de compilação. Ou você quer processar a requisição em múltiplas etapas, onde cada etapa pode interromper o pipeline.

O middleware pipeline do ASP.NET Core é Chain of Responsibility. O logging pipeline com múltiplos handlers é Chain of Responsibility. Middlewares de autenticação, autorização, rate limiting em sequência — todos Chain of Responsibility.

State — comportamento varia com o estado

State permite que um objeto altere seu comportamento quando seu estado interno muda. O problema: um objeto tem comportamento que varia significativamente dependendo de seu estado, e você está gerenciando isso com uma série crescente de if (status == "draft") ... else if (status == "confirmed") ... espalhados pelo código. State extrai cada estado em uma classe que implementa o comportamento para aquele estado.

Uma Order com estados Draft, Confirmed, Shipped, Cancelled — onde cada estado tem operações válidas diferentes — é candidata ao padrão State. Em vez de verificar o status em cada método, o método delega ao objeto de estado atual.

Mediator — comunicação centralizada

Mediator define um objeto que encapsula como um conjunto de objetos interage, promovendo acoplamento fraco ao impedir que objetos se refiram explicitamente uns aos outros. O problema: muitos objetos precisam se comunicar entre si, criando uma teia de dependências difícil de manter. O Mediator centraliza essa comunicação.

MediatR em .NET é literalmente uma implementação do padrão Mediator — handlers se registram no mediator, e qualquer código que precise disparar uma ação publica para o mediator sem saber quem vai tratar.

Implementação dos padrões comportamentais mais usados

// Strategy — algoritmo intercambiável via interface
public interface IDiscountStrategy { Money Calculate(Order order, Customer customer); }

public class GoldTierDiscount : IDiscountStrategy
{
    public Money Calculate(Order order, Customer customer)
        => new(order.Total.Amount * 0.10m, order.Total.Currency);
}

public class VolumeDiscount : IDiscountStrategy
{
    public Money Calculate(Order order, Customer customer)
        => order.Total.Amount > 1000 ? new(order.Total.Amount * 0.05m, order.Total.Currency) : Money.Zero("BRL");
}

// Composição de strategies
public class CompositeDiscount : IDiscountStrategy
{
    private readonly IReadOnlyList<IDiscountStrategy> _strategies;
    public Money Calculate(Order order, Customer customer)
        => _strategies.Aggregate(Money.Zero("BRL"), (acc, s) => acc.Add(s.Calculate(order, customer)));
}

// Observer — Domain Events como implementação de Observer
public interface IDomainEventHandler<TEvent> where TEvent : IDomainEvent
{
    Task HandleAsync(TEvent @event, CancellationToken ct = default);
}

public class OrderPlacedEmailNotifier : IDomainEventHandler<OrderPlacedEvent>
{
    private readonly IEmailSender _email;
    public async Task HandleAsync(OrderPlacedEvent ev, CancellationToken ct)
        => await _email.SendAsync(ev.CustomerEmail, "Pedido confirmado", BuildBody(ev));
}

public class OrderPlacedInventoryUpdater : IDomainEventHandler<OrderPlacedEvent>
{
    private readonly IInventory _inventory;
    public async Task HandleAsync(OrderPlacedEvent ev, CancellationToken ct)
        => await _inventory.ReserveAsync(ev.OrderId, ev.Lines, ct);
}
// Ambos registrados como handlers — publicador não sabe de nenhum deles

// Chain of Responsibility — middleware pipeline
public interface IHandler<TRequest> { Task HandleAsync(TRequest request, Func<Task> next); }

public class AuthorizationHandler : IHandler<HttpContext>
{
    public async Task HandleAsync(HttpContext ctx, Func<Task> next)
    {
        if (!ctx.User.Identity!.IsAuthenticated) { ctx.Response.StatusCode = 401; return; }
        await next(); // passa adiante se autenticado
    }
}

public class RateLimitHandler : IHandler<HttpContext>
{
    public async Task HandleAsync(HttpContext ctx, Func<Task> next)
    {
        if (IsRateLimited(ctx)) { ctx.Response.StatusCode = 429; return; }
        await next();
    }
}

// State — comportamento varia com status do pedido
public abstract class OrderState
{
    public abstract void AddLine(Order order, OrderLine line);
    public abstract void Confirm(Order order);
    public abstract void Cancel(Order order);
}

public class DraftState : OrderState
{
    public override void AddLine(Order order, OrderLine line) => order.AddLineInternal(line);
    public override void Confirm(Order order) => order.TransitionTo(new ConfirmedState());
    public override void Cancel(Order order) => order.TransitionTo(new CancelledState());
}

public class ConfirmedState : OrderState
{
    public override void AddLine(Order order, OrderLine line)
        => throw new InvalidOperationException("Pedido confirmado não aceita linhas");
    public override void Confirm(Order order)
        => throw new InvalidOperationException("Pedido já confirmado");
    public override void Cancel(Order order) => order.TransitionTo(new CancelledState());
}
from abc import ABC, abstractmethod
from typing import Callable, Awaitable

# Strategy — em Python, funções são citizens de primeira classe
# A versão com classe é mais explícita; a funcional é mais leve

# Versão funcional (mais pythônica)
DiscountFn = Callable[["Order", "Customer"], float]

def gold_discount(order: "Order", customer: "Customer") -> float:
    return order.total * 0.10 if customer.tier == "gold" else 0.0

def volume_discount(order: "Order", customer: "Customer") -> float:
    return order.total * 0.05 if order.total > 1000 else 0.0

def compose_discounts(*strategies: DiscountFn) -> DiscountFn:
    def combined(order, customer):
        return sum(s(order, customer) for s in strategies)
    return combined

# Uso
discount = compose_discounts(gold_discount, volume_discount)
savings = discount(order, customer)

# Versão com classe (mais explícita, mais fácil de testar individualmente)
class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, order: "Order", customer: "Customer") -> float: ...

class GoldTierDiscount(DiscountStrategy):
    def calculate(self, order, customer): return order.total * 0.10

# Observer — com lista de handlers registrados
class EventBus:
    def __init__(self): self._handlers: dict[type, list] = {}

    def subscribe(self, event_type: type, handler) -> None:
        self._handlers.setdefault(event_type, []).append(handler)

    async def publish(self, event: object) -> None:
        import asyncio
        handlers = self._handlers.get(type(event), [])
        await asyncio.gather(*(h(event) for h in handlers))

# Uso
bus = EventBus()
bus.subscribe(OrderPlacedEvent, send_confirmation_email)
bus.subscribe(OrderPlacedEvent, update_inventory)
# publisher chama bus.publish(event) sem saber dos handlers

# Chain of Responsibility — middleware ASGI/WSGI
from typing import Protocol

class Middleware(Protocol):
    async def __call__(self, request: dict, call_next) -> dict: ...

class AuthMiddleware:
    def __init__(self, app): self._app = app
    async def __call__(self, request, call_next):
        if not request.get("user"):
            return {"status": 401, "body": "Unauthorized"}
        return await call_next(request)

class LoggingMiddleware:
    def __init__(self, app): self._app = app
    async def __call__(self, request, call_next):
        print(f"→ {request['method']} {request['path']}")
        response = await call_next(request)
        print(f"← {response['status']}")
        return response

# Composição da cadeia
def build_chain(handler, *middlewares):
    for mw_class in reversed(middlewares):
        handler = mw_class(handler)
    return handler

app = build_chain(order_handler, LoggingMiddleware, AuthMiddleware)

# State — estado como objeto
class OrderState(ABC):
    @abstractmethod
    def add_line(self, order: "Order", line) -> None: ...
    @abstractmethod
    def confirm(self, order: "Order") -> None: ...

class DraftState(OrderState):
    def add_line(self, order, line): order._lines.append(line)
    def confirm(self, order): order._state = ConfirmedState()

class ConfirmedState(OrderState):
    def add_line(self, order, line): raise ValueError("Pedido já confirmado")
    def confirm(self, order): raise ValueError("Pedido já confirmado")

class Order:
    def __init__(self): self._lines = []; self._state: OrderState = DraftState()
    def add_line(self, line): self._state.add_line(self, line)
    def confirm(self): self._state.confirm(self)
// Strategy — interfaces Go são pequenas e implícitas (duck typing)
type DiscountStrategy interface {
    Calculate(order *Order, customer *Customer) float64
}

type GoldDiscount struct{}
func (GoldDiscount) Calculate(o *Order, c *Customer) float64 {
    if c.Tier == "gold" { return o.Total * 0.10 }
    return 0
}

// Go favorece funções como strategy quando não há estado
type DiscountFn func(order *Order, customer *Customer) float64

func ComposeDiscounts(fns ...DiscountFn) DiscountFn {
    return func(o *Order, c *Customer) float64 {
        var total float64
        for _, fn := range fns { total += fn(o, c) }
        return total
    }
}

// Observer — channel-based (Go idiomático)
type EventBus struct {
    handlers map[string][]func(any)
    mu       sync.RWMutex
}

func (b *EventBus) Subscribe(eventType string, handler func(any)) {
    b.mu.Lock(); defer b.mu.Unlock()
    b.handlers[eventType] = append(b.handlers[eventType], handler)
}

func (b *EventBus) Publish(eventType string, event any) {
    b.mu.RLock(); handlers := b.handlers[eventType]; b.mu.RUnlock()
    for _, h := range handlers { go h(event) } // fire-and-forget; adicionar WaitGroup se necessário
}

// Chain of Responsibility — middleware HTTP (padrão Go idiomático)
type Middleware func(http.Handler) http.Handler

func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "unauthorized", 401); return
        }
        next.ServeHTTP(w, r)
    })
}

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("→ %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

// Uso: handler := Chain(ordersHandler, LoggingMiddleware, AuthMiddleware)

// State — interface com transição explícita
type orderState interface {
    addLine(o *Order, line OrderLine) error
    confirm(o *Order) error
}

type draftState struct{}
func (draftState) addLine(o *Order, line OrderLine) error { o.lines = append(o.lines, line); return nil }
func (draftState) confirm(o *Order) error { o.state = confirmedState{}; return nil }

type confirmedState struct{}
func (confirmedState) addLine(o *Order, _ OrderLine) error { return errors.New("pedido confirmado") }
func (confirmedState) confirm(*Order) error                { return errors.New("já confirmado") }

type Order struct { lines []OrderLine; state orderState }
func NewOrder() *Order                         { return &Order{state: draftState{}} }
func (o *Order) AddLine(l OrderLine) error     { return o.state.addLine(o, l) }
func (o *Order) Confirm() error                { return o.state.confirm(o) }

Alternativas funcionais a padrões GoF

Em linguagens com suporte a funções de primeira classe (todas as linguagens modernas têm isso), muitos padrões GoF podem ser substituídos por funções. Strategy vira uma função passada como parâmetro. Template Method vira uma função que recebe callbacks. Observer vira uma lista de funções registradas. Command vira uma função armazenada.

Isso não invalida os padrões — invalida a necessidade de criar hierarquias de classe para implementá-los. O conceito do padrão permanece válido: "há uma família de algoritmos intercambiáveis" ainda é Strategy, mesmo que seja implementado como uma função. Conhecer o nome do padrão ainda facilita a comunicação.

O risco de pattern matching

O perigo de conhecer bem os Design Patterns é começar a ver o problema que cada padrão resolve onde não há esse problema. Você vê uma lista de ifs e pensa "isso deveria ser um Strategy". Você vê dois objetos colaborando e pensa "isso deveria ser um Observer". Você cria classes para os padrões antes de ter o problema que os justifica.

A heurística saudável: se você não consegue articular exatamente qual problema o padrão resolve para o código em questão — qual rigidez ele elimina, qual extensibilidade ele habilita, qual acoplamento ele remove — você provavelmente está impondo o padrão em vez de descobri-lo. Três linhas similares são mais legíveis do que uma abstração prematura.

regra final

Design Patterns são nomes para soluções que você já precisaria inventar. Use-os quando o problema que o padrão resolve for o problema que você tem. O objetivo é o código que expressa a intenção claramente — não o código que demonstra conhecimento de padrões.

Referências

  1. Gamma, E. et al. — Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. Caps 5: Behavioral Patterns — o texto original.
  2. Freeman, E.; Freeman, E. — Head First Design Patterns, 2ª ed. O'Reilly, 2020. Strategy, Observer, Command com exemplos visuais.
  3. Kerievsky, J. — Refactoring to Patterns. Addison-Wesley, 2004. Como chegar aos behavioral patterns via refatoração.
  4. Martin, R. C. — Agile Software Development. Prentice Hall, 2002. Open/Closed Principle e Strategy como consequência.
  5. Fowler, M. — "Replacing Conditional Dispatcher with Command". martinfowler.com. Command emergindo de refatoração.
  6. Dahan, U. — "Clarified CQRS". udidahan.com, 2009. Command em contexto de CQRS.
  7. Richardson, C. — Microservices Patterns. Manning, 2018. Chain of Responsibility em API Gateways.
  8. Seemann, M. — "Functional design is intrinsically testable". blog.ploeh.dk, 2015. Alternativas funcionais a behavioral patterns.
  9. Wlaschin, S. — Domain Modeling Made Functional. Pragmatic Bookshelf, 2018. Como padrões funcionais substituem behavioral GoF.
  10. Refactoring.Guru — refactoring.guru/design-patterns/behavioral-patterns. Referência visual completa.
  11. Nesteruk, D. — Design Patterns in .NET Core. Apress, 2019. Implementações C# modernas dos behavioral patterns.
  12. Holub, A. — "Why I Hate Frameworks". holub.com, 2004. O argumento contra over-engineering com padrões.