Kotlin委託屬性-簡化數據訪問

一、概述

Kotlin有很多語法糖,最近看了委託屬性, 用於改造筆者的開源組件LightKV, 確實提高了不少易用性。
關於LightKV,筆者在上一篇文章《LightKV-高性能key-value存儲組件》中有介紹其原理,有興趣的讀者可以瞭解一下。

LightKV的用法和SharePreferences類似,都是key-value結構,通過指定key讀寫value。
key-value 的 API 適用於存儲統計,緩存,配置......等各種信息,
隨着APP的迭代,必然會有越來越多的信息需要存儲,對應用開發而言,key-value的存儲不可或缺。

筆者上一篇文章中,有熱心網友提到:“想法很好,不過感覺用處不大,如果要存的數據很少那就sp …… ”
誠然,SDK已經提供了SharePreferences了,而且當用SharePreferences還沒遇到性能瓶頸時,也就沒有嘗試別的組件的的動力了。

而且,之前的那一版,只做到了“高效”,沒有做到“易用”。


二、舊版用法

public class AppData {
    private static final SyncKV DATA =
            new LightKV.Builder(GlobalConfig.getAppContext(), "app_data")
                    .logger(AppLogger.getInstance())
                    .executor(AsyncTask.THREAD_POOL_EXECUTOR)
                    .keys(Keys.class)
                    .encoder(new ConfuseEncoder())
                    .sync();

    // keys define
    public interface Keys {
        int SHOW_COUNT = 1 | DataType.INT;
        int ACCOUNT = 2 | DataType.STRING ;
        int TOKEN = 3 | DataType.STRING;
        int SECRET = 4 | DataType.ARRAY | DataType.ENCODE;
    }

    public static SyncKV data() {
        return DATA;
    }

    public static String getString(int key) {
        return DATA.getString(key);
    }

    public static void putString(int key, String value) {
        DATA.putString(key, value);
        DATA.commit();
    }

    public static byte[] getArray(int key) {
        return DATA.getArray(key);
    }

    public static void putArray(int key, byte[] value) {
        DATA.putArray(key, value);
        DATA.commit();
    }

    // ......
}
val account = AppData.getString(AppData.Keys.ACCOUNT)
if(TextUtils.isEmpty(account)){
      AppData.putString(AppData.Keys.ACCOUNT, "[email protected]")
}

該用法的複雜度在於:
如果想用靜態方法(調用時簡單一些),則每一個數據存儲類都需要實現一份各種類型的get和set;
如果直接返回data()來讀寫, 寫起來會比較長:

val account = AppData2.data().getString(Keys.ACCOUNT)
if(TextUtils.isEmpty(account)){
     AppData2.data().putString(Keys.ACCOUNT, "[email protected]")
}

直到後來瞭解了Kotlin委託, 彷彿看到了曙光……


三、新版用法

object AppData : KVData() {
    override fun createInstance(): LightKV {
        return LightKV.Builder(GlobalConfig.appContext, "app_data")
                .logger(AppLogger)
                .executor(AsyncTask.THREAD_POOL_EXECUTOR)
                .encoder(GzipEncoder)
                .sync()
    }

    var showCount by int(1)
    var account by string(2)
    var token by string(3)
    var secret by array(4 or DataType.ENCODE)
}
val account = AppData.account
if (TextUtils.isEmpty(account)) {
   AppData.account = "[email protected]"
}

使用Kotlin委託,省了各種put和set的方法調用,看起來像是在直接訪問AppData的屬性。


四、屬性委託的實現

4.1 聲明屬性

語法: val/var <屬性名>: <類型> by <表達式>。

class Example {
    var p: String by Delegate()
}

by 後面的表達式是對應的委託, 屬性的 get() 和 set() 會被委託給它的 getValue() 和 setValue() 方法。
當然,如果聲明的是val, 則不會委託set()方法。

4.2 實現委託

屬性的委託,需要提供一個 getValue() 函數和 setValue() 函數(如果聲明的是var 的話),並以operator修飾。

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

例子中,thisRef 是 Example 的引用, 參數 property 保存了對屬性p的描述,例如可以通過property.name獲取p的名字.

4.3 訪問屬性

訪問 p 時,將調用 Delegate 中的 getValue() 函數;
給 p 賦值時,將調用 setValue() 函數。

val e = Example()
println(e.p)
e.p = "NEW"

輸出結果:

Example@33a17727, thank you for delegating ‘p’ to me!
NEW has been assigned to ‘p’ in Example@33a17727.

4.4 屬性委託的原理

class C {
    var prop: Type by MyDelegate()
}
// 由編譯器生成的相應代碼:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

前後對比,不難看出,其實屬性委託的本質是“代理模式”的語法封裝。

五、優化LightKV

5.1 定義抽象類

abstract class KVData{
    internal var autoCommit = true

    abstract fun createInstance() : LightKV

    val data: LightKV by lazy {
        createInstance()
    }

    protected fun boolean(key: Int) = KVProperty<Boolean>(key or DataType.BOOLEAN)
    protected fun int(key: Int) = KVProperty<Int>(key or DataType.INT)
    protected fun float(key: Int) = KVProperty<Float>(key or DataType.FLOAT)
    protected fun double(key: Int) = KVProperty<Double>(key or DataType.DOUBLE)
    protected fun long(key: Int) = KVProperty<Long>(key or DataType.LONG)
    protected fun string(key: Int) = KVProperty<String>(key or DataType.STRING)
    protected fun array(key: Int) = KVProperty<ByteArray>(key or DataType.ARRAY)

    fun disableAutoCommit(){
        autoCommit = false
    }

    fun enableAutoCommit(){
        autoCommit = true
        data.commit()
    }
}

該抽象類聲明瞭LightKV, 添加了自動提交開關,以及定了個各種類型委託。

5.2 實現委託

爲方便編寫委託, Kotlin標準庫定義了的ReadWriteProperty接口:

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

使用時實現接口的方法即可。
爲了統一定義各個類型委託,我們在構造函數傳入key, 由key決定對應的類型操作。
通過thisRef.data(LightKV)和 key, 分別在getValue和setValue方法中實現取值和賦值。

class KVProperty<T>(private val key: Int) : ReadWriteProperty<KVData, T> {
    @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
    override operator fun getValue(thisRef: KVData, property: KProperty<*>): T
    = with(thisRef.data) {
        return when (key and DataType.MASK) {
            DataType.BOOLEAN -> getBoolean(key)
            DataType.INT -> getInt(key)
            DataType.FLOAT -> getFloat(key)
            DataType.LONG -> getLong(key)
            DataType.DOUBLE -> getDouble(key)
            DataType.STRING -> getString(key)
            DataType.ARRAY -> getArray(key)
            else -> throw IllegalArgumentException("Invalid Key: $key")
        } as T
    }

    override operator fun setValue(thisRef: KVData, property: KProperty<*>, value: T)  
    = with(thisRef.data) {
        when (key and DataType.MASK) {
            DataType.BOOLEAN -> putBoolean(key, value as Boolean)
            DataType.INT -> putInt(key, value as Int)
            DataType.FLOAT -> putFloat(key, value as Float)
            DataType.LONG -> putLong(key, value as Long)
            DataType.DOUBLE -> putDouble(key, value as Double)
            DataType.STRING -> putString(key, value as String)
            DataType.ARRAY -> putArray(key, value as ByteArray)
            else -> throw IllegalArgumentException("Invalid Key: $key")
        }
        if(mMode == LightKV.SYNC_MODE && thisRef.autoCommit){
            commit()
        }
    }
}

在LightKV爲SYNC_MODE時自動commit()。
當然,如果需要批量提交。可以調用disableAutoCommit()禁用自動提交。

最後,在使用時,繼承KVData,聲明屬性,即可像訪問變量一樣讀寫LightKV的數據(參見第三節)。


六、下載

repositories {
    jcenter()
}

dependencies {
    implementation 'com.horizon.lightkv:lightkv:1.0.4'
}

項目地址:
https://github.com/No89757/LightKV

七、結語

以前筆者對語法糖是不感興趣的,覺得語法糖掩蓋了細節,容易使人“只知其然而不知其所以然”;
但是後來漸漸地也開始接受了,技術的發展日新月異,不可能什麼都從底層開始構築。
業界流傳有“人生苦短,我用python”,說的就是高級語言所帶來的便利,可以節約不少時間。
當然,C語言,彙編語言,還是需要有人去寫,要看問題領域。
對APP開發而言,誠然有大量的“搬磚”工作,磚頭搬累了,來一發語法糖,也是不錯的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章