讀書筆記《Kotlin核心編程》

dive-into-kotlin.jpg

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語法實在是高。

彩蛋

看完書以后整理的筆記大綱

筆記大綱
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380