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

翻譯說明:java

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

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

原文做者: Marcin Moskalaapp

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

這是Effective Java的第一條規則:函數

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

讓咱們一塊兒來探索吧。post

內容前情回顧

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

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索