1. 重點理解val的使用規則
引用1
如果說var代表了varible(變量),那么val可看成value(值)的縮寫。但也有人覺得這樣并不直觀或準確,而是把val解釋成varible+final,即通過val聲明的變量具有Java中的final關鍵字的效果,也就是引用不可變。
val聲明的變量是只讀變量,它的引用不可更改,但并不代表其引用對象也不可變。事實上,我們依然可以修改引用對象的可變成員。
引用2
優先使用val來避免副作用
在很多Kotlin的學習資料中,都會傳遞一個原則:優先使用val來聲明變量。這相當正確,但更好的理解可以是:盡可能采用val、不可變對象及純函數(其實就是沒有副作用的函數,具備引用透明性)來設計程序。
引用3
然而,在Kotlin編程中,我們推薦優先使用val來聲明一個本身不可變的變量,這在大部分情況下更具有優勢:
? 這是一種防御性的編碼思維模式,更加安全和可靠,因為變量的值永遠不會在其他地方被修改(一些框架采用反射技術的情況除外);
? 不可變的變量意味著更加容易推理,越是復雜的業務邏輯,它的優勢就越大。
點評
上面說的其實非常明確了,val聲明的變量具有Java中的final關鍵字的效果,也就是引用不可變,但其引用的內容是可變的。其實這里扯出了兩個概念對我來說更重要,一個是變量或函數的副作用,一個是防御性編程思維。
在后續編程中,會注意到變量副作用這塊,盡量避免。而防御性思維其實一直都有,對于外部輸入的參數,總是站在不可靠的角度上對待,從而寫出可靠的代碼。
2. 關于函數和Lambda
引用1
Kotlin天然支持了部分函數式特性。函數式語言一個典型的特征就在于函數是頭等公民——我們不僅可以像類一樣在頂層直接定義一個函數,也可以在一個函數內部定義一個局部函數。
引用2
所謂的高階函數,你可以把它理解成“以其他函數作為參數或返回值的函數”。高階函數是一種更加高級的抽象機制,它極大地增強了語言的表達能力。
引用3
Kotlin存在一種特殊的語法,通過兩個冒號來實現對于某個類的方法進行引用。
為什么使用雙冒號的語法?
如果你了解C#,會知道它也有類似的方法引用特性,只是語法上不同,是通過點號來實現的。然而,C#的這種方式存在二義性,容易讓人混淆方法引用表達式與成員表達式,所以Kotlin采用::(沿襲了Java 8的習慣),能夠讓我們更加清晰地認識這種語法。
引用4
Lambda表達式,你可以把它理解成簡化表達后的匿名函數,實質上它就是一種語法糖。
現在來總結下Lambda的語法:
? 一個Lambda表達式必須通過{}來包裹;
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
? 如果Lambda聲明了參數部分的類型,且返回值類型支持類型推導,那么Lambda變量就可以省略函數類型聲明;
val sum = { x: Int, y: Int -> x + y }
? 如果Lambda變量聲明了函數類型,那么Lambda的參數部分的類型就可以省略。
val sum: (Int, Int) -> Int = { x, y -> x + y }
引用5
區分函數、Lambda
? fun在沒有等號、只有花括號的情況下,是我們最常見的代碼塊函數體,如果返回非Unit值,必須帶return。
fun foo(x: Int) { print(x) } fun foo(x: Int, y: Int): Int { return x * y }
? fun帶有等號,是單表達式函數體。該情況下可以省略return。
fun foo(x: Int, y: Int) = x + y
? 不管是用val還是fun,如果是等號加花括號的語法,那么構建的就是一個Lambda表達式,Lambda的參數在花括號內部聲明。所以,如果左側是fun,那么就是Lambda表達式函數體,也必須通過()或invoke來調用Lambda。
val foo = { x: Int, y: Int -> x + y } // foo.invoke(1, 2)或foo(1, 2) fun foo(x: Int) = { y: Int -> x + y } // foo(1).invoke(2)或foo(1)(2)
點評
這部分內容對我個人而言,是區分函數和lambda。之前沒有仔細思考擴這個問題,一直是憑著感覺來。這次算是理清楚了:如果是等號加花括號的語法,那么構建的就是一個Lambda表達式。那么調用時就必須通過()或invoke來實現。
還有一點是對函數是頭等公民的理解,之前也聽過這句話,但是具體體現在哪不得而知。你見過在Java中,函數里面在定義一個函數嗎?你見過在Java類外部定義一個函數嗎?沒有吧,因為Java中對象是頭等公民。而Kt中你可以在函數中再定義函數,再類頂層定義函數,這就是區別。
3. 表達式
引用
.. 閉區間 , until 半開區間
for(i in 1 until 10){ print(i) // 123456789 } println() for(i in 1..10){ print(i) // 12345678910 } println() for(i in 0..0){ print(i) // output 0 } println() for(i in 0 until 0){ print(i) // nothing }
點評
關于until半開區間的特性,自己還在工作過程中犯過一個錯誤,需求其實很簡單,就是在一個范圍內獲取一個隨機數,但是當你寫出這句時 (0 until 0).random(),就有bug在等著你了。
上面實例代碼中其實做了實驗,打印 0 until 0 是沒有任何內容輸出的,再去向無任何輸出內容的表達式要一個隨機數,編譯器不報錯能干嘛呢?
print((0 until 0).random()) // Exception in thread "main" java.util.NoSuchElementException: Cannot get random in empty range: 0..-1
print((0..0).random()) // 0
4. init語句塊
引用1
Kotlin引入了一種叫作init語句塊的語法,它屬于構造方法的一部分,兩者在表現形式上卻是分離的。Bird類的構造方法在類的外部,它只能對參數進行賦值。如果我們需要在初始化時進行其他的額外操作,那么我們就可以使用init語句塊來執行。
當沒有val或var的時候,構造方法的參數可以在init語句塊被直接調用。其實它們還可以用于初始化類內部的屬性成員的情況。
class Bird(weight: Double = 0.00, age: Int = 0,color: String = "blue") { val weight: Double = weight //在初始化屬性成員時調用weight val age: Int = age val color: String = color }
除此之外,我們并不能在其他地方使用。
class Bird(weight: Double, age: Int, color: String) { fun printWeight() { print(weight) // Unresolved reference: weight } }
事實上,我們的構造方法還可以擁有多個init,它們會在對象被創建時按照類中從上到下的順序先后執行。多個init語句塊有利于我們進一步對初始化的操作進行職能分離,這在復雜的業務開發(如Android)中顯得特別有用。
引用2
我們在Bird類中可以用val或者var來聲明構造方法的參數。這一方面代表了參數的引用可變性,另一方面它也使得我們在構造類的語法上得到了簡化。
為什么這么說呢?事實上,構造方法的參數名前當然可以沒有val和var,然而帶上它們之后就等價于在Bird類內部聲明了一個同名的屬性,我們可以用this來進行調用。比如我們前面定義的Bird類就類似于以下的實現:
class Bird( weight: Double = 0.00, // 參數名前沒有val age: Int = 0, color: String = "blue") { val weight: Double val age: Int val color: String init { this.weight = weight // 構造方法參數可以在init語句塊被調用 this.age = age this.color = color } }
點評
這一部分的內容對我個人而言是知識盲區,這次算是補上了。用val或var修飾的構造方法參數,實際上等價于在類內部聲明了一個同名屬性,可以用this進行調用。
5. 關于延遲初始化
引用1
總結by lazy語法的特點如下
? 該變量必須是引用不可變的,而不能通過var來聲明。
? 在被首次調用時,才會進行賦值操作。一旦被賦值,后續它將不能被更改。
class Bird(val weight: Double, val age: Int, val color: String) { val sex: String by lazy { if (color == "yellow") "male" else "female" } }
另外系統會給lazy屬性默認加上同步鎖,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一時刻只允許一個線程對lazy屬性進行初始化,所以它是線程安全的。但若你能確認該屬性可以并行執行,沒有線程安全問題,那么可以給lazy傳遞LazyThreadSafetyMode.PUBLICATION參數。你還可以給lazy傳遞LazyThreadSafetyMode. NONE參數,這將不會有任何線程方面的開銷,當然也不會有任何線程安全的保證。
val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) { //并行模式 if (color == "yellow") "male" else "female" } val sex: String by lazy(LazyThreadSafetyMode.NONE) { //不做任何線程保證也不會有任何線程開銷 if (color == "yellow") "male" else "female" }
引用2
總結lateinit語法特點如下
? 主要用于var聲明的變量,然而它不能用于基本數據類型,如Int、Long等,我們需要用Integer這種包裝類作為替代。
class Bird(val weight: Double, val age: Int, val color: String) { lateinit var sex: String // sex可以延遲初始化 fun printSex() { this.sex = if (this.color == "yellow") "male" else "female" println(this.sex) } } fun main(args: Array<String>) { val bird = Bird(1000.0, 2, "bule") bird.printSex() } // 運行結果 female
引用3
你可能比較好奇,如何讓用var聲明的基本數據類型變量也具有延遲初始化的效果,一種可參考的解決方案是通過Delegates.notNull<T>,這是利用Kotlin中委托的語法來實現的。
var test by Delegates.notNull<Int>() fun doSomething() { test = 1 println("test value is ${test}") test = 2 }
點評
這塊主要是總結by lazy和lateinit的區別,根據兩者的區別選擇合適的延遲方案很重要。簡單粗暴一點就是by lazy對應val,lateint對應var。
6. 可見性修飾符
引用
可見性修飾符對比Kotlin中的可見性修飾符也與Java中的很類似。但也有不一樣的地方,主要有以下幾點:
1)Kotlin與Java的默認修飾符不同,Kotlin中是public,而Java中是default,它只允許包內訪問。
2)Kotlin中有一個獨特的修飾符internal。
3)Kotlin可以在一個文件內單獨聲明方法及常量,同樣支持可見性修飾符。
4)Java中除了內部類可以用private修飾以外,其他類都不允許private修飾,而Kotlin可以。
5)Kotlin和Java中的protected的訪問范圍不同,Java中是包、類及子類可訪問,而Kotlin只允許類及子類。
關于internal
Kotlin中有一個獨特的修飾符internal,和default有點像但也有所區別。internal在Kotlin中的作用域可以被稱作“模塊內訪問”。那么到底什么算是模塊呢?以下幾種情況可以算作一個模塊
? 一個Eclipse項目? 一個Intellij IDEA項目
? 一個Maven項目
? 一個Grandle項目
? 一組由一次Ant任務執行編譯的代碼
總的來說,一個模塊可以看作一起編譯的Kotlin文件組成的集合。那么,Kotlin中為什么要誕生這么一種新的修飾符呢?Java的包內訪問不好嗎?
Java的包內訪問中確實存在一些問題。舉個例子,假如你在Java項目中定義了一個類,使用了默認修飾符,那么現在這個類是包私有,其他地方將無法訪問它。然后,你把它打包成一個類庫,并提供給其他項目使用,這時候如果有個開發者想使用這個類,除了copy源代碼以外,還有一個方式就是在程序中創建一個與該類相同名字的包,那么這個包下面的其他類就可以直接使用我們前面的定義的類。這樣我們便可以直接訪問該類了。
而Kotlin默認并沒有采用這種包內可見的作用域,而是使用了模塊內可見,模塊內可見指的是該類只對一起編譯的其他Kotlin文件可見。開發工程與第三方類庫不屬于同一個模塊,這時如果還想使用該類的話只有復制源碼一種方式了。這便是Kotlin中internal修飾符的一個作用體現。
關于private
在Java程序中,我們很少見到用private修飾的類,因為Java中的類或方法沒有單獨屬于某個文件的概念。比如,我們創建了Rectangle.java這個文件,那么它里面的類要么是public,要么是包私有,而沒有只屬于這個文件的概念。若要用private修飾,那么這個只能是其他類的內部類。而Kotlin中則可以用private給單獨的類修飾,它的作用域就是當前這個Kotlin文件。
關于protected
Java中的protected修飾的內容作用域是包內、類及子類可訪問,而在Kotlin中,由于沒有包作用域的概念,所以protected修飾符在Kotlin中的作用域只有類及子類。
在了解了Kotlin中的可見修飾符后,我們來思考一個問題:前面已經講解了為什么要誕生internal這個修飾符,那么為什么Kotlin中默認的可見性修飾符是public,而不是internal呢?
關于這一點,Kotlin的開發人員在官方論壇進行了說明,這里我做一個總結:Kotlin通過分析以往的大眾開發的代碼,發現使用public修飾的內容比其他修飾符的內容多得多,所以Kotlin為了保持語言的簡潔性,考慮多數情況,最終決定將public當作默認修飾符。
點評
Kotlin與Java的可見性修飾符比較這一部分是我強烈推薦仔細閱讀的部分,可見性修飾符雖然簡單但卻非常重要,kotlin的internal、private、protected修飾符都有自身獨特的特點,跟你原本掌握的Java有很大的不同。
對比學習可能會讓我們對兩門語言的理解層次更深。
Kotlin中沒有包內可見這種作用域,轉而代之的是模塊內可見,這種方式對比Java中的包內可見在某種意義上可能會更加“安全”。
另一邊Java中某個類或方法沒有單獨屬于某個文件的概念,而Kotlin中則可以用private單獨修飾某個類,它的作用域就是當前這個kotlin文件,這種設計在我看來可能會讓你更加能精準控制某個類的訪問權限。
7. getter和setter
引用
1)用val聲明的屬性將只有getter方法,因為它不可修改;而用var修飾的屬性將同時擁有getter和setter方法。
2)用private修飾的屬性編譯器將會省略getter和setter方法,因為在類外部已經無法訪問它了,這兩個方法的存在也就沒有意義了。
8. 內部類vs嵌套類
引用
眾所周知,在Java中,我們通過在內部類的語法上增加一個static關鍵詞,把它變成一個嵌套類。然而,Kotlin則是相反的思路,默認是一個嵌套類,必須加上inner關鍵字才是一個內部類,也就是說可以把靜態的內部類看成嵌套類。
內部類和嵌套類有明顯的差別,具體體現在:內部類包含著對其外部類實例的引用,在內部類中我們可以使用外部類中的屬性;而嵌套類不包含對其外部類實例的引用,所以它無法調用其外部類的屬性。
open class Horse { //馬 fun runFast() { println("I can run fast") } } open class Donkey { //驢 fun doLongTimeThing() { println("I can do some thing long time") } } class Mule { //騾子 fun runFast() { HorseC().runFast() } fun doLongTimeThing() { DonkeyC().doLongTimeThing() } private inner class HorseC : Horse() private inner class DonkeyC : Donkey() }
9. 數據類的約定與使用
引用
如果你要在Kotlin聲明一個數據類,必須滿足以下幾點條件:
? 數據類必須擁有一個構造方法,該方法至少包含一個參數,一個沒有數據的數據類是沒有任何用處的;
? 與普通的類不同,數據類構造方法的參數強制使用var或者val進行聲明;
? data class之前不能用abstract、open、sealed或者inner進行修飾;
? 在Kotlin1.1版本前數據類只允許實現接口,之后的版本既可以實現接口也可以繼承類。
10. 何謂伴生
引用
顧名思義,“伴生”是相較于一個類而言的,意為伴隨某個類的對象,它屬于這個類所有,因此伴生對象跟Java中static修飾效果性質一樣,全局只有一個單例。它需要聲明在類的內部,在類被裝載時會被初始化。
11. 關于泛型
引用
關于協變
普通方式定義的泛型是不變的,簡單來說就是不管類型A和類型B是什么關系,Generic<A>與Generic<B>(其中Generic代表泛型類)都沒有任何關系。比如,在Java中String是Oject的子類型,但List<String>并不是List<Object>的子類型,在Kotlin中泛型的原理也是一樣的。但是,Kotlin的List為什么允許List<String>賦值給List<Any>呢?
public interface List<E> extends Collection<E> { ... } public interface List<out E> : Collection<E> { ... }
關鍵在于這兩個List并不是同一種類型。如果在定義的泛型類和泛型方法的泛型參數前面加上out關鍵詞,說明這個泛型類及泛型方法是協變,簡單來說類型A是類型B的子類型,那么Generic<A>也是Generic<B>的子類型,比如在Kotlin中String是Any的子類型,那么List<String>也是List<Any>的子類型,所以List<String>可以賦值給List<Any>。
List協變的特點是它將無法添加元素,只能從里面讀取內容。假如支持協變的List允許插入新對象,那么它就不再是類型安全的了,也就違背了泛型的初衷。
所以我們可以得出結論:支持協變的List只可以讀取,而不可以添加。其實從out這個關鍵詞也可以看出,out就是出的意思,可以理解為List是一個只讀列表。在Java中也可以聲明泛型協變,用通配符及泛型上界來實現協變:<? extends Object>,其中Object可以是任意類。
關于逆變
簡單來說,假若類型A是類型B的子類型,那么Generic<B>反過來是Generic<A>的子類型。
前面我們說過,用out關鍵字聲明的泛型參數類型將不能作為方法的參數類型,但可以作為方法的返回值類型,而in剛好相反。
interface WirteableList<in T> { fun get(index: Int): T //Type parameter T is declared as 'in' but occurs in 'out' position in type T fun get(index: Int): Any //允許 fun add(t: T): Int //允許 }
我們不能將泛型參數類型當作方法返回值的類型,但是作為方法的參數類型沒有任何限制,其實從in這個關鍵詞也可以看出,in就是入的意思,可以理解為消費內容,所以我們可以將這個列表看作一個可寫、可讀功能受限的列表,獲取的值只能為Any類型。在Java中使用<? super T>可以達到相同效果。
Kotlin與Java的型變比較
Kotlin與Java的型變比較關于通配符
MutableList<*>與MutableList<Any?>不是同一種列表,后者可以添加任意元素,而前者只是通配某一種類型,但是編譯器卻不知道這是一種什么類型,所以它不允許向這個列表中添加元素,因為這樣會導致類型不安全。
不過細心的讀者應該發現前面所說的協變也是不能添加元素,那么它們兩者之間有什么關系呢?其實通配符只是一種語法糖,背后上也是用協變來實現的。所以MutableList<*>本質上就是MutableList<out Any?>,使用通配符與協變有著一樣的特性。
點評
這一小節對于理解泛型的型變有很大的幫助,不過前提是你需要先理解Java中的PECS原則(Producer Extends Consumer Super),再閱讀下面的協變和逆變就會輕松不少,其中的示例代碼好評。
協變和逆變描述的就是在集合中,子類與父類之間的轉換關系。協變即子類集合可賦值給父類集合,逆變即父類集合可賦值給子類集合,這是他們最大的特點。只是由于Java本身泛型的擦除特性,整出了一些副作用,如:協變不可添加元素,逆變讀取元素不安全;協變不可作為入參,逆變不可作為返回值等副作用。
Java泛型是高階知識,對于開發框架有很大的幫助,屬于進階必備技能。泛型的詳細知識可參考 http://www.lxweimin.com/p/716e941b3128 里面的2.12小節。
12. 關于惰性求值
引用1
在編程語言理論中,惰性求值(Lazy Evaluation)表示一種在需要時才進行求值的計算方式。在使用惰性求值的時候,表達式不在它被綁定到變量之后就立即求值,而是在該值被取用時才去求值。通過這種方式,不僅能得到性能上的提升,還有一個最重要的好處就是它可以構造出一個無限的數據類型。
通過上面的定義我們可以簡單歸納出惰性求值的兩個好處,一個是優化性能,另一個就是能夠構造出無限的數據類型。
list.asSequence().filter {it > 2}.map {it * 2}.toList()
其實,Kotlin中序列的操作就分為兩類,一類是中間操作,另一類則為末端操作。
引用2 中間操作
在對普通集合進行鏈式操作的時候,有些操作會產生中間集合,當用這類操作來對序列進行求值的時候,它們就被稱為中間操作,比如上面的filter和map。每一次中間操作返回的都是一個序列,產生的新序列內部知道如何去變換原來序列中的元素。中間操作都是采用惰性求值的
引用3 末端操作
在對集合進行操作的時候,大部分情況下,我們在意的只是結果,而不是中間過程。末端操作就是一個返回結果的操作,它的返回值不能是序列,必須是一個明確的結果,比如列表、數字、對象等表意明確的結果。末端操作一般都放在鏈式操作的末尾,在執行末端操作的時候,會去觸發中間操作的延遲計算,也就是將“被需要”這個狀態打開了。
普通集合在進行鏈式操作的時候會先在list上調用filter,然后產生一個結果列表,接下來map就在這個結果列表上進行操作。而序列則不一樣,序列在執行鏈式操作的時候,會將所有的操作都應用在一個元素上,也就是說,第1個元素執行完所有的操作之后,第2個元素再去執行所有的操作,以此類推。
13. 內聯函數簡化抽象工廠
引用
何為抽象工廠模式?即為創建一組相關或相互依賴的對象提供一個接口,而且無須指定它們的具體類。
package factory interface Computer class Dell : Computer class Asus : Computer class Acer : Computer abstract class AbstractFactory { abstract fun produce(): Computer companion object { operator fun invoke(factory: AbstractFactory): AbstractFactory { return factory } } } class DellFactory : AbstractFactory() { override fun produce() = Dell() } class AsusFactory : AbstractFactory() { override fun produce() = Asus() } class AcerFactory : AbstractFactory() { override fun produce() = Acer() } abstract class AbstractFactory2 { abstract fun produce(): Computer companion object { inline operator fun <reified T : Computer> invoke(): AbstractFactory2 = when (T::class) { Dell::class -> DellFactory2() Asus::class -> AsusFactory2() Acer::class -> AcerFactory2() else -> throw IllegalArgumentException() } } } class DellFactory2 : AbstractFactory2() { override fun produce() = Dell() } class AsusFactory2 : AbstractFactory2() { override fun produce() = Asus() } class AcerFactory2 : AbstractFactory2() { override fun produce() = Acer() } fun main() { testAbsFactory() testAbsFactory2() } private fun testAbsFactory2() { // Kotlin中的內聯函數來改善每次都要傳入工廠類對象的做法 val dellFactory = AbstractFactory2<Dell>() val dell = dellFactory.produce() println(dell) } private fun testAbsFactory() { // 當你每次創建具體的工廠類時,都需要傳入一個具體的工廠類對象作為參數進行構造,這個在語法上顯然不是很優雅 val dellFactory = AbstractFactory(DellFactory()) val dell = dellFactory.produce() println(dell) }
由于Kotlin語法的簡潔,以上例子的抽象工廠類的設計也比較直觀。然而,當你每次創建具體的工廠類時(AbstractFactory),都需要傳入一個具體的工廠類對象作為參數進行構造,這個在語法上顯然不是很優雅。而AbstractFactory2就是利用Kotlin中的內聯函數來改善這一情況。我們所需要做的,就是用inline+reified重新實現AbstractFactory2類中的invoke方法。
這下我們的AbstractFactory2類中的invoke方法定義的前綴變長了很多,但是不要害怕,如果你已經掌握了內聯函數的具體應用,應該會很容易理解它。我們來分析下這段代碼:
1)通過將invoke方法用inline定義為內聯函數,我們就可以引入reified關鍵字,使用具體化參數類型的語法特性;
2)要具體化的參數類型為Computer,在invoke方法中我們通過判斷它的具體類型,來返回對應的工廠類對象。
現在我們終于可以用類似創建一個泛型類對象的方式,來構建一個抽象工廠具體對象了。不管是工廠方法還是抽象工廠,利用Kotlin的語言特性,我們在一定程度上改進、簡化了Java中設計模式的實現。
點評
這一節的知識點在實際工作中有很大的用處,inline結合reified,實現具體化類型參數。對比Java,kt在這塊確實抗打,代碼寫出來又進一步優雅了呢。
14. 構建者模式的不足
引用
1)如果業務需求的參數很多,代碼依然會顯得比較冗長;
2)你可能會在使用Builder的時候忘記在最后調用build方法;
3)由于在創建對象的時候,必須先創建它的構造器,因此額外增加了多余的開銷,在某些十分注重性能的情況下,可能就存在一定的問題。
15. by關鍵字簡化裝飾者模式
引用
裝飾者模式,在不必改變原類文件和使用繼承的情況下,動態地擴展一個對象的功能。該模式通過創建一個包裝對象,來包裹真實的對象。
總結來說,裝飾者模式做的是以下幾件事情:
? 創建一個裝飾類,包含一個需要被裝飾類的實例;
? 裝飾類重寫所有被裝飾類的方法;
? 在裝飾類中對需要增強的功能進行擴展。
可以發現,裝飾者模式很大的優勢在于符合“組合優于繼承”的設計原則,規避了某些場景下繼承所帶來的問題。然而,它有時候也會顯得比較啰唆,因為要重寫所有的裝飾對象方法,所以可能存在大量的樣板代碼。
在Kotlin中,我們可以讓裝飾者模式的實現變得更加優雅。猜想你已經想到了它的類委托特性,我們可以利用by關鍵字,將裝飾類的所有方法委托給一個被裝飾的類對象,然后只需覆寫需要裝飾的方法即可。
interface MacBook { fun getCost(): Int fun getDesc(): String fun getProdDate(): String } class MacBookPro : MacBook { override fun getCost() = 10000 override fun getDesc() = "Macbook Pro" override fun getProdDate() = "Late 2019" } class ProcessorUpgradeMacBookPro(private val macBook: MacBook) : MacBook by macBook { override fun getCost() = macBook.getCost() + 219 override fun getDesc() = macBook.getDesc() + ", +1G Memory" } fun main() { val macBookPro = MacBookPro() val processorUpgradeMacBookPro = ProcessorUpgradeMacBookPro(macBookPro) println(processorUpgradeMacBookPro.getCost()) println(processorUpgradeMacBookPro.getDesc()) }
如代碼所示,我們創建一個代表MacBook Pro的類,它實現了MacBook的接口的3個方法,分別表示它的預算、機型信息,以及生產的年份。當你覺得原裝MacBook的內存配置不夠的時候,希望再加入一條1G的內存,這時候配置信息和預算方法都會受到影響。
所以通過Kotlin的類委托語法,我們實現了一個ProcessorUpgradeMacbookPro類,該類會把MacBook接口所有的方法都委托給構造參數對象macbook。因此,我們只需通過覆寫的語法來重寫需要變更的cost和getDesc方法。由于生產年份是不會改變的,所以不需重寫,ProcessorUpgradeMacbookPro類會自動調用裝飾對象的getProdDate方法。
總的來說,Kotlin通過類委托的方式減少了裝飾者模式中的樣板代碼,否則在不繼承Macbook類的前提下,我們得創建一個裝飾類和被裝飾類的公共父抽象類。
點評
裝飾者模式問題所在:要重寫所有的裝飾對象的方法。這也就極大的限制了其使用場景,有時候還不如繼承來的實在。但kt中,通過by關鍵字委托給一個對象,完全化解了這波尷尬,只能說kt語法實在是高。
彩蛋
看完書以后整理的筆記大綱