[譯] 當設計模式遇上 Kotlin

boileryao發表於2017-06-22

Kotlin 正在得到越來越廣泛的應用。如果把常用的設計模式用 Kotlin 來實現會是什麼樣子呢?

受到 Mario Fusco 的“從‘四人幫’到 lambda”(相關的視訊部落格程式碼)的啟發,我決定動手實現一些電腦科學領域最著名的設計模式,用 “Kotlin”!(“四人幫”指 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides,四人在所著的《Design Patterns: Elements of Reusable Object-Oriented Software 》一書中介紹了 23 種設計模式,該書被譽為設計模式的經典之作。——譯註)

當然,我的目標不是簡單的 實現 這些模式。因為 Kotlin 支援物件導向程式設計並且和 Java 是可互操作的,我可以從 Mario 的倉庫直接複製貼上每一個 Java 檔案(先不管是“傳統”的還是“lambda 風格”的),它們將仍然可以正常工作

需要特別說明一下,這些模式的發明是為了彌補起源於上世紀九十年代的一些指令式程式設計語言(尤其是 C++)的不足。很多現代程式語言提供瞭解決這些不足的特性,我們完全不需要再寫多餘的程式碼或者做刻意模仿設計模式這種事了。

這就是為什麼我像 Mario (相關倉庫地址:gof)那樣,去尋找一種更簡單方便、更慣用的方式來解決這些模式所要解決的問題。

如果不想看下面這坨說明文字的話,你可以直接去 這個 GitHub 倉庫 看程式碼。


眾所周知,根據“四人幫”的定義設計模式可以分為三種: 結構型建立型行為型

一開始,我們先來看結構型設計模式。這不是很好搞,因為結構型設計模式是關於結構的。怎樣用一個 不同 的結構來實現這個結構呢,臣妾做不到啊。不過, 裝飾器模式 是個例外。雖然在技術層面來說算是結構型,但就使用來說,更多是和行為及職責有關的(裝飾器模式,每個負責進行包裝的類具有增加某一行為這一職責。——譯註)。

結構型設計模式

裝飾器模式(Decorator)

動態地給物件新增行為(職責)

假設我們想用一些特效(duang)來裝飾 Text 這個類:

class Text(val text: String) {
    fun draw() = print(text)
}複製程式碼

如果瞭解這個模式的話,你應該知道我們需要建立一些類來“修飾”(即,擴充行 為) Text 類。

在 Kotlin 中,我們可以用 函式擴充(extension functions) 來避免建立這麼一大坨類:

fun Text.underline(decorated: Text.() -> Unit) {
    print("_")
    this.decorated()
    print("_")
}

fun Text.background(decorated: Text.() -> Unit) {
    print("\u001B[43m")
    this.decorated()
    print("\u001B[0m")
}複製程式碼

有了這些擴充函式,我們現在可以例項化一個 Text 物件,並且在不建立其他類的情況下來修飾它的 draw 方法:

Text("Hello").run {
    background {
        underline {
            draw()
        }
    }
}複製程式碼

執行這段程式碼,你會看見帶有彩色背景的“_Hello_”(如果終端支援 ansi 顏色的話)。

跟原本的裝飾者相比,這裡有一個不足:由於沒有用來裝飾的類了,所以我們不能使用“預裝飾”過的物件了。

可以再次使用函式來解決這個問題,函式是 Kotlin 中的“一等公民”。我們可以這樣寫:

fun preDecorated(decorated: Text.() -> Unit): Text.() -> Unit {
    return { background { underline { decorated() } } }
}複製程式碼

建立型設計模式

Builder 模式

將複雜物件的構造與其表示分開,以便相同的構造過程可以建立不同形式的物件

Builder 模式很好用,可以避免臃腫的建構函式引數列表,還能方便地複用預先定義好的配置物件的程式碼。 Kotlin 的 apply 擴充套件原生支援 Builder 模式。

假設有一個 Car 類:

class Car() {
    var color: String = "red"
    var doors = 3
}複製程式碼

除了為這個類單獨建立一個 CarBuilder ,我們可以使用 applyalso 也行)擴充來初始化一輛車:

Car().apply {
    color = "yellow"
    doors = 5
}複製程式碼

由於函式可以賦值給一個變數,所以這個初始化過程也可以放在一個變數裡。這樣,我們就有了一個預先定義好的 Builder “函式”,比如 val yellowCar: Car.() -> Unit = { color = "yellow" }

原型模式(Prototype)

使用原型化的例項指定要建立的物件的種類,並通過複製此例項來建立特定的新物件

在 Java 中,原型模式理論上可以用 Cloneable 介面和 Object.clone() 來實現。然而,clone 有很大的不足,所以我們應該避免使用它。

Kotlin 用資料類(data classes)提供瞭解決方案。

當使用資料類的時候,我們將免費得到 equalshashCodetoStringcopy 這幾個函式。通過 copy,我們可以複製一整個物件並且修改所得到的新物件的一些屬性。

data class EMail(var recipient: String, var subject: String?, var message: String?)
...

val mail = EMail("abc@example.com", "Hello", "Don't know what to write.")

val copy = mail.copy(recipient = "other@example.com")

println("Email1 goes to " + mail.recipient + " with subject " + mail.subject)
println("Email2 goes to " + copy.recipient + " with subject " + copy.subject)複製程式碼

單例模式(Singleton)

確保一個類只有一個例項,並提供這個例項的全域性訪問點

儘管近來 單例模式 被認為是“反設計模式的”,但是它也有自己獨特的用處(本文不會討論這個話題,只是戰戰剋剋剋剋的來使用它)。

在 Java 中建立 單例 還是需要一番操作的,但是在 Kotlin 中只需要簡單的使用 object 宣告就可以了。

object Dictionary {
    fun addDefinition(word: String, definition: String) {
        definitions.put(word.toLowerCase(), definition)
    }

    fun getDefinition(word: String): String {
        return definitions[word.toLowerCase()] ?: ""
    }
}複製程式碼

這裡使用的 object 關鍵詞會自動建立出 Dictionary 這個類以及它的一個單例。這個單例以“懶漢模式”建立,用到它時才會進行建立。

單例的訪問方式和 Java 的靜態方法差不多:

val word = "kotlin"
Dictionary.addDefinition(word, "an awesome programming language created by JetBrains")
println(word + " is " + Dictionary.getDefinition(word))複製程式碼

行為型設計模式

模板方法(Template Method)

在操作中定義演算法(步驟)的骨架,將一些步驟委託給子類

這個設計模式同時用到了類的繼承。定義一些 抽象方法 並且在基類呼叫這些方法。抽象方法由子類負責實現。

//java
public abstract class Task {
        protected abstract void work();
        public void execute(){
            beforeWork();
            work();
            afterWork();
        }
    }複製程式碼

現在從 Task 派生出一個在 work 方法中真正做了事情的具體類。

裝飾器模式 使用函式擴充類似,這裡的 模板方法 通過頂層函式實現。

//kotlin
fun execute(task: () -> Unit) {
    val startTime = System.currentTimeMillis() //"beforeWork()"
    task()
    println("Work took ${System.currentTimeMillis() - startTime} millis") //"afterWork()"
}

...
//usage:
execute {
    println("I'm working here!")
}複製程式碼

看,根本沒有必要寫任何類!有人可能會有疑問,這不是 策略模式 嗎,這個疑問不無道理。從另一方面來看,策略模式模板方法 確實在解決很相似的問題(如果有什麼不同)。

策略模式(Strategy)

定義一系列演算法,封裝每個演算法,並使它們可以互換

有一些 Customer ,他們每個月都要付一筆特定的費用。對於某些特定的人,這筆費用可以打折。我們不去為每種打折 策略 都去寫一個對應的 Customer 子類,而是採用 策略模式

class Customer(val name: String, val fee: Double, val discount: (Double) -> Double) {
    fun pricePerMonth(): Double {
        return discount(fee)
    }
}複製程式碼

注意這裡沒有使用介面,而是使用 (Double) -> Double (Double 到 Double)的函式來替代。為了使這個變換看上去有意義,我們可以宣告一個型別別名,這樣也不失高階函式的靈活性: typealias Discount = (Double) -> Double.

無論哪種方式,我都可以定義多種 策略 來計算折扣。

val studentDiscount = { fee: Double -> fee/2 }
val noDiscount = { fee: Double -> fee }
...

val student = Customer("Ned", 10.0, studentDiscount)
val regular = Customer("John", 10.0, noDiscount)

println("${student.name} pays %.2f per month".format(student.pricePerMonth()))
println("${regular.name} pays %.2f per month".format(regular.pricePerMonth()))複製程式碼

迭代器模式(Iterator)

提供了一種在不暴露其底層表示的情況下順序訪問聚合物件內部元素的方法

其實很難遇到需要手搓一個 迭代器 的情況。大多數情況,包裝一個 List 並且實現 Iterable介面要更簡單方便。

在 Kotlin 中, iterator() 是個操作符函式。這意味著當一個類定義了 operator fun iterator() 這個函式後,可以使用 for 迴圈來遍歷它(不需要宣告介面)。這個函式也能通過擴充函式配合使用,這是很酷炫的。 通過擴充函式,我們可以讓 每一個 物件都是可迭代的。看下面這個例子:

class Sentence(val words: List<String>)
...
operator fun Sentence.iterator(): Iterator<String> = words.iterator()複製程式碼

現在我們可以在 Sentence 上進行迭代操作了。如果沒有這個類的控制權的話,迭代器仍然將正常工作。

更多的模式……

這篇文章確實提到了相當幾個設計模式,但這不是 “四人幫” 設計模式的全部。就像我在一開始提到的那樣,尤其是結構型設計模式很難甚至根本不可能用和 Java 不同的方法來實現。 你可以在 這個程式碼倉庫 找到更多的設計模式。歡迎來提交反饋和 PR。☺

希望這篇文章能給你些啟發,讓你認識到 Kotlin 可以為廣為人知的問題帶來的新的解決方案。

最後我想說的是,倉庫中的程式碼量大概有 ⅓ 的 Kotlin 和 ⅔ 的 Java,雖然這兩部分程式碼幹了同樣的事情?


封面圖片來自 stocksnap.io


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章