
Bayangkan Anda perlu menyimpan ID pengguna di UserDefaults . Apa yang akan menjadi langkah pertama?
Biasanya, bisnis dimulai dengan menambahkan konstanta untuk kunci dan memeriksa keunikannya. Ini berlaku untuk sebagian besar toko nilai kunci lainnya. Dan konsekuensi dari desain primitif dari repositori tersebut tidak terbatas pada kunci, antarmuka dalam bentuk serangkaian metode yang tidak sistematis mengarah ke sejumlah masalah yang mungkin terjadi:
- Kesalahan dalam menulis kunci : kunci yang berbeda dapat digunakan untuk membaca dan menulis entitas yang sama.
- Jenis nilai yang tidak diperbaiki : misalnya, dengan tombol yang sama Anda dapat menulis angka dan membaca string.
- Tabrakan kunci : entitas yang berbeda dengan kunci yang sama dapat direkam di berbagai bagian proyek.
Dalam dunia yang ideal, kompiler harus melindungi dari masalah-masalah ini, tidak memungkinkan Anda untuk membangun sebuah proyek jika ada konflik utama atau jenis nilai tidak cocok. Untuk menerapkan penyimpanan yang aman tersebut, Anda dapat menggunakan wadah untuk nilai, dan panduan ini akan membantu Anda mempersiapkannya menggunakan UserDefaults
sebagai contoh.
Protokol penyimpanan
Jadi, hal pertama yang kita butuhkan adalah protokol untuk repositori itu sendiri, yang akan membantu mengabstraksi dari tipenya. Oleh karena itu, kita akan memiliki antarmuka tunggal untuk bekerja dengan UserDefaults
, dan dengan gantungan kunci, dan dengan beberapa penyimpanan nilai kunci lainnya. Protokol ini terlihat sangat sederhana:
protocol KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? func setValue<T: Codable>(_ value: T?, forKey key: String) }
Jadi setiap penyimpanan yang sesuai dengan protokol KeyValueStorage
harus menerapkan dua metode umum : pengambil dan penyetel nilai kunci dalam bentuk string. Dalam kasus ini, nilai-nilai itu sendiri sesuai dengan protokol Codable
, yang memungkinkan penyimpanan instance dari tipe yang memiliki representasi universal (misalnya, JSON atau PropertyList ).
Implementasi data warehouse standar tidak mendukung tipe Codable
untuk nilai, misalnya, UserDefaults
sama. Oleh karena itu, perbedaan jenis ini adalah bonus sampingan yang memungkinkan Anda untuk menyimpan primitif Swift (angka, string, dll.) Dan seluruh struktur data, sekaligus menjaga antarmuka penyimpanan tetap sederhana.
Implementasi protokol
Ada dua cara untuk mengimplementasikan protokol KeyValueStorage
:
- Tanda tangani penyimpanan yang ada di bawah protokol dan tambahkan metode yang diperlukan:
extension UserDefaults: KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? {
- Bungkus penyimpanan dalam jenis yang terpisah, sembunyikan bidangnya untuk penggunaan eksternal:
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? {
Metode kedua terlihat lebih rumit, tetapi skala lebih baik, misalnya, Anda dapat mengatur awalan kunci untuk setiap instance penyimpanan. Selain itu, pembungkus menyembunyikan bidang tambahan dan hanya menyisakan bidang protokol yang tersedia, sehingga memecahkan masalah potensial dari konflik tanda tangan.
Untuk mengimplementasikan nilai metode value(forKey:)
dan setValue(:forKey:)
penting untuk menyediakan kompatibilitas data. Ini diperlukan agar nilai yang disimpan oleh alat UserDefaults
standar dapat diambil dengan metode dari KeyValueStorage
, dan sebaliknya.
Contoh lengkap dari kelas PersistentStorage
siap digunakan tersedia di sini .
Wadah nilai
Sekarang kita telah mengambil abstrak dari jenis penyimpanan, kita akan menambahkan sebuah wadah untuk nilai berdasarkan kunci. Ini akan berguna untuk merangkum semua bidang yang diperlukan ke dalam satu entitas yang nyaman, yang dapat ditransfer dan digunakan secara terpisah dari penyimpanan itu sendiri. Wadah seperti itu diimplementasikan sebagai kelas generik kecil:
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 } }
Jenis nilai untuk wadah dibatasi oleh protokol Codable
dengan cara yang sama seperti dalam metode toko itu sendiri, sehingga nilai properti terhitung hanya proksi panggilan dengan kunci tetap dan jenis nilai untuk itu.
Jika diinginkan, Anda dapat memperluas fungsionalitas wadah, misalnya, menambahkan nilai default yang akan digunakan jika tidak ada data untuk kunci di toko.
Contoh implementasi wadah dengan nilai default 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 } }
Wadah ini memecahkan dua masalah pertama kami - itu memperbaiki kunci dan ketik untuk data yang sedang dibaca dan ditulis. Jadi setiap upaya untuk menulis nilai dari tipe yang salah akan dihentikan oleh kompiler pada tahap build:
func doSomething(with container: KeyValueContainer<Int>) { container.value = "Text"
Tetap untuk menyelesaikan masalah terakhir - untuk menjamin keunikan kunci penyimpanan. Kekhawatiran ini juga bisa "digantung" pada kompiler menggunakan trik yang agak menarik.
Keunikan kunci
Untuk mengatasi masalah konflik, kami menggunakan nama properti yang dihitung di mana wadah itu sendiri akan dibuat sebagai kunci. Untuk melakukan ini, tambahkan ekstensi sederhana ke protokol KeyValueStorage
kami:
extension KeyValueStorage { func makeContainer<T: Codable>(key: String = #function) -> KeyValueContainer<T> { KeyValueContainer(storage: self, key: key) } }
Jadi dalam semua implementasi protokol penyimpanan, metode generik akan ditambahkan yang mengembalikan sebuah wadah dengan kunci yang ditentukan. Yang menarik dalam metode ini adalah parameter key
, yang secara default memiliki nilai yang sama dengan fungsi ekspresi # khusus ( dokumentasi ). Ini berarti bahwa pada tahap build, fungsi #function
literal diganti dengan nama deklarasi dari mana metode makeContainer(key:)
dipanggil.
Konstruksi ini memungkinkan Anda untuk mendeklarasikan kontainer dalam ekstensi penyimpanan, dan namanya akan menjadi nama properti yang dihitung jika metode makeContainer()
dipanggil tanpa parameter key
di dalamnya:
extension PersistentStorage { var foobar: KeyValueContainer<Int> { makeContainer() } }
Dalam contoh, instance penyimpanan PersistentStorage
akan menerima properti foobar
untuk wadah dengan kunci foobar
dengan nama yang sama, yang tipe nilainya akan berupa bilangan bulat. Mencoba menambahkan wadah foobar
kedua untuk penyimpanan akan menghasilkan kesalahan kompilasi, yang menjamin kami keunikan kunci.

Untuk meringkas
Wadah nilai mengatasi semua masalah antarmuka penyimpanan yang disebutkan dan tidak terbatas pada UserDefaults
. Cukup untuk menandatangani (membungkus) penyimpanan nilai kunci apa pun di bawah protokol KeyValueStorage
dengan implementasi yang sesuai, dan itu sudah dapat digunakan untuk membuat wadah yang aman.
Secara terpisah, perlu diperhatikan kenyamanan menambahkan dan menggunakan wadah seperti itu, semuanya terbatas pada desain yang sederhana dan dapat dimengerti:
extension PersistentStorage {
Anda hanya perlu membiasakan diri dengan fakta bahwa nama properti yang dihitung adalah kunci untuk nilai, yang sangat penting ketika refactoring kode. Juga, jangan lupa tentang migrasi repositori, jika Anda masih tidak bisa melakukannya tanpa mengganti nama.
Itu saja. Saya akan dengan senang hati memberi tanggapan dalam komentar. Sampai jumpa!