翻譯說明:java
原標題: Effective Java in Kotlin, item 2: Consider a builder when faced with many constructor parametersandroid
原文地址: blog.kotlin-academy.com/effective-j…git
原文做者: Marcin Moskala程序員
這篇文章對Java程序員將會有很大的影響。當咱們在處理各類各樣的對象建立的操做是,這是一個很常見的場景。Effective Java中提出的很好的論據建議開發人員使用Builder構建器而不是伸縮構造函數模式。雖然Kotlin改變了不少 - 它給了咱們更好的可能性。咱們很快就會看到它github
這是Effective Java edition 2的第二條規則:安全
面對許多構造函數時使用BUILDERS
併發
讓咱們來探索吧。app
在Java中,一般方式是使用可選的構造函數參數的可伸縮構造器模式去定義一個對象。當咱們使用可伸縮構造器模式時,能夠爲每一個使用的集合或參數定義一個單獨的構造器。如下是Kotlin的一個例子:框架
class Dialog constructor(
val title: String,
val text: String?,
val onAccept: (() -> Unit)?
) {
constructor(title: String, text: String)
: this(title, text, null)
constructor(title: String)
: this(title, "")
}
// Usage
val dialog1 = Dialog("Some title", "Great dialog", { toast("I was clicked") })
val dialog2 = Dialog("Another dialog","I have no buttons")
val dialog3 = Dialog("Dialog with just a title")
複製代碼
在Android中很是常見的例子就是咱們如何實現一個自定義View。 儘管這種模式在JVM世界中很流行,可是Effective Java認爲對於更大或更復雜的對象,咱們應該使用Builder模式。Builder模式首先以可讀和緊湊的方式獲取參數列表,而後驗證並實例化對象。這是一個例子:ide
class Dialog private constructor(
val title: String,
val text: String?,
val onAccept: (() -> Unit)?
) {
class Builder(val title: String) {
var text: String? = null
var onAccept: (() -> Unit)? = null
fun setText(text: String?): Builder {
this.text = text
return this
}
fun setOnAccept(onAccept: (() -> Unit)?): Builder {
this.onAccept = onAccept
return this
}
fun build() = Dialog(title, text, onAccept)
}
}
// Usage
val dialog1 = Dialog.Builder("Some title")
.setText("Great dialog")
.setOnAccept { toast("I was clicked") }
.build()
val dialog2 = Dialog.Builder("Another dialog")
.setText("I have no buttons")
.build()
val dialog3 = Dialog.Builder("Dialog with just a title").build()
複製代碼
在可伸縮的構造器模式中,聲明和用法都顯得比較強大,可是builder模式有着更爲重要的優勢:
當咱們須要設置可選參數時,這個特性使構建器模式對大多數類更加明確,有彈性而且更好。
本章最受歡迎的部分,來自Effective Java的第二版,以下:
Builder模式模擬Ada和Python語言中的命名可選參數。
很棒的是,在Kotlin中,咱們不須要模擬命名的可選參數,由於咱們能夠直接使用它們。在大多數狀況下,可選參數比Builder構建器要更好。只需比較上面的構建器模式和下面命名的可選參數便可,就會發現聲明和使用都更清晰,更短,更具表現力:
class Dialog(
val title: String,
val text: String? = null,
val onAccept: (() -> Unit)? = null
)
// Usage
val dialog1 = Dialog(
title = "Some title",
text = "Great dialog",
onAccept = { toast("I was clicked") }
)
val dialog2 = Dialog(
title = "Another dialog",
text = "I have no buttons"
)
val dialog3 = Dialog(title = "Dialog with just a title")
複製代碼
具備命名可選參數的構造函數具備Builder構建器模式的大部分優勢
在這個簡單的示例中,具備命名可選參數的構造函數看起來更好,可是,若是咱們須要針對不一樣參數的不一樣建立變體呢?假設咱們爲不一樣的參數集建立不一樣類型的對話框。咱們能夠在Builder構建器中輕鬆地解決該問題:
interface Dialog {
fun show()
class Builder(val title: String) {
var text: String? = null
var onAccept: (() -> Unit)? = null
fun setText(text: String?): Builder {
this.text = text
return this
}
fun setOnAccept(onAccept: (() -> Unit)?): Builder {
this.onAccept = onAccept
return this
}
fun build(): Dialog = when {
text != null && onAccept != null ->
TitleTextAcceptationDialog(title, text!!, onAccept!!)
text != null ->
TitleTextDialog(title, text!!)
onAccept != null ->
TitleAcceptationDialog(title, onAccept!!)
else -> TitleDialog(title)
}
}
}
// Usage
val dialog1 = Dialog.Builder("Some title")
.setText("Great dialog")
.setOnAccept { toast("I was clicked") }
.build()
val dialog2 = Dialog.Builder("Another dialog")
.setText("I have no buttons")
.build()
val dialog3 = Dialog.Builder("Dialog with just a title").build()
複製代碼
那咱們可使用命名的可選參數來解決該類問題嗎?是的,咱們可使用不一樣的構造函數或使用工廠方法來實現相同的功能!如下是針對上述問題示例的解決方案:
interface Dialog {
fun show()
}
fun makeDialog( title: String, text: String? = null, onAccept: (() -> Unit)?
): Dialog = when {
text != null && onAccept != null ->
TitleTextAcceptationDialog(title, text, onAccept)
text != null ->
TitleTextDialog(title, text)
onAccept != null ->
TitleAcceptationDialog(title, onAccept)
else ->
TitleDialog(title)
}
// Usage
val dialog1 = makeDialog(
title = "Some title",
text = "Great dialog",
onAccept = { toast("I was clicked") }
)
val dialog2 = makeDialog(
title = "Another dialog",
text = "I have no buttons"
)
val dialog3 = makeDialog(title = "Dialog with just a title")
複製代碼
這是咱們的另外一個例子,咱們再次看到命名參數優於builder模式的地方:
Builder構建器模式的一個優勢在於具備填充參數特性可用做工廠模式。雖然這種狀況不多見,但這種優點微乎其微。
Builder構建器模式的另外一個討論點是咱們能夠部分填充構建器並進一步傳遞它。這樣咱們就能夠定義建立部分填充構建器的方法,而且能夠修改它們(好比咱們的應用程序的默認對話框)。爲了有相似的構造函數或工廠方法的可能性,咱們須要自動的柯里化(這在Kotlin中是可能的,但不是沒有名稱和默認參數丟失)。雖然這種對象建立方式不是很是常見,但一般也不是優選的。若是咱們要爲應用程序定義默認對話框,可使用函數來建立它並將全部自定義元素做爲可選參數傳遞。這種方法能夠更好地控制對話框Dialog的建立。
通常規則是,在大多數狀況下,命名可選參數應優先於構建器模式。儘管這不是Kotlin給咱們的Builder建造者模式的惟一新選擇。另外一個很是受歡迎的是用於對象構建的DSL。咱們來描述一下。
假設咱們須要設置具備多個處理程序的監聽器。相似於Java的經典方法是使用對象表達式:
taskNameView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
// ...
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// ...
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// no-op
}
})
複製代碼
這種方法不是很方便,可使用命名的可選參數輕鬆替換爲更簡潔的工廠方法:
fun makeTextWatcher( afterTextChanged: ((s: Editable?) -> Unit)? = null,
beforeTextChanged: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = null,
onTextChanged: ((s: CharSequence?, start: Int, before: Int, count: Int) -> Unit)? = null
) = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
afterTextChanged?.invoke(s)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
beforeTextChanged?.invoke(s, start, count, after)
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextChanged?.invoke(s, start, before, count)
}
}
// Usage
taskNameView.addTextChangedListener(makeTextWatcher(
afterTextChanged = { s ->
// ..
},
beforeTextChanged = { s, start, count, after ->
// ...
}
))
複製代碼
請注意,咱們能夠輕鬆地把進一步改形成TextView的擴展函數:
taskNameView.addTextChangedListener(
afterTextChanged = { s ->
// ..
},
beforeTextChanged = { s, start, count, after ->
// ...
}
)
複製代碼
這是DSL的一個簡單示例。支持這種表示法的函數能夠在相似Anko或Android-ktx這樣的流行Kotlin庫中找到。例如,這是咱們如何在Anko中定義和顯示warning對話框:
alert("Hi, I'm Roy", "Have you tried turning it off and on again?"){
yesButton { toast("Oh…") }
noButton {}
}.show()
複製代碼
問題在於這種表示方法須要寫不少支持它們的聲明,例如這是咱們如何定義上面的TextView的addOnTextChangedListener擴展方法:
fun TextView.addOnTextChangedListener( config: TextWatcherConfiguration.() -> Unit
) {
val listener = TextWatcherConfiguration().apply { config() }
addTextChangedListener(listener)
}
class TextWatcherConfiguration : TextWatcher {
private var beforeTextChangedCallback: (BeforeTextChangedFunction)? = null
private var onTextChangedCallback: (OnTextChangedFunction)? = null
private var afterTextChangedCallback: (AfterTextChangedFunction)? = null
fun beforeTextChanged(callback: BeforeTextChangedFunction) {
beforeTextChangedCallback = callback
}
fun onTextChanged(callback: OnTextChangedFunction) {
onTextChangedCallback = callback
}
fun afterTextChanged(callback: AfterTextChangedFunction) {
afterTextChangedCallback = callback
}
override fun beforeTextChanged( s: CharSequence, start: Int, count: Int, after: Int ) {
beforeTextChangedCallback?.invoke(s.toString(), start, count, after)
}
override fun onTextChanged( s: CharSequence, start: Int, before: Int, count: Int ) {
onTextChangedCallback?.invoke(s.toString(), start, before, count)
}
override fun afterTextChanged(s: Editable) {
afterTextChangedCallback?.invoke(s)
}
}
private typealias BeforeTextChangedFunction =
(text: String, start: Int, count: Int, after: Int) -> Unit
private typealias OnTextChangedFunction =
(text: String, start: Int, before: Int, count: Int) -> Unit
private typealias AfterTextChangedFunction =
(s: Editable) -> Unit
複製代碼
對單個甚至兩個用法進行此類聲明是不合理的。另外一方面,當咱們開發一個庫時,這不是問題。這就是爲何在大多數狀況下咱們使用庫中定義的DSL的緣由。當咱們定義它們時,它們很是強大。請注意,在DSL內部,包括使用控制循環語句結構(if
for
,等等),定義變量等。這是一個爲Kot.Academy官網生成的HTML使用DSL的例子。
private fun RDOMBuilder<DIV>.authorDiv( author: String?, authorUrl: String? ) {
author ?: return
div(classes = "main-text multiline space-top") {
+"Author: "
if (authorUrl.isNullOrBlank()) {
+author
} else {
a(href = authorUrl) { +author }
}
}
}
複製代碼
除了聲明以外,咱們也指定了如何定義這個元素的邏輯。這樣的DSL一般比具備命名可選參數的構造函數或工廠方法強大得多。固然它也更復雜,更難定義。
在一些Android項目中能夠觀察到有趣的解決方案,其中開發人員實現了使用切除構建器的簡化DSL。
假設咱們使用來自庫(或框架)的對話框Dialog,它提供構建器做爲建立方法(假設它是用Java實現的):
val dialog1 = Dialog.Builder("Some title")
.setText("Great dialog")
.setOnAccept { toast("I was clicked") }
.build()
複製代碼
這就是如何實現和使用很是簡單的DSL構建器:
fun Dialog(title: String, init: Dialog.Builder.()->Unit) =
Dialog.Builder(title).apply(init).build()
// Usage
val dialog1 = Dialog("Some title") {
text = "Great dialog"
setOnAccept { toast("I was clicked") }
}
複製代碼
(只有在Java中定義了此方法時,咱們才能將text
設置爲屬性) 這樣咱們就擁有了DSL的最大優勢和很是簡單的聲明。這也代表了DSL和Builder構建器模式的共同點。他們有相似的理念,但DSL更像是下一代的建造者模式。
Effective Java的參數在Kotlin中仍然有效,而構建器模式比以前的Java替代方案更合理。儘管 Kotlin介紹了按名稱指定參數並提供默認參數。多虧了這一點,咱們能夠更好地替代構建器模式。Kotlin還提供了容許DSL使用的功能。定義良好的DSL甚至是更好的替代方案,由於它提供了更大的靈活性並容許在對象定義實現邏輯。
去斷定對象建立到底有多複雜不是一個簡單的問題,並且每每須要必定的經驗。Kotlin給咱們帶來很是重要的可能性,此外它們對Kotlin的發展也產生了積極的影響。
首先,回答下爲何要翻譯這篇文章,這篇文章是Effective Kotlin系列的第二篇,這篇講的是咱們熟悉的Builder建造者模式,當咱們遇到構造器中有不少參數的時,我都會考慮使用Builder模式來替代它。固然這只是Java中常見操做,可是Kotlin是否是得循序漸進照着Java來呢?顯然不是,Kotlin中有着更爲優雅和強大的實現方式構造器+默認值參數。若是不瞭解本篇文章初學者,估計就會拿着Kotlin語言生搬硬套成Java的Builder實現方式,殊不知Kotlin中有更爲優雅的實現方案。
其實,本篇文章繼續體現Effective Kotlin一個宗旨: 經過對比Effective Java高效編碼準則,在Kotlin中去尋找更爲優雅實現方式和替代解決方案,而不是帶着Java思惟寫着Kotlin代碼來翻譯實現。從而進一步體驗Kotlin與Java不一樣以及各自優缺點。
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~