(譯)Effective Kotlin系列之考慮使用靜態工廠方法替代構造器(一)

極客熊貓發表於2018-08-21

翻譯說明:

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

原文地址: blog.kotlin-academy.com/effective-j…

原文作者: 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”,這也是我想說只要讀者感興趣,我會一直堅持把該系列文章翻譯下去,一起學習。

(譯)Effective Kotlin系列之考慮使用靜態工廠方法替代構造器(一)

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

相關文章