Design de software

Escape do inferno de ifs com o State Pattern do Swift

O State Pattern reúne o comportamento e as transições de cada estado em objetos ou enums, reduzindo condicionais espalhadas. Este artigo explica como refatorar estados de tela em Swift e diferenciar, pela intenção, estruturas que parecem o Strategy Pattern.

5 min de leitura
Imagem de capa de Escape do inferno de ifs com o State Pattern do Swift

Você já precisou abrir if e alterar dez ifs só para mudar um estado de tela?

Quando os estados de carregamento, sucesso e falha são todos tratados com if isLoading e else if hasError, o código rapidamente fica difícil de manter.

Hoje vou explicar, com base em experiências reais, como organizar o inferno de ifs em objetos de estado usando o State Pattern do Swift.

Indo direto ao ponto: o State Pattern significa separar cada estado em um objeto próprio (ou enum) e colocar a lógica de transição dentro desse objeto. Assim, as ramificações condicionais ficam concentradas em um único lugar.

A diferença em relação ao Strategy Pattern, cuja estrutura é quase igual, está em quem conduz a mudança. Strategy Pattern vs. State Pattern permite distinguir primeiro a seleção externa da transição interna de estado.

O que você vai aprender

  1. Por que o inferno de ifs acontece
  2. Duas formas de dividir estados em objetos no Swift
  3. Quando usar enum ou protocol
  4. Uma sequência de refatoração pronta para o trabalho

Por que o inferno de ifs acontece?

No início, com apenas dois estados, if parece suficiente. Mas, conforme os requisitos crescem, eles viram 5 ou 6: carregando, sucesso, falha, tela vazia, tentando novamente e assim por diante.

O problema é que essas combinações de estado ficam espalhadas por vários métodos. Elas se repetem na lógica de ativação de botões como if, na atualização da tela como if e nos callbacks de rede como if. Por isso, ao adicionar um estado, você precisa encontrar e corrigir cada bloco if; esquecer um já é suficiente para criar um bug.

Quando os estados ficam espalhados, os bugs se escondem; quando são reunidos, eles aparecem.

O State Pattern existe justamente para impedir essa “dispersão”.


Como dividir estados em objetos no Swift (abordagem com enum)

A forma mais leve de começar é enum. Você define os estados como valores e pode armazenar os dados necessários com valores associados. Este é um exemplo de estados de tela representados por um enum.

enum ViewState {
    case loading
    case loaded(items: [String]) // Anexar dados em caso de sucesso
    case failed(message: String) // Anexar o motivo da falha
    case empty
}

Agora a atualização da tela termina com switch uma única vez. Graças aos valores associados, não existe mais um estado ambíguo como “sucesso com dados nil”.

switch state {
case .loading:      showSpinner()
case .loaded(let items): render(items)
case .failed(let msg):   showError(msg)
case .empty:        showEmptyView()
}

Para a maioria dos estados de tela, a abordagem com enum é suficiente. switch detecta casos esquecidos em tempo de compilação, deixando a adição de estados mais segura.

Reunir os estados da tela em um único switch deixa o código muito mais fácil de manter
Reunir os estados da tela em um único switch deixa o código muito mais fácil de manter

enum vs. protocol: quando usar cada um?

Se o comportamento de cada estado for muito diferente e as regras de transição forem complexas, a abordagem com protocol é melhor. Cada estado vira um tipo que retorna o próximo estado por conta própria. As diferenças são estas.

Categoria Abordagem com enum Abordagem com protocol
Situação adequada Estados centrados em dados Cada estado tem um comportamento diferente
Adicionar estado Adicionar case Adicionar tipo
Lógica de transição switch externo Encapsulada dentro dos objetos de estado
Curva de aprendizado Baixa Um pouco mais alta

O ponto central da abordagem com protocol é que cada objeto assume a responsabilidade pelas transições de estado.

protocol PlayerState {
    func play() -> PlayerState  // Retornar o próximo estado
    func pause() -> PlayerState
}

struct PlayingState: PlayerState {
    func play() -> PlayerState { self }        // Manter se já estiver reproduzindo
    func pause() -> PlayerState { PausedState() } // Mudar para pausado
}

Assim, regras como “o que acontece se eu pressionar pause durante a reprodução?” existem apenas dentro daquele estado. As ramificações desaparecem, e cada estado precisa conhecer apenas as próprias regras.

Transição com protocol em que cada estado escolhe o próximo estado
Transição com protocol em que cada estado escolhe o próximo estado

Refatoração prática: siga esta ordem

Tentar mudar tudo de uma vez pode assustar. Eu fiz a migração aos poucos, seguindo esta ordem.

  1. Anotar a lista de estados realmente representados pelo if espalhado
  2. Definir esses estados em um único enum
  3. Trocar primeiro o código de atualização da tela por switch
  4. Eliminar de vez com enum as combinações impossíveis, como carregamento e erro ao mesmo tempo
  5. Promover para protocol apenas as partes com regras de transição complexas

O ponto é começar com um enum e promovê-lo para protocol somente quando necessário. Projetar tudo com protocol desde o início pode deixar o código mais pesado.

Fica muito mais fácil desenhar os estados assim antes de levá-los para um enum
Fica muito mais fácil desenhar os estados assim antes de levá-los para um enum

Perguntas frequentes (Q&A)

P. Preciso usar o padrão mesmo com apenas 3 estados?

Não necessariamente. Porém, se os estados estiverem espalhados por vários métodos, vale reuni-los em um enum independentemente da quantidade.

P. Posso usar isso também com SwiftUI?

Sim. Funciona muito bem manter o estado no formato @Published var state: ViewState e fazer a ramificação na view com switch.

P. Um enum não fica bagunçado quando tem muitos valores associados?

Quando passam de 3 valores associados, recomendo agrupá-los em uma struct separada e armazená-los como case loaded(Result).


Pode parecer estranho no começo, mas, depois que você aprende a reunir estados em objetos, não vai querer voltar ao código antigo. Comece organizando uma tela pequena com um enum. Você vai sair do inferno de ifs mais rápido do que imagina. Boa sorte!

Continue lendo