翻譯說明:

原標題: Effective Java in Kotlin, item 1: Consider static factory methods instead of constructors

原文地址: blog.kotlin-academy.com

原文作者: Marcin Moskala

由Joshua Bloch撰寫的Effective Java這本書是Java開發中最重要的書之一。我經常引用它,這也就是為什麼我經常被要求提及更多有關於它的原因。我也對它和Kotlin相關的一些內容非常感興趣,這就是為什麼我決定用Kotlin去一個一個去闡述它們,這是Kotlin學院的博客。只要我看到讀者的興趣,我就繼續下去;)

這是Effective Java的第一條規則:

考慮使用靜態工廠方法替代構造器

讓我們一起來探索吧。

內容前情回顧

Effective Java的第一條規則就表明開發者應該更多考慮使用靜態工廠方法來替代構造器。靜態工廠方法是一個被用來創建一個對象實例的靜態方法。這裡有一些有關靜態工廠方法使用的Java例子:

Boolean trueBoolean = Boolean.valueOf(true);
String number = String.valueOf(12);
List<Integer> list = Arrays.asList(1, 2, 4);

靜態工廠方法是替代構造器一種非常高效的方法。這裡列舉了一些他們優點:

  • 與構造器不同的是,靜態工廠方法他們有方法名. 方法名就表明了一個對象是怎麼創建以及它的參數列表是什麼。例如,正如你所看到的下列代碼: new ArrayList(3).你能猜到3代表什麼意思嗎?它是應該被認為是數組的第一元素還是一個集合的size呢?這無疑不能做到一目瞭然。例如,ArrayList.withSize(3)這個擁有方法名場景就會消除所有疑惑。這是方法名非常有用的一種:它解釋了對象創建的參數或特徵方式。擁有方法名的另一個原因是它解決了具有相同參數類型的構造函數之間的衝突。
  • 與構造器不同的是,每次調用它們時無需創建新對象. 當我們使用靜態工廠方法時,可以使用緩存機制去優化一個對象的創建,這種方式可以提升對象創建時性能。我們還可以定義這樣的靜態工廠方法,如果對象不能被創建就直接返回一個null,就像Connections.createOrNull()方法一樣,當Connection對象由於某些原因不能被創建時就返回一個null.
  • 與構造器不同的是,他們可以返回其返回類型的任何子類的對象. 這個可以在不同的情況下被用來提供更靈活的對象。當我們想要去隱藏介面後面的真正對象時,靜態工廠方法就顯得尤為重要了。例如,在kotlin中所有的Collection都是被隱藏介面背後的。這點很重要是因為在不同平臺的底層引擎下他們是不同的類。當我們調用listOf(1,2,3),如果是在Kotlin/JVM平臺下運行就會返回一個ArrayList對象。相同的調用如果是在Kotlin/JS平臺將會返回一個JavaScript的數組。這是一個優化的實現,並不是一個已存在的問題,因為兩者的集合類都是實現了Kotlin中的List介面。listOf返回的類型是List,這是一個我們正在運行的介面。一般來說隱藏在底層引擎下的實際類型和我們並沒有多大關係. 類似地,在任何靜態工廠方法中,我們可以返回不同類型甚至更改類型的具體實現,只要它們隱藏在某些超類或介面後面,並且被指定為靜態工廠方法返回類型即可。
  • 與構造器不同的是,他們可以減少創建參數化類型實例的冗長程度. 這是一個Java才會有的問題,Kotlin不存在該問題,因為Kotlin中有更好的類型推斷。關鍵是當我們調用構造函數時,我們必須指定參數類型,即使它們非常明確了。然而在調用靜態工廠方法時,則可以避免使用參數類型。

雖然以上那些都是支持靜態工廠方法的使用非常有力的論據,但是Joshua Bloch也指出了一些有關靜態工廠方法缺點:

  • 它們不能用於子類的構造. 在子類構造中,我們需要使用父類構造函數,而不能使用靜態工廠方法。
  • 它們很難和其他靜態方法區分開來. 除了以下情況:valueOf,of,getInstance,newInstance, getTypenewType.這些是不同類型的靜態工廠方法的通用名稱。

在以上論點討論完後,得出的直觀結論是,用於構造對象或者和對象結構緊密相關的對象構造的函數應該被指定為構造函數。另一方面,當構造與對象的結構沒有直接關聯時,則很有可能應該使用靜態工廠方法來定義。

讓我們來到Kotlin吧,當我在學習Kotlin的時候,我感覺有人正在設計它,同時在他面前有一本Effective Java。它解答了本書中描述的大多數Java問題。Kotlin還改變了工廠方法的實現方式。讓我們一起來分析下吧。

伴生工廠方法

在Kotlin中不允許有static關鍵字修飾的方法,類似於Java中的靜態工廠方法通常被稱為伴生工廠方法,它是一個放在伴生對象中的工廠方法:

class MyList {
//...
companion object {
fun of(vararg i: Int) { /*...*/ }
}
}

用法與靜態工廠方法的用法相同:

MyList.of(1,2,3,4)

在底層實現上,伴生對象實際上就是個單例類,它有個很大的優點就是Companion對象可以繼承其他的類。這樣我們就可以實現多個通用工廠方法並為它們提供不同的類。使用的常見例子是Provider類,我用它作為DI的替代品。我有下列這個類:

abstract class Provider<T> {
var original: T? = null
var mocked: T? = null
abstract fun create(): T
fun get(): T = mocked ?: original ?: create()
.apply { original = this }
fun lazyGet(): Lazy<T> = lazy { get() }
}

對於不同的元素,我只需要實現具體的創建函數即可:

interface UserRepository {
fun getUser(): User
companion object: Provider<UserRepository> {
override fun create() = UserRepositoryImpl()
}
}

有了這樣的定義,我可以通過UserReposiroty.get()方法獲取repository實例對象,或者在代碼的任何地方通過val user by UserRepository.lazyGet()這種懶載入方式獲得相應的實例對象。我也可以為測試例子指定不同的實現或者通過UserRepository.mocked = object: UserRepository { /*...*/ }實現模擬測試的需求。

與Java相比,這是一個很大的優勢,其中所有的SFM(靜態工廠方法)都必須在每個對象中手動實現。此外通過使用介面委託來重用工廠方法的方式仍然被低估了。在上面的例子中我們可以使用這種方式:

interface Dependency<T> {
var mocked: T?
fun get(): T
fun lazyGet(): Lazy<T> = lazy { get() }
}
abstract class Provider<T>(val init: ()->T): Dependency<T> {
var original: T? = null
override var mocked: T? = null

override fun get(): T = mocked ?: original ?: init()
.apply { original = this }
}
interface UserRepository {
fun getUser(): User
companion object: Dependency<UserRepository> by Provider({
UserRepositoryImpl()
})
}

用法是相同的,但請注意,使用介面委託我們可以從單個伴生對象中的不同類獲取工廠方法,並且我們只能獲得介面中指定的功能(依據介面隔離原則的設計非常好)。瞭解更多有關介面委託

擴展工廠方法

請注意另一個優點就是考慮將工廠方法放在一個伴生對象裏而不是被定義為一個靜態方法: 我們可以為伴生對象定義擴展方法。因此如果我們想要把伴生工廠方法加入到被定義在外部類庫的Kotlin類中,我們還是可以這樣做的(只要它能定義任意的伴生對象)

interface Tool {
companion object { }
}
fun Tool.Companion.createBigTool() : BigTool { }

或者,伴生對象被命名的情況:

interface Tool {
companion object Factory { }
}
fun Tool.Factory.createBigTool() : BigTool { }

讓我們從代碼中共享外部庫的使用這是一種很強大的可能,據我所知 Kotlin現在是唯一提供這種可能性的語言。

頂層函數

在Kotlin中,更多的是定義頂層函數而不是CFM(伴生對象工廠方法)。比如一些常見的例子listOf,setOf和mapOf,同樣庫設計者正在制定用於創建對象的頂級函數。它們將會被廣泛使用。例如,在Android中,我們傳統上定義一個函數來創建Activity Intent作為靜態方法:

//java
class MainActivity extends Activity {
static Intent getIntent(Context context) {
return new Intent(context, MainActivity.class);
}
}

在Kotlin的Anko庫中,我們可以使用頂層函數intentFor加reified類型來替代:

intentFor<MainActivity>()

這種解決方案的問題在於,雖然公共的頂層函數隨處可用,但是很容易讓用戶丟掉IDE的提示。這個更大的問題在於當有人創建頂級函數時,方法名不直接指明它不是方法。使用頂級函數創建對象是小型和常用對象創建方式的完美選擇,比如List或者Map,因為listOf(1,2,3)List.of(1,2,3)更簡單並且更具有可讀性。但是公共的頂層函數需要被謹慎使用以及不能濫用。

偽構造器

Kotlin中的構造函數與頂級函數的工作方式類似:

class A()
val a = A()

它們也可以和頂層函數一樣被引用:

val aReference = ::A

類構造函數和函數之間唯一的區別是函數名不是以大寫開頭的。雖然技術上允許,但是這個事實已經適用於Kotlin的很多不同地方其中包括Kotlin標準庫在內。ListMutableList都是介面,但是它們沒有構造器,但是Kotlin開發者希望允許以下List的構造:

List(3) { "$it" } // same as listOf("0", "1", "2")

這就是為什麼在Collections.kt中就包含以下函數(自Kotlin 1.1起):

public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)

public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
val list = ArrayList<T>(size)
repeat(size) { index -> list.add(init(index)) }
return list
}

它們看起來很像構造器,很多開發人員都沒有意識到它們是底層實現的頂層函數。同時,它們具有SFM(靜態工廠方法)的一些優點:它們可以返回類型的子類型,並且它們不需要每次都創建對象。它們也沒有構造器相關的要求。例如,輔助構造函數需要立即調用超類的主構造函數或構造函數。當我們使用偽構造函數時,我們可以推遲構造函數的使用:

fun ListView(config: Config) : ListView {
val items = // Here we read items from config
return ListView(items) // We call actual constructor
}

頂層函數和作用域

我們可能想要在類之外創建工廠方法的另一個原因是我們想要在某個特定作用域內創建它。就像我們只在某個特定的類或文件中需要工廠方法一樣。

有些人可能會爭辯說這種使用會產生誤導,因為對象創建作用域通常與該類可見作用域相關聯。所有的這些可能性都是表達意圖的強有力的工具,它們需要被理智地使用。雖然對象創建的具體方式包含有關它的信息,但在某些情況下使用這種可能性是非常有價值的。

主構造器

Kotlin中有個很好的特性叫做主構造器。在Kotlin類中只能有一個主構造器,但是它們比Java中已知的構造函數(在Kotlin中稱為輔助構造器)更強大。主構造器的參數可以被用在類創建的任何地方。

class Student(name: String, surname: String) {
val fullName = "$name $surname"
}

更重要的是,可以直接定義這些參數作為屬性:

class Student(val name: String, val surname: String) {
val fullName
get() = "$name $surname"
}

應該清楚的是,主構造器與類創建是密切相關的。請注意,當我們使用帶有默認參數的主構造器時,我們不需要伸縮構造器。感謝所有這些,主構造器經常被使用(我在我的項目上創建了數百個類,我發現只有少數沒有用主構造器),並且很少使用輔助構造器。這很棒。我認為就應該是這樣的。主構造器與類結構和初始化緊密相關,因此在我們應該定義構造器而不是工廠方法時,它完全符合需求條件。對於其他情況,我們很可能應該使用伴隨對象工廠方法或頂級函數而不是輔助構造器。

創建對象的其他方式

Kotlin的工廠方法優點並不僅僅是Kotlin如何改進對象的創建。在下一篇文章中,我們將描述Kotlin如何改進構建器模式。例如,包含多個優化,允許用於創建對象的DSL:

val dialog = alertDialog {
title = "Hey, you!"
message = "You want to read more about Kotlin?"
setPositiveButton { makeMoreArticlesForReader() }
setNegativeButton { startBeingSad() }
}

我想起來了。在本文中,我最初只描述了靜態工廠方法的直接替代方法,因為這是Effective Java的第一項。與本書相關的其他吸引人的Kotlin功能將在下一篇文章中描述。如果您想收到通知,請訂閱消息推送。

總結

雖然Kotlin在對象創建方面做了很多改變,但是Effective Java中有關靜態工廠方法的爭論仍然是最火的。改變的是Kotlin排除了靜態成員方法,而是我們可以使用如下具有SFM優勢的替代方法:

  • 伴生對象工廠方法
  • 頂層函數
  • 偽構造器
  • 擴展工廠方法

它們中的每一個都是在不同的需求場景下使用,並且每個都具有與Java SFM不同的優點。

一般規則是,在大多數情況下,我們創建對象所需的全部是主構造器,默認情況下連接到類結構和創建。當我們需要其他構建方法時,我們應該最有可能使用一些SFM替代方案。

譯者有話說

首先,說下為什麼我想要翻譯有關Effective Kotlin一系列的文章。總的來說Kotlin對大家來說已經不陌生,相信有很多小夥伴,無論是在公司項目還是自己平時demo項目去開始嘗試使用kotlin.當學完了Kotlin的基本使用的時候,也許你是否能感受已經到了一個瓶頸期,那麼我覺得Effective Kotlin就是不錯選擇。Effective Kotlin教你如何去寫出更好更優的Kotlin代碼,有的人會有疑問這和Java有什麼區別,我們都知道Kotlin和Java極大兼容,但是它們區別還是存在的。如果你已經看過Effective Java的話,相信在對比學習狀態,你能把Kotlin理解更加透徹。

然後還有一個原因,在Medium上注意到該文章的作者已經幾乎把對應Effective Java 的知識點用Kotlin過了一遍,從中指出了它們相同點、不同點以及Kotlin在相同的場景下實現的優勢所在。

最後,用原作者一句話結尾 「I will continue as long as I see an interest from readers」,這也是我想說只要讀者感興趣,我會一直堅持把該系列文章翻譯下去,一起學習。

weixin.qq.com/r/aUjI0Lv (二維碼自動識別)

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~


推薦閱讀:
相關文章