Design de software

Padrão Visitor no Swift: quando o double dispatch é necessário

No desenvolvimento para iOS, chega um momento em que você precisa continuar adicionando operações a objetos de vários tipos, como formas ou nós.

4 min de leitura
Imagem de capa de Padrão Visitor no Swift: quando o double dispatch é necessário

No desenvolvimento para iOS, chega um momento em que você precisa continuar adicionando operações a objetos de vários tipos, como formas ou nós.

É comum inspecionar os tipos um por um com if let e as?. Cada novo tipo exige alterar novamente as ramificações.

Este artigo explica, com exemplos, o padrão Visitor do Swift e quando exatamente seu núcleo, o double dispatch, é necessário.

Ao escolher um método, o Swift considera apenas o tipo dinâmico do receiver, o objeto que recebe a mensagem.

Quando tanto “qual forma?” quanto “qual operação?” podem mudar, o dispatch simples não basta. O double dispatch determina os dois tipos em runtime por meio de duas chamadas.


Dispatch de métodos no Swift: por que um só não basta?

Ao chamar shape.draw() no Swift, é executado shape correspondente ao tipo real de draw().

Isso é dispatch simples: o método é escolhido usando apenas o tipo do receiver, ****.

O problema aparece quando há várias operações.

As formas incluem círculos, retângulos e triângulos; as operações incluem desenhar, calcular área e exportar JSON. Ao combinar os dois eixos, as combinações aumentam rapidamente.

Por isso, é comum escrever um código assim.

// Ele cresce cada vez que operações são adicionadas para uma forma if-else
func export(_ shape: Shape) -> String {
    if let c = shape as? Circle { return drawCircle(c) }
    if let r = shape as? Rect   { return drawRect(r) }
    return "" // É preciso alterar este trecho sempre que surge uma forma nova
}

E quando uma nova forma é adicionada? Você precisa abrir esta função e inserir outra ramificação.

Com cinco operações, é como ter cinco funções desse tipo.

Quando os if-else se acumulam assim, eu vejo isso como um sinal.
Quando os if-else se acumulam assim, eu vejo isso como um sinal.

Quando o double dispatch é necessário?

Meu critério é simples.

Quando o comportamento de um método depende simultaneamente de dois tipos, esse é o momento de usar double dispatch.

O exemplo anterior é exatamente assim: o código depende tanto do tipo da forma quanto do tipo da operação.

O dispatch padrão do Swift considera apenas o tipo do receiver, então o outro eixo acaba sendo tratado manualmente com verificações como as?.

Essa ramificação manual é um code smell.

O double dispatch deixa até a decisão do segundo tipo por conta da sobrecarga da linguagem. Assim, você não precisa inspecionar os tipos manualmente.

Por outro lado, se houver apenas uma operação ou se os tipos não forem crescer, não há motivo para usar este padrão.


Como implementar o padrão Visitor no Swift?

O ponto central é dividir a chamada em duas. Por isso ele se chama double dispatch.

protocol Shape { func accept(_ v: Visitor) -> String }
struct Circle: Shape { func accept(_ v: Visitor) -> String { v.visit(self) } }
struct Rect: Shape   { func accept(_ v: Visitor) -> String { v.visit(self) } }

protocol Visitor {
    func visit(_ c: Circle) -> String   // Aqui o tipo da forma é determinado
    func visit(_ r: Rect) -> String      // A operação é determinada pela sobrecarga
}

Vamos acompanhar o fluxo.

Na primeira chamada, shape.accept(v), o tipo real de forma é determinado. Em runtime, escolhe-se chamar o Circle de accept ou o de Rect.

Na segunda chamada, v.visit(self), o tipo estático de self já está definido como Circle ou Rect. Assim, o visit sobrecarregado é selecionado corretamente.

As duas chamadas se sobrepõem e determinam os dois tipos com precisão.

O ponto de encontro das duas chamadas: double dispatch
O ponto de encontro das duas chamadas: double dispatch

Para adicionar uma operação, basta criar uma nova struct que adote Visitor. O código existente das formas permanece intacto.


Padrão Visitor: usar ou não? Comparando os prós e contras

Nenhum padrão é perfeito. Vamos analisar os trade-offs.

Categoria Ramificação por tipo (as? / switch) Padrão Visitor
Adicionar uma operação Alterar ramificações em vários lugares Adicionar um novo Visitor
Adicionar um novo tipo Adicionar uma ramificação Alterar todos os Visitor
Legibilidade do código Fica complexo à medida que as ramificações crescem Separação clara por operação
Custo inicial de implementação Baixo Relativamente alto

Como mostra a tabela, o padrão Visitor brilha quando as operações aumentam com frequência e os tipos permanecem estáveis.

Por outro lado, se os tipos (formas) continuarem aumentando, ele dará mais trabalho, pois todos os Visitor precisarão ser alterados.

accept e visit: apenas duas chamadas
accept e visit: apenas duas chamadas

Perguntas frequentes (Q&A)

P. O Swift tem generics. Ainda assim, o Visitor é necessário?

Generics são poderosos quando o tipo é definido em tempo de compilação. Mas quando tipos reais se misturam em runtime, como em um array [Shape], o double dispatch continua útil.

P. Não dá para usar enum e switch também?

Dá. Se os tipos forem fixos, enum + switch é mais conciso. Visitor se encaixa melhor quando as operações continuam aumentando.


Ao encontrar um comportamento que depende simultaneamente de dois tipos, é hora de pensar em double dispatch e no padrão Visitor do Swift.

Se você tem um código em que as as?ramificações continuam aumentando, experimente refatorá-lo com o exemplo de hoje. A estrutura ficará muito mais fácil de manter 🙂

Leia também