委托模式被證明是一種很好的替代繼承的方式,Kotlin 在語言層面對委托模式提供了非常優(yōu)雅的支持(語法糖)。
先給大家看看我用 Kotlin 的屬性委托語法糖在 Android 工程里面做的一件有用工作——SharedPreferences 的讀寫委托。
文中陳列的所有代碼已匯總成 Demo 傳至 github,點(diǎn)這兒獲取源碼。建議配合 Demo 閱讀本文。
項(xiàng)目主要文件結(jié)構(gòu)如下:
│ App.kt
│
├─base
│ SpBase.kt
│
├─delegates
│ SPDelegates.kt
│ SPUtils.kt
│
├─demo
└─ui
MainActivity.kt
先來看看 delegates 包下的文件。
SPUtils 是個讀寫 SharedPreferences(以下簡稱 SP) 存儲項(xiàng)的基礎(chǔ)工具類:
/**
* @author xiaofei_dev
* @desc 讀寫 SP 存儲項(xiàng)的基礎(chǔ)工具類
*/
object SPUtils {
val SP by lazy {
App.instance.getSharedPreferences("default", Context.MODE_PRIVATE)
}
//讀 SP 存儲項(xiàng)
fun <T> getValue(name: String, default: T): T = with(SP) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default) ?: ""
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw java.lang.IllegalArgumentException()
}
@Suppress("UNCHECKED_CAST")
res as T
}
//寫 SP 存儲項(xiàng)
fun <T> putValue(name: String, value: T) = with(SP.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
主要使用泛型實(shí)現(xiàn)了完善的 SP 讀寫,整體還是非常簡潔易懂的。上下文對象使用了自定義的 Application 類實(shí)例(見 Demo 中的 App 類)。
Kotlin 中的委托屬性
下面重點(diǎn)來看一下 SPDelegates 類的定義:
/**
* @author xiaofei_dev
* @desc <p>讀寫 SP 存儲項(xiàng)的輕量級委托類,如下,
* 讀 SP 的操作委托給該類對象的 getValue 方法,
* 寫 SP 操作委托給該類對象的 setValue 方法,
* 注意這兩個方法不用你顯式調(diào)用,把一切交給編譯器就行(還是語法糖)
* 具體使用此類定義 SP 存儲項(xiàng)的代碼請參考 SpBase 文件</p>
*/
class SPDelegates<T>(private val key: String, private val default: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return SPUtils.getValue(key, default)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
SPUtils.putValue(key, value)
}
}
SPDelegates 類實(shí)現(xiàn)了 Kotlin 標(biāo)準(zhǔn)庫中聲明的用于屬性委托的 ReadWriteProperty 接口(SPDelegates 類的用法后面會詳細(xì)說到),從名字可以看出此接口是可讀寫的(適用于 var 聲明的屬性),除此之外還有個 ReadOnlyProperty (只讀)接口(適用于 val 聲明的屬性)。
對于屬性的委托類(以SPDelegates為例),要求必須提供一個 getValue() 函數(shù)(和一個setValue()函數(shù)——對于 var 屬性)。其getValue 方法的參數(shù)要求如下:
-
thisRef—— 必須與 屬性所屬類 的類型(對于擴(kuò)展屬性——指被擴(kuò)展的類型)相同或是它的超類型(參見后面 SpBase 單例類中的注釋); -
property—— 必須是類型KProperty<*>(Kotlin 標(biāo)準(zhǔn)庫 kotlin.reflect (反射)包下的一個類)或其超類型。
對于其 setValue 方法,前兩個參數(shù)同 getValue。第三個參數(shù)value 必須與屬性同類型或是它的子類型。
以上概念暫時看不懂不要緊,下面通過委托屬性的具體應(yīng)用來加深理解。
接著是具體使用到委托屬性的 SpBase 單例類:
/**
* @author xiaofei_dev
* @desc 定義的 SP 存儲項(xiàng)
*/
object SpBase{
//SP 存儲項(xiàng)的鍵
private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
// 這就定義了一個 SP 存儲項(xiàng)
// 把 SP 的讀寫操作委托給 SPDelegates 類的一個實(shí)例(使用 by 關(guān)鍵字,by 是 Kotlin 語言層面的一個原語),
// 此時訪問 SpBase 的 contentSomething (你可以簡單把其看成 Java 里的一個靜態(tài)變量)屬性即是在讀取 SP 的存儲項(xiàng),
// 給 contentSomething 屬性賦值即是寫 SP 的操作,就這么簡單
// 這里用到的 SPDelegates 對象的 getValue 方法的 thisRef(見上文) 參數(shù)的類型正是外層的 SpBase
var contentSomething: String by SPDelegates(CONTENT_SOMETHING, "我是一個 SP 存儲項(xiàng),點(diǎn)擊編輯我")
}
上面代碼中,單例 SpBase 的屬性 contentSomething 就是一個定義好的 SP 存儲項(xiàng)。得益于語言級別的強(qiáng)大語法糖支持,寫出來的代碼可以如此簡潔而優(yōu)雅。讀寫 SP 存儲項(xiàng)的請求通過屬性委托給了一個 SPDelegates 對象,委托屬性的語法為
val/var <屬性名>: <類型> by <表達(dá)式>
其最后會被編譯器解釋成下面這樣的代碼(大致上):
object SpBase{
private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
private val propDelegate = SPDelegates(CONTENT_SOMETHING, "我是一個 SP 存儲項(xiàng),點(diǎn)擊編輯我")
var contentSomething: String
get() = propDelegate.getValue(this, this::contentSomething)//讀SP
set(value) = propDelegate.setValue(this, this::contentSomething, value)//寫SP
}
還是比較容易理解的。下面演示下這個定義好的 SP 存儲項(xiàng)如何使用,見 Demo 的 MainActivity 類文件:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
}
private fun initView(){
//讀取 SP 內(nèi)容顯示到界面上
editContent.setText(SpBase.contentSomething)
btnSave.setOnClickListener {
//保存 SP 項(xiàng)
SpBase.contentSomething = "${editContent.text}"
Toast.makeText(this, R.string.main_save_success, Toast.LENGTH_SHORT).show()
}
}
}
整體比較簡單,就是個讀寫 SP 存儲項(xiàng)的過程。大家可以實(shí)際運(yùn)行下 Demo 看下具體效果。
從零實(shí)現(xiàn)一個屬性的委托類
上文述及的 SPDelegates 類實(shí)現(xiàn)了 Kotlin 標(biāo)準(zhǔn)庫提供的 ReadWriteProperty 接口,我們當(dāng)然也可以不借助任何接口來實(shí)現(xiàn)一個屬性委托類,只要其提供一個getValue() 函數(shù)(和一個setValue()函數(shù)——對于 var 屬性)并且符合我們上面討論的參數(shù)要求就行。下面來定義一個平凡的屬性委托類 Delegate (見 Demo 的 demo 包下 Example 文件):
/**
* @author xiaofei_dev
* @desc 不用實(shí)現(xiàn)任何接口的平凡屬性委托類
*/
class Delegate<T> {
private var value: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
println("$thisRef, thank you for delegating '${property.name}' to me! The value is $value")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
this.value = value
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
使用方式依舊:
class Example {
//委托屬性
var p: String? by Delegate()
}
fun main(args: Array<String>) {
val e = Example()
e.p = "hehe"
println(e.p)
}
控制臺輸出如下:
hehe has been assigned to 'p' in com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb.
com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb, thank you for delegating 'p' to me! The value is hehe
hehe
你可以自己跑下試試~
關(guān)于委托模式
有必要單獨(dú)花篇幅解釋下何為委托模式。
簡而言之,在委托模式中,有兩個對象共同處理同一個請求,接受請求的對象將請求委托給另一個對象來處理。
委托模式最簡單的例子:
//委托類,墨水能用來打印文字( ̄▽ ̄)"
class Ink {
fun print() {
print("This message comes from the delegate class,Not Printer.")
}
}
class Printer {
//委托對象
var ink = Ink()
fun print() {
//Printer 的實(shí)例會將請求委托給另一個對象(DelegateNormal 的對象)來處理
ink.print()//調(diào)用委托對象的方法
}
}
fun main(args: Array<String>) {
val printer = Printer()
printer.print()
}
控制臺輸出如下:
This message comes from the delegate class,Not Printer.
委托模式使我們可以用聚合來代替繼承,是許多其他設(shè)計(jì)模式(如狀態(tài)模式、策略模式、訪問者模式)的基礎(chǔ)。
Kotlin 的委托模式
Kotlin 可以做到零樣板代碼實(shí)現(xiàn)委托模式(而不是像上面展示的那樣還需要樣板代碼)!
比如我們現(xiàn)在有如下接口和類:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
Base 接口想做的就是在控制臺打印些什么東西。這沒啥問題,我們已經(jīng)在 BaseImpl 類上完整實(shí)現(xiàn)了 Base 接口。
此時我們想再給 Base 接口寫一個實(shí)現(xiàn)時可以這么做:
class Derived(b: Base) : Base by b
這其實(shí)跟下面的寫法是等價的(編譯器實(shí)際生成的):
class Derived(val delegate: Base) : Base {
override fun print() {
delegate.print()
}
}
注意不是下面這種:
class Derived(val delegate: Base){
fun print() {
delegate.print()
}
}
Kotlin 通過編譯器的黑魔法將許多樣板代碼封印在了 by 這樣一個語言級別的原語中(又是語法糖)。使用方式:
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print()
}
控制臺輸出如下:
10
Kotlin 標(biāo)準(zhǔn)庫中其他屬性委托
說回屬性委托,Kotlin 的標(biāo)準(zhǔn)庫為一些常用的委托寫好了工廠方法,下面一一列舉。
延遲屬性 Lazy
fun main(args: Array<String>) {
//延遲計(jì)算屬性的值,lazy 后面 lambda 表達(dá)式中的邏輯只會執(zhí)行一次(且是線程安全的)并記錄結(jié)果,后續(xù)調(diào)用屬性的 get() 方法只是返回記錄的結(jié)果
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
println(lazyValue)
println(lazyValue)
}
控制臺輸出如下:
computed!
Hello
Hello
可觀察屬性 Observable
Delegates.observable()接受兩個參數(shù):初始值與修改時處理程序。 每次給屬性賦值時就會調(diào)用該處理程序(在賦值后執(zhí)行)。處理程序有三個參數(shù):被賦值屬性的 KProperty 對象、舊值與新值:
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
控制臺輸出如下:
<no name> -> first
first -> second
把屬性儲存在映射中
你甚至可以在一個映射(map)中存儲屬性的值。 這種情況下,你可以直接將屬性委托給映射實(shí)例:
class Student(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main(args: Array<String>) {
val student = Student(mapOf(
"name" to "xiaofei",
"age" to 25
))
println(student.name)
println(student.age)
}
當(dāng)然這種應(yīng)用必須確保屬性的名字和 map中的鍵對應(yīng)起來,不然你可能會收獲一個 NoSuchElementException 運(yùn)行時異常,大概像這樣:
java.util.NoSuchElementException: Key XXXX is missing in the map.
言止于此,未完待續(xù)。
參考文獻(xiàn)
- 《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》
- 《Kotlin for Android Developers》
- 《Kotlin 極簡教程》
- 《Kotlin 核心編程》
- 維基百科 委托模式 詞條
- Kotlin 官方文檔