Swift: contêineres para armazenar valores-chave


Imagine que você precisa salvar o ID do usuário em UserDefaults . Qual será o primeiro passo?


Normalmente, um negócio começa adicionando uma constante para uma chave e verificando sua singularidade. Isso é verdade para a maioria dos outros armazenamentos de valores-chave. E as conseqüências do design primitivo de tais repositórios não se limitam às chaves; uma interface na forma de um conjunto de métodos não sistemático leva a vários problemas possíveis:


  • Erros ao escrever chaves : chaves diferentes podem ser usadas para ler e escrever a mesma entidade.
  • Tipo de valor não corrigido : por exemplo, pela mesma chave, você pode escrever um número e ler uma string.
  • Colisão de chaves : diferentes entidades com a mesma chave podem ser registradas em diferentes partes do projeto.

Em um mundo ideal, o compilador deve se proteger contra esses problemas, não permitindo que você construa um projeto se houver um conflito de chave ou se o tipo de valor não corresponder. Para implementar esse armazenamento seguro, você pode usar contêineres para valores, e este passo a passo o ajudará a prepará-los usando UserDefaults como exemplo.


Protocolo de armazenamento


Portanto, a primeira coisa que precisamos é de um protocolo para o próprio repositório, o que ajudará a abstrair de seu tipo. Portanto, teremos uma interface única para trabalhar com UserDefaults e com um chaveiro e com algum outro armazenamento de valor-chave. Este protocolo parece bem simples:


 protocol KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? func setValue<T: Codable>(_ value: T?, forKey key: String) } 

Portanto, qualquer armazenamento que esteja em conformidade com o protocolo KeyValueStorage deve implementar dois métodos genéricos : getter e setter de valores-chave na forma de uma string. Ao mesmo tempo, os próprios valores correspondem ao protocolo Codable , que permite armazenar instâncias de tipos que possuem uma representação universal (por exemplo, JSON ou PropertyList ).


As implementações padrão do armazém de dados não suportam o tipo Codable para valores, por exemplo, os mesmos UserDefaults . Portanto, essa distinção de tipo é um bônus colateral que permite armazenar primitivas do Swift (números, seqüências de caracteres etc.) e estruturas de dados inteiras, mantendo a interface de armazenamento simples.


Implementação de protocolo


Existem duas maneiras de implementar o protocolo KeyValueStorage :


  • Assine o armazenamento existente sob o protocolo e adicione os métodos necessários:

 extension UserDefaults: KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? { //   } func setValue<T: Codable>(_ value: T?, forKey key: String) { //   } } 

  • Embrulhe o armazenamento em um tipo separado, ocultando seus campos para uso externo:

 class PersistentStorage: KeyValueStorage { private let userDefaults: UserDefaults let suiteName: String? let keyPrefix: String init?(suiteName: String? = nil, keyPrefix: String = "") { guard let userDefaults = UserDefaults(suiteName: suiteName) else { return nil } self.userDefaults = userDefaults self.suiteName = suiteName self.keyPrefix = keyPrefix } func value<T: Codable>(forKey key: String) -> T? { //   } func setValue<T: Codable>(_ value: T?, forKey key: String) { //   } } 

O segundo método parece mais complicado, mas dimensiona melhor, por exemplo, você pode definir o prefixo da chave para cada instância de armazenamento. Além disso, o wrapper oculta campos extras e deixa apenas os campos de protocolo disponíveis, resolvendo possíveis problemas de conflitos de assinatura.


Para implementar os métodos value(forKey:) e setValue(:forKey:) é importante fornecer compatibilidade de dados. Isso é necessário para que os valores armazenados pelas ferramentas padrão UserDefaults possam ser recuperados pelos métodos do KeyValueStorage e vice-versa.


Um exemplo completo da classe PersistentStorage pronto para uso está disponível aqui .


Contentor de valor


Agora que abstraímos do tipo de armazenamento, adicionaremos um contêiner para o valor por chave. Será útil para encapsular todos os campos necessários em uma entidade conveniente, que pode ser transferida e usada separadamente do próprio armazenamento. Esse contêiner é implementado como uma pequena classe genérica :


 class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String var value: T? { get { storage.value(forKey: key) } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String) { self.storage = storage self.key = key } } 

O tipo de valor para o contêiner é limitado pelo protocolo Codable da mesma maneira que nos métodos do próprio armazenamento, portanto, a propriedade value calculada simplesmente procura por proxies chamadas com uma chave fixa e um tipo de valor.


Se desejar, você pode expandir a funcionalidade do contêiner, por exemplo, adicione um valor padrão que será usado se não houver dados para a chave na loja.


Exemplo de implementação de um contêiner com um valor padrão
 class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String let defaultValue: T? var value: T? { get { storage.value(forKey: key) ?? defaultValue } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String, defaultValue: T? = nil) { self.storage = storage self.key = key self.defaultValue = defaultValue } } 

O contêiner resolve nossos dois primeiros problemas - ele corrige a chave e o tipo dos dados que estão sendo lidos e gravados. Portanto, qualquer tentativa de escrever um valor de um tipo incorreto será interrompida pelo compilador no estágio de construção:


 func doSomething(with container: KeyValueContainer<Int>) { container.value = "Text" //   } 

Resta resolver o último problema - garantir a exclusividade das chaves de armazenamento. Essa preocupação também pode ser "pendurada" no compilador usando um truque bastante interessante.


Exclusividade chave


Para resolver o problema do conflito, usamos o nome da propriedade computada na qual o próprio contêiner será criado como chave. Para fazer isso, adicione uma extensão simples ao nosso protocolo KeyValueStorage :


 extension KeyValueStorage { func makeContainer<T: Codable>(key: String = #function) -> KeyValueContainer<T> { KeyValueContainer(storage: self, key: key) } } 

Portanto, em todas as implementações do protocolo de armazenamento, é adicionado um método genérico que retorna um contêiner com a chave especificada. De particular interesse nesse método é o parâmetro key , que por padrão tem um valor igual à expressão especial #function ( documentation ). Isso significa que, no estágio de construção, o literal #function substituído pelo nome da declaração a partir da qual o makeContainer(key:) foi chamado.


Essa construção permite declarar contêineres em extensões de armazenamento, e seus nomes serão os nomes das propriedades calculadas se o método makeContainer() for chamado sem o parâmetro- key :


 extension PersistentStorage { var foobar: KeyValueContainer<Int> { makeContainer() } } 

No exemplo, as instâncias de armazenamento PersistentStorage receberão a propriedade foobar do contêiner com a chave foobar com o mesmo nome, cujo tipo de valor será um número inteiro. Tentar adicionar um segundo contêiner foobar para armazenamento resultará em um erro de compilação, o que nos garante a exclusividade das chaves.



Resumir


Os contêineres de valor resolvem todos os problemas da interface de armazenamento mencionados e não se limitam aos UserDefaults . É suficiente assinar (agrupar) qualquer armazenamento de valor-chave no protocolo KeyValueStorage com a implementação correspondente e já pode ser usado para criar contêineres seguros.


Separadamente, vale a pena notar a conveniência de adicionar e usar esses recipientes; tudo se limita a projetos simples e compreensíveis:


 extension PersistentStorage { //   var foobar: KeyValueContainer<Int> { makeContainer() } } //   let foobar = storageInstance.foobar.value //   storageInstance.foobar.value = 123 

Você só precisa se acostumar com o fato de que o nome da propriedade calculada é a chave do valor, o que é especialmente importante ao refatorar o código. Além disso, não se esqueça da migração do repositório, se você ainda não conseguiu renomear.


Só isso. Ficarei feliz em comentar nos comentários. Tchau!

Source: https://habr.com/ru/post/pt481456/


All Articles