Design de software

Padrão Proxy em Swift: o objeto proxy oculto por trás de lazy

Ao desenvolver para iOS, você acaba usando lazy var com muita frequência.

4 min de leitura
Imagem de capa de Padrão Proxy em Swift: o objeto proxy oculto por trás de lazy

Ao desenvolver para iOS, você acaba usando lazy var com muita frequência.

Eu também era assim. Por hábito, usava isso para adiar a inicialização de objetos pesados.

Então surgiu uma dúvida: lazy não é, na prática, o mesmo Padrão Proxy dos livros de padrões de projeto?

Vamos começar pela conclusão.

lazy é a versão integrada à linguagem de um Virtual Proxy que adia a criação até o momento em que o objeto real é necessário.

Hoje vou explicar o que é o Padrão Proxy em Swift e como esse conceito se esconde atrás da palavra-chave lazy, seguindo a ordem em que o entendi ao trabalhar diretamente com o código.

Veja primeiro o resumo principal.

  1. O Padrão Proxy é um padrão estrutural que controla o acesso colocando um intermediário diante do objeto real.
  2. lazy tem o mesmo objetivo de um Virtual Proxy que adia a criação.
  3. Porém, lazy não consegue fazer controle de acesso ou logging; ele cuida apenas da criação tardia.
  4. Se você precisa de logging ou verificação de permissões, deve criar o objeto proxy diretamente.

Afinal, o que é o Padrão Proxy do Swift?

Proxy significa intermediário.

Você coloca, diante do objeto que realiza o trabalho de verdade, um intermediário com a mesma interface.

Por fora, parece que você chama o objeto real, mas, na prática, passa pelo intermediário.

O intermediário pode simplesmente encaminhar a solicitação ou fazer algo a mais, como verificar permissões, registrar chamadas ou adiar a criação do objeto real.

Há três usos comuns.

  • Virtual Proxy: adiar a criação de objetos pesados até o uso efetivo
  • Protection Proxy: bloquear acessos não autorizados
  • Logging Proxy: registrar o histórico de chamadas

O protagonista de hoje é o primeiro: o Virtual Proxy. É exatamente o ponto de contato com lazy.


A identidade do objeto intermediário oculto por trás de lazy

Vamos imaginar uma situação que lida com imagens em alta resolução.

Carregar imagens é pesado. Carregar tudo antecipadamente, quando nem aparece na tela, desperdiça memória.

Daí surge a ideia de carregar apenas quando for realmente necessário. Esse é o núcleo do Virtual Proxy.

Uma implementação direta usando o Padrão Proxy ficaria assim.

protocol Image { func display() }

// Intermediário: adiar a criação do objeto real até que seja necessário
final class ImageProxy: Image {
    private let filename: String
    private var real: RealImage?          // Ainda não criado
    init(_ filename: String) { self.filename = filename }
    func display() {
        if real == nil { real = RealImage(filename) }  // Criado neste momento
        real?.display()
    }
}

display() só é criado quando é chamado pela primeira vez; nesse momento, RealImage é criado.

Até lá, ImageProxy espera tranquilamente carregando apenas o nome do arquivo. Esse é exatamente o trabalho do intermediário.

Mas o Swift incorporou esse padrão à própria sintaxe. É o lazy.

O momento em que o intermediário segura o objeto real em seu lugar
O momento em que o intermediário segura o objeto real em seu lugar
final class Gallery {
    // é criado uma única vez no primeiro acesso a esta propriedade
    lazy var cover: RealImage = RealImage("cover.png")
}

let g = Gallery()   // ainda RealImage não criado
g.cover.display()   // finalmente criado aqui

No momento em que Gallery() é criado, RealImage ainda não existe.

Ele é criado no primeiro acesso a g.cover. O compilador cria por você a criação tardia que ImageProxy fazia acima.


Então, se existe lazy, não precisamos do Padrão Proxy?

Essa também foi a minha primeira dúvida.

A resposta é não.

lazy faz apenas a criação tardia.

Ele não consegue bloquear acessos, registrar chamadas nem retornar objetos diferentes conforme a condição.

Resumi a diferença em uma tabela.

Item Propriedade lazy Proxy criado manualmente
Criação tardia Sim Sim
Verificação de permissão de acesso Não Sim
Logging e cache de chamadas Não Sim
Quantidade de código Uma linha Uma classe
Reutilização Limitada à propriedade Reutilizável em vários lugares

Por isso, o critério é simples.

Se você só precisa “adiar a criação”, lazy uma linha é a resposta. Não há motivo para criar uma classe.

Por outro lado, se houver permissões, logging ou uma chamada remota para encapsular, você precisa criar um objeto proxy diretamente.

Quando preciso adiar uma inicialização pesada, esta é sempre a primeira linha que me vem à mente
Quando preciso adiar uma inicialização pesada, esta é sempre a primeira linha que me vem à mente

Há um ponto importante de atenção.

lazy não garante segurança de thread.

Se várias threads acessarem pela primeira vez ao mesmo tempo, a inicialização poderá ocorrer duas vezes. Em um ambiente multithread, é mais seguro envolver essa parte em um proxy próprio e cuidar da sincronização.


Resumo das perguntas frequentes

P. Qual é a diferença entre lazy e uma computed property?

lazy é calculada uma única vez e armazena o valor. Uma computed property é recalculada a cada acesso. Se quiser fazer uma inicialização pesada apenas uma vez, lazy é a opção certa.

P. Por que lazy só pode ser declarado como var?

Como a inicialização ocorre depois, uma instância cujo valor ainda não foi definido existe brevemente. let não permite isso, então lazy let é impossível pela sintaxe.

Criar uma classe intermediária própria deixa claro de imediato o que lazy faz por você
Criar uma classe intermediária própria deixa claro de imediato o que lazy faz por você

Em resumo, lazy é como um presente no qual a linguagem já empacotou o Virtual Proxy do Padrão Proxy.

Não é preciso criar uma classe intermediária para algo que cabe em uma linha, mas entender o conceito escondido por trás dela muda claramente sua forma de ler o código.

A partir de hoje, ao usar lazy var, pense: “Ah, estou colocando um intermediário aqui.” O padrão parecerá muito mais próximo.

Continue lendo