Design de software

Padrão Strategy no Swift: trocando algoritmos com protocolos e closures

Você já se deparou com um código em que os if-else não paravam de crescer para cada forma de pagamento?

4 min de leitura
Imagem de capa de Padrão Strategy no Swift: trocando algoritmos com protocolos e closures

Você já se deparou com um código em que os if-else não paravam de crescer para cada forma de pagamento?

Quando opções de ordenação, cálculo de descontos e lógica de filtros começam a se misturar em um único switch enorme, fica cada vez mais assustador mexer no código.

É nessa hora que entra o padrão Strategy do Swift.

Hoje vamos explicar, sob uma perspectiva prática, como trocar algoritmos como se fossem componentes usando protocolos e closures.

Vamos começar pelo essencial.

O padrão Strategy separa o que fazer de como fazer, permitindo trocar algoritmos externamente.

No Swift, isso pode ser implementado de duas formas: com protocolos, uma abordagem mais pesada, ou com closures, uma abordagem mais leve.

Vamos mostrar cada uma delas a seguir.


O que é o padrão Strategy? Resumo em 3 linhas

Vamos definir a estrutura antes de entrar em teorias complicadas.

  1. Agrupar os algoritmos que serão trocados sob uma especificação comum (protocolo)
  2. Criar o comportamento real como objetos de estratégia ou closures separados
  3. Quem usa conhece apenas a especificação e não precisa conhecer os detalhes concretos

É como trocar a arma de um personagem de jogo.

O personagem só precisa saber “atacar”; seja espada, arco ou magia, a arma equipada cuida do resto.

Você troca apenas a arma sem alterar o código do personagem.

Essa troca é praticamente todo o padrão Strategy.


Implementando com protocolo (a abordagem clássica)

Vamos começar pela abordagem com protocolo mais tradicional.

Usaremos o cálculo de descontos como exemplo. Há vários métodos de cálculo, como membro comum, membro VIP e aplicação de cupom.

O código abaixo define a especificação comum que toda estratégia deve seguir e uma estratégia concreta.

// Especificação comum que toda estratégia de desconto deve seguir
protocol DiscountStrategy {
    func discount(for price: Int) -> Int
}

// VIP Estratégia: 20% desconto
struct VIPDiscount: DiscountStrategy {
    func discount(for price: Int) -> Int { price * 20 / 100 }
}

Agora vamos ver quem armazena e usa essa estratégia.

O objeto cliente não se importa com qual estratégia recebe.

Checkout conhece apenas a especificação, não o interior
Checkout conhece apenas a especificação, não o interior
struct Checkout {
    var strategy: DiscountStrategy   // Local onde a estratégia pode ser trocada

    func finalPrice(_ price: Int) -> Int {
        price - strategy.discount(for: price)
    }
}

// Trocar apenas a estratégia no momento do pagamento
let cart = Checkout(strategy: VIPDiscount())

Mesmo com uma nova política de descontos, você não precisa alterar Checkout.

Basta criar uma nova estrutura que siga DiscountStrategy.

Ampliar funcionalidades sem tocar no código existente: essa é a maior vantagem percebida no dia a dia.


Implementando com closures (a abordagem idiomática do Swift)

Mas criar uma estrutura toda vez que a estratégia é simples acaba sendo trabalhoso.

Envolver toda lógica simples em um protocolo pode deixar o código mais verboso.

Nesses casos, closures são muito mais leves e naturais no Swift.

struct Checkout {
    // Receber a própria estratégia como uma função
    var discount: (Int) -> Int

    func finalPrice(_ price: Int) -> Int {
        price - discount(price)
    }
}

// Definir a estratégia diretamente no local
let cart = Checkout(discount: { $0 * 20 / 100 })

Sem declarar um tipo separado, você pode escrever a lógica no momento em que a passa.

Para trocar algoritmos leves, como critérios de ordenação, condições de filtro e transformações simples, essa abordagem é muito mais elegante.

Até sorted(by:) é, na prática, o padrão Strategy que usamos todos os dias
Até sorted(by:) é, na prática, o padrão Strategy que usamos todos os dias

Na verdade, sorted(by:) da biblioteca padrão é exatamente esse padrão Strategy baseado em closure.

A estrutura da ordenação permanece fixa, e apenas o critério de comparação é trocado por um closure.

Ou seja, já o usamos todos os dias.


Protocolos e closures: quando usar cada um?

Organizei em uma tabela os critérios que uso no trabalho.

Categoria Abordagem com protocolo Abordagem com closure
Situação adequada Estratégia complexa ou com estado Estratégia curta e simples
Reutilização Fácil de reutilizar em vários lugares Boa para uso pontual
Quantidade de código Exige declaração de tipo; maior Definição no local; menor
Testes Fácil de validar como tipo individual Validar apenas a lógica de forma simples
Legibilidade A intenção fica clara pelo nome Clara quando curta, confusa quando longa

Este é o meu critério.

Se a estratégia possui estado interno, é reutilizada em várias telas ou precisa expressar claramente sua intenção pelo nome, use um protocolo.

Se você só troca na hora uma lógica de uma ou duas linhas, closure é a escolha certa.

Não há problema algum em combinar os dois. Uso protocolos para políticas maiores e closures para opções detalhadas.


Perguntas frequentes (Q&A)

P. Qual é a diferença entre o padrão Strategy e herança (sobrescrita)?

A herança fica vinculada à classe pai, então o comportamento é fixado em tempo de compilação.

Já o padrão Strategy permite trocar estratégias livremente durante a execução e não depende de uma hierarquia de classes.

P. Qual é a diferença em relação a usar o switch de um enum?

Com switch, é preciso abrir e modificar o código existente sempre que surge um novo caso.

Com o padrão Strategy, basta “adicionar” uma nova estratégia, reduzindo a necessidade de alterar o código existente.


No fim, a essência do padrão Strategy é simples: mover as partes variáveis para fora e facilitar sua troca.

Usando os protocolos e closures de hoje conforme a situação, você certamente escapará daquele inferno cansativo de if-else. Comece refatorando um pequeno trecho de código!

Continue lendo