Swift: conteneurs pour stocker les valeurs clés


Imaginez que vous devez enregistrer l'ID utilisateur dans UserDefaults . Quelle sera la première étape?


Habituellement, une entreprise commence par ajouter une constante pour une clé et vérifier son caractère unique. Cela est vrai pour la plupart des autres magasins de valeurs-clés. Et les conséquences de la conception primitive de tels référentiels ne se limitent pas aux clés; une interface sous la forme d'un ensemble de méthodes non systématique entraîne un certain nombre de problèmes possibles:


  • Erreurs d'écriture des clés : différentes clés peuvent être utilisées pour lire et écrire la même entité.
  • Type de valeur non fixé : par exemple, avec la même clé, vous pouvez écrire un nombre et lire une chaîne.
  • Collision de clés : différentes entités avec la même clé peuvent être enregistrées dans différentes parties du projet.

Dans un monde idéal, le compilateur devrait se protéger contre ces problèmes, ne vous permettant pas de créer un projet en cas de conflit de clé ou si le type de valeur ne correspond pas. Pour implémenter un tel stockage sécurisé, vous pouvez utiliser des conteneurs pour les valeurs, et cette procédure pas à pas vous aidera à les préparer en utilisant UserDefaults comme exemple.


Protocole de stockage


Donc, la première chose dont nous avons besoin est un protocole pour le référentiel lui-même, ce qui aidera à faire abstraction de son type. Par conséquent, nous aurons une interface unique pour travailler à la fois avec UserDefaults , avec un trousseau de clés et avec un autre stockage de valeur-clé. Ce protocole semble assez simple:


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

Ainsi, tout stockage conforme au protocole KeyValueStorage doit implémenter deux méthodes génériques : getter et setter de valeurs de clé sous la forme d'une chaîne. Dans le même temps, les valeurs elles-mêmes correspondent au protocole Codable , qui permet de stocker des instances de types qui ont une représentation universelle (par exemple, JSON ou PropertyList ).


Les implémentations standard de l'entrepôt de données ne prennent pas en charge le type Codable pour les valeurs, par exemple, les mêmes UserDefaults . Par conséquent, cette distinction de type est un bonus supplémentaire qui vous permet de stocker à la fois des primitives Swift (nombres, chaînes, etc.) et des structures de données entières, tout en conservant une interface de stockage simple.


Mise en œuvre du protocole


Il existe deux façons d'implémenter le protocole KeyValueStorage :


  • Signez le stockage existant sous le protocole et ajoutez les méthodes nécessaires:

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

  • Enveloppez le stockage dans un type distinct, en cachant ses champs pour une utilisation externe:

 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) { //   } } 

La deuxième méthode semble plus compliquée, mais évolue mieux, par exemple, vous pouvez définir le préfixe de clé pour chaque instance de stockage. De plus, l'encapsuleur masque les champs supplémentaires et ne laisse que les champs de protocole disponibles, résolvant ainsi les problèmes potentiels de conflits de signature.


Pour implémenter les méthodes value(forKey:) et setValue(:forKey:) il est important de prévoir la compatibilité des données. Cela est nécessaire pour que les valeurs stockées par les outils standard UserDefaults puissent être récupérées par des méthodes de KeyValueStorage , et vice versa.


Un exemple complet de la classe PersistentStorage prête à l'emploi est disponible ici .


Conteneur de valeur


Maintenant que nous avons extrait du type de stockage, nous allons ajouter un conteneur pour la valeur par clé. Il sera utile pour encapsuler tous les champs nécessaires dans une entité pratique, qui peut être transférée et utilisée séparément du stockage lui-même. Un tel conteneur est implémenté comme une petite classe générique :


 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 } } 

Le type de valeur pour le conteneur est limité par le protocole Codable de la même manière que dans les méthodes du magasin lui-même, de sorte que la propriété value calculée calcule simplement les appels avec une clé fixe et un type de valeur.


Si vous le souhaitez, vous pouvez étendre les fonctionnalités du conteneur, par exemple, ajouter une valeur par défaut qui sera utilisée s'il n'y a pas de données pour la clé dans le magasin.


Exemple d'implémentation d'un conteneur avec une valeur par défaut
 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 } } 

Le conteneur résout nos deux premiers problèmes - il corrige la clé et le type des données lues et écrites. Ainsi, toute tentative d'écriture d'une valeur d'un type incorrect sera arrêtée par le compilateur au stade de la construction:


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

Il reste à résoudre le dernier problème - garantir l'unicité des clés de stockage. Cette préoccupation peut également être "bloquée" sur le compilateur en utilisant une astuce plutôt intéressante.


Unicité clé


Pour résoudre le problème de conflit, nous utilisons le nom de la propriété calculée dans laquelle le conteneur lui-même sera créé comme clé. Pour ce faire, ajoutez une extension simple à notre protocole KeyValueStorage :


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

Ainsi, dans toutes les implémentations du protocole de stockage, une méthode générique sera ajoutée qui renvoie un conteneur avec la clé spécifiée. Un intérêt particulier dans cette méthode est le paramètre key , qui par défaut a une valeur égale à l'expression spéciale #function ( documentation ). Cela signifie qu'au stade de la construction, le littéral #function remplacé par le nom de la déclaration à partir de laquelle la makeContainer(key:) été appelée.


Cette construction vous permet de déclarer des conteneurs dans des extensions de stockage, et leurs noms seront les noms des propriétés calculées si la méthode makeContainer() est appelée sans le paramètre key en eux:


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

Dans l'exemple, les instances de stockage PersistentStorage recevront la propriété foobar du conteneur avec la clé foobar du même nom, dont le type de valeur sera un entier. Tenter d'ajouter un deuxième conteneur foobar pour le stockage entraînera une erreur de compilation, ce qui nous garantit l'unicité des clés.



Pour résumer


Les conteneurs de valeurs résolvent tous les problèmes d'interface de stockage mentionnés et ne sont pas limités aux UserDefaults . Il suffit de signer (envelopper) tout stockage de valeur de clé sous le protocole KeyValueStorage avec l'implémentation correspondante, et il peut déjà être utilisé pour créer des conteneurs sécurisés.


Séparément, il convient de noter la commodité d'ajouter et d'utiliser de tels conteneurs, tout est limité à des conceptions simples et compréhensibles:


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

Il vous suffit de vous habituer au fait que le nom de la propriété calculée est la clé de la valeur, ce qui est particulièrement important lors de la refactorisation du code. N'oubliez pas non plus la migration du référentiel, si vous ne pouviez toujours pas vous passer de renommer.


C’est tout. Je serai heureux de faire part de vos commentaires dans les commentaires. Salut!

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


All Articles