委託模式被證實是一種很好的替代繼承的方式,Kotlin 在語言層面對委託模式提供了很是優雅的支持(語法糖)。html
先給你們看看我用 Kotlin 的屬性委託語法糖在 Android 工程裏面作的一件有用工做——SharedPreferences
的讀寫委託。java
文中陳列的全部代碼已彙總成 Demo 傳至 github,點這兒獲取源碼。建議配合 Demo 閱讀本文。git
項目主要文件結構以下:github
│ App.kt
│
├─base
│ SpBase.kt
│
├─delegates
│ SPDelegates.kt
│ SPUtils.kt
│
├─demo
└─ui
MainActivity.kt
複製代碼
先來看看 delegates 包下的文件。編程
SPUtils
是個讀寫 SharedPreferences
(如下簡稱 SP) 存儲項的基礎工具類:設計模式
/** * @author xiaofei_dev * @desc 讀寫 SP 存儲項的基礎工具類 */
object SPUtils {
val SP by lazy {
App.instance.getSharedPreferences("default", Context.MODE_PRIVATE)
}
//讀 SP 存儲項
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 存儲項
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()
}
}
複製代碼
主要使用泛型實現了完善的 SP 讀寫,總體仍是很是簡潔易懂的。上下文對象使用了自定義的 Application
類實例(見 Demo 中的 App 類)。安全
下面重點來看一下 SPDelegates
類的定義:app
/** * @author xiaofei_dev * @desc <p>讀寫 SP 存儲項的輕量級委託類,以下, * 讀 SP 的操做委託給該類對象的 getValue 方法, * 寫 SP 操做委託給該類對象的 setValue 方法, * 注意這兩個方法不用你顯式調用,把一切交給編譯器就行(仍是語法糖) * 具體使用此類定義 SP 存儲項的代碼請參考 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
類實現了 Kotlin 標準庫中聲明的用於屬性委託的 ReadWriteProperty
接口(SPDelegates
類的用法後面會詳細說到),從名字能夠看出此接口是可讀寫的(適用於 var
聲明的屬性),除此以外還有個 ReadOnlyProperty
(只讀)接口(適用於 val
聲明的屬性)。ide
對於屬性的委託類(以SPDelegates
爲例),要求必須提供一個 getValue()
函數(和一個setValue()
函數——對於 var 屬性)。其getValue
方法的參數要求以下:函數
thisRef
—— 必須與 屬性所屬類 的類型(對於擴展屬性——指被擴展的類型)相同或是它的超類型(參見後面 SpBase 單例類中的註釋);property
—— 必須是類型 KProperty<*>
(Kotlin 標準庫 kotlin.reflect (反射)包下的一個類)或其超類型。對於其 setValue
方法,前兩個參數同 getValue
。第三個參數value
必須與屬性同類型或是它的子類型。
以上概念暫時看不懂沒關係,下面經過委託屬性的具體應用來加深理解。
接着是具體使用到委託屬性的 SpBase
單例類:
/** * @author xiaofei_dev * @desc 定義的 SP 存儲項 */
object SpBase{
//SP 存儲項的鍵
private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
// 這就定義了一個 SP 存儲項
// 把 SP 的讀寫操做委託給 SPDelegates 類的一個實例(使用 by 關鍵字,by 是 Kotlin 語言層面的一個原語),
// 此時訪問 SpBase 的 contentSomething (你能夠簡單把其當作 Java 裏的一個靜態變量)屬性便是在讀取 SP 的存儲項,
// 給 contentSomething 屬性賦值便是寫 SP 的操做,就這麼簡單
// 這裏用到的 SPDelegates 對象的 getValue 方法的 thisRef(見上文) 參數的類型正是外層的 SpBase
var contentSomething: String by SPDelegates(CONTENT_SOMETHING, "我是一個 SP 存儲項,點擊編輯我")
}
複製代碼
上面代碼中,單例 SpBase
的屬性 contentSomething
就是一個定義好的 SP 存儲項。得益於語言級別的強大語法糖支持,寫出來的代碼能夠如此簡潔而優雅。讀寫 SP 存儲項的請求經過屬性委託給了一個 SPDelegates
對象,委託屬性的語法爲
val/var <屬性名>: <類型> by <表達式>
其最後會被編譯器解釋成下面這樣的代碼(大體上):
object SpBase{
private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
private val propDelegate = SPDelegates(CONTENT_SOMETHING, "我是一個 SP 存儲項,點擊編輯我")
var contentSomething: String
get() = propDelegate.getValue(this, this::contentSomething)//讀SP
set(value) = propDelegate.setValue(this, this::contentSomething, value)//寫SP
}
複製代碼
仍是比較容易理解的。下面演示下這個定義好的 SP 存儲項如何使用,見 Demo 的 MainActivity 類文件:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
}
private fun initView(){
//讀取 SP 內容顯示到界面上
editContent.setText(SpBase.contentSomething)
btnSave.setOnClickListener {
//保存 SP 項
SpBase.contentSomething = "${editContent.text}"
Toast.makeText(this, R.string.main_save_success, Toast.LENGTH_SHORT).show()
}
}
}
複製代碼
總體比較簡單,就是個讀寫 SP 存儲項的過程。你們能夠實際運行下 Demo 看下具體效果。
上文述及的 SPDelegates
類實現了 Kotlin 標準庫提供的 ReadWriteProperty
接口,咱們固然也能夠不借助任何接口來實現一個屬性委託類,只要其提供一個getValue()
函數(和一個setValue()
函數——對於 var 屬性)而且符合咱們上面討論的參數要求就行。下面來定義一個平凡的屬性委託類 Delegate
(見 Demo 的 demo 包下 Example 文件):
/** * @author xiaofei_dev * @desc 不用實現任何接口的平凡屬性委託類 */
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
複製代碼
你能夠本身跑下試試~
有必要單獨花篇幅解釋下何爲委託模式。
簡而言之,在委託模式中,有兩個對象共同處理同一個請求,接受請求的對象將請求委託給另外一個對象來處理。
委託模式最簡單的例子:
//委託類,墨水能用來打印文字( ̄▽ ̄)"
class Ink {
fun print() {
print("This message comes from the delegate class,Not Printer.")
}
}
class Printer {
//委託對象
var ink = Ink()
fun print() {
//Printer 的實例會將請求委託給另外一個對象(DelegateNormal 的對象)來處理
ink.print()//調用委託對象的方法
}
}
fun main(args: Array<String>) {
val printer = Printer()
printer.print()
}
複製代碼
控制檯輸出以下:
This message comes from the delegate class,Not Printer.
複製代碼
委託模式使咱們能夠用聚合來代替繼承,是許多其餘設計模式(如狀態模式、策略模式、訪問者模式)的基礎。
Kotlin 能夠作到零樣板代碼實現委託模式(而不是像上面展現的那樣還須要樣板代碼)!
好比咱們如今有以下接口和類:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
複製代碼
Base
接口想作的就是在控制檯打印些什麼東西。這沒啥問題,咱們已經在 BaseImpl
類上完整實現了 Base
接口。
此時咱們想再給 Base 接口寫一個實現時能夠這麼作:
class Derived(b: Base) : Base by b
複製代碼
這其實跟下面的寫法是等價的(編譯器實際生成的):
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 的標準庫爲一些經常使用的委託寫好了工廠方法,下面一一列舉。
fun main(args: Array<String>) {
//延遲計算屬性的值,lazy 後面 lambda 表達式中的邏輯只會執行一次(且是線程安全的)並記錄結果,後續調用屬性的 get() 方法只是返回記錄的結果
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
println(lazyValue)
println(lazyValue)
}
複製代碼
控制檯輸出以下:
computed!
Hello
Hello
複製代碼
Delegates.observable()
接受兩個參數:初始值與修改時處理程序。 每次給屬性賦值時就會調用該處理程序(在賦值後執行)。處理程序有三個參數:被賦值屬性的 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
)中存儲屬性的值。 這種狀況下,你能夠直接將屬性委託給映射實例:
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)
}
複製代碼
固然這種應用必須確保屬性的名字和 map
中的鍵對應起來,否則你可能會收穫一個 NoSuchElementException
運行時異常,大概像這樣:
java.util.NoSuchElementException: Key XXXX is missing in the map.
複製代碼
言止於此,未完待續。