[譯]Object的局限性——Kotlin中的帶參單例模式

原文:Kotlin singletons with argument
——object has its limits

作者:Christophe Beyls
譯者:卻把清梅嗅

Kotlin中,單例模式被用于替換該編程語(yǔ)言中不存在的static成員和字段。 你通過(guò)簡(jiǎn)單地聲明object以創(chuàng)建一個(gè)單例:

object SomeSingleton

與類(lèi)不同,object 不允許有任何構(gòu)造函數(shù),如果有需要,可以通過(guò)使用 init 代碼塊進(jìn)行初始化的行為:

object SomeSingleton {
    init {
        println("init complete")
    }
}

這樣object將被實(shí)例化,并且在初次執(zhí)行時(shí),其init代碼塊將以線(xiàn)程安全的方式懶惰地執(zhí)行。 為了這樣的效果,Kotlin對(duì)象實(shí)際上依賴(lài)于Java靜態(tài)代碼塊 。上述Kotlin的 object 將被編譯為以下等效的Java代碼:

public final class SomeSingleton {
   public static final SomeSingleton INSTANCE;

   private SomeSingleton() {
      INSTANCE = (SomeSingleton)this;
      System.out.println("init complete");
   }

   static {
      new SomeSingleton();
   }
}

這是在JVM上實(shí)現(xiàn)單例的首選方式,因?yàn)樗梢栽诰€(xiàn)程安全的情況下懶惰地進(jìn)行初始化,同時(shí)不必依賴(lài)復(fù)雜的雙重檢查加鎖(double-checked locking)等加鎖算法。 通過(guò)在Kotlin中簡(jiǎn)單地使用object進(jìn)行聲明,您可以獲得安全有效的單例實(shí)現(xiàn)。

image

圖:無(wú)盡的孤獨(dú)——單例(譯者:作者的描述讓我想起了一個(gè)悲情的角色,Maiev Shadowsong

傳遞一個(gè)參數(shù)

但是,如果初始化的代碼需要一些額外的參數(shù)呢?你不能將任何參數(shù)傳遞給它,因?yàn)?code>Kotlin的object關(guān)鍵字不允許存在任何構(gòu)造函數(shù)。

有些情況下,將參數(shù)傳遞給單例初始化代碼塊是被推薦的方式。 替代方法要求單例類(lèi)需要知道某些能夠獲取該參數(shù)的外部組件(component),但違反了關(guān)注點(diǎn)分離的原則并且使得代碼不可被復(fù)用。

為了緩解這個(gè)問(wèn)題,該外部組件可以是 依賴(lài)注入系統(tǒng)。這的確是一個(gè)具有可行性的解決方案,但您并不總是希望使用這種類(lèi)型的庫(kù)——并且,在某些情況下您也無(wú)法使用它,就像在接下來(lái)的Android示例中我將會(huì)所提到的。

在Kotlin中,您必須通過(guò)不同的方式去管理單例的另一種情況是,單例的具體實(shí)現(xiàn)是由外部工具或庫(kù)(比如RetrofitRoom等等)生成的,它們的實(shí)例是通過(guò)使用Builder模式或Factory模式來(lái)獲取的——在這種情況下,您通常將單例通過(guò)interfaceabstract class進(jìn)行聲明,而不是object。

一個(gè)Android示例

Android平臺(tái)上,您經(jīng)常需要將Context實(shí)例作為參數(shù)傳遞給單例組件的初始化代碼塊中,以便它們可以獲取 文件路徑,讀取系統(tǒng)設(shè)置開(kāi)啟Service等等,但您還希望避免對(duì)其進(jìn)行靜態(tài)引用(即使是Application的靜態(tài)引用在技術(shù)上是安全的)。 有兩種方法可以實(shí)現(xiàn)這一目標(biāo):

  • 提前初始化:在運(yùn)行任何(幾乎)其他代碼之前,通過(guò)在Application.onCreate()中調(diào)用初始化所有組件,此時(shí)Application是可用的——這個(gè)簡(jiǎn)單的解決方案的主要缺點(diǎn)是它是通過(guò)阻塞主線(xiàn)程的方式來(lái)減慢應(yīng)用程序啟動(dòng),并初始化了所有組件,甚至包括那些不會(huì)立即使用的組件。另一個(gè)鮮為人知的問(wèn)題是,在調(diào)用此方法之前,Content Provider也許已經(jīng)被實(shí)例化了(正如文檔中所提到的),因此,若Content Provider使用全局的相關(guān)組件,則您必須保證能夠在Application.onCreate()之前初始化該組件,否則您的申請(qǐng)依然可能會(huì)導(dǎo)致應(yīng)用崩潰。
  • 延遲初始化:這是推薦的方法。組件是單例,返回其實(shí)例的函數(shù)持有Context參數(shù)。該單例將在第一次調(diào)用該函數(shù)時(shí)使用此參數(shù)進(jìn)行創(chuàng)建和初始化操作。這需要一些同步機(jī)制才能保證線(xiàn)程的安全。使用此模式的標(biāo)準(zhǔn)Android組件的示例是LocalBroadcastManager
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)

可復(fù)用的Kotlin實(shí)現(xiàn)方式

我們可以通過(guò)封裝邏輯來(lái)懶惰地在SingletonHolder類(lèi)中創(chuàng)建和初始化帶有參數(shù)的單例。

為了使該邏輯的線(xiàn)程安全,我們需要實(shí)現(xiàn)一個(gè)同步算法,它是最有效的算法,同時(shí)也是最難做到的——它就是 雙重檢查鎖定算法(double-checked locking algorithm)

open class SingletonHolder<out T, in A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile private var instance: T? = null

    fun getInstance(arg: A): T {
        val i = instance
        if (i != null) {
            return i
        }

        return synchronized(this) {
            val i2 = instance
            if (i2 != null) {
                i2
            } else {
                val created = creator!!(arg)
                instance = created
                creator = null
                created
            }
        }
    }
}

請(qǐng)注意,為了使算法正常工作,這里需要將@Volatile注解對(duì)instance成員進(jìn)行標(biāo)記。

這可能不是最緊湊或優(yōu)雅的Kotlin代碼,但它是為雙重檢查鎖定算法生成最行之有效的代碼。請(qǐng)信任Kotlin的作者:實(shí)際上,這些代碼正是從Kotlin標(biāo)準(zhǔn)庫(kù)中的 lazy() 函數(shù)的實(shí)現(xiàn)中直接借用的,默認(rèn)情況下它是同步的。它已被修改為允許將參數(shù)傳遞給creator函數(shù)。

有鑒于其相對(duì)的復(fù)雜性,它不是您想要多次編寫(xiě)(或者閱讀)的那種代碼,實(shí)際上其目標(biāo)是,讓您每次必須使用參數(shù)實(shí)現(xiàn)單例時(shí),都能夠重用該SingletonHolder類(lèi)進(jìn)行實(shí)現(xiàn)。

聲明getInstance()函數(shù)的邏輯位置在singleton類(lèi)的伴隨對(duì)象內(nèi)部,這允許通過(guò)簡(jiǎn)單地使用單例類(lèi)名作為限定符來(lái)調(diào)用它,就好像Java中的靜態(tài)方法一樣。Kotlin的伴隨對(duì)象提供的一個(gè)強(qiáng)大功能是它也能夠像任何其他對(duì)象一樣從基類(lèi)繼承,從而實(shí)現(xiàn)與僅靜態(tài)繼承相當(dāng)?shù)墓δ堋?/p>

在這種情況下,我們希望使用SingletonHolder作為單例類(lèi)的伴隨對(duì)象的基類(lèi),以便在單例類(lèi)上重用并自動(dòng)公開(kāi)其getInstance()函數(shù)。

對(duì)于SingletonHolder類(lèi)構(gòu)造方法中的creator參數(shù),它是一個(gè)函數(shù)類(lèi)型,您可以聲明為一個(gè)內(nèi)聯(lián)(inline)的lambda,但更常用的情況是 作為一個(gè)函數(shù)引用的依賴(lài)交給構(gòu)造器,最終其代碼如下所示:

class Manager private constructor(context: Context) {
    init {
        // Init using context argument
    }

    companion object : SingletonHolder<Manager, Context>(::Manager)
}

現(xiàn)在可以使用以下語(yǔ)法調(diào)用單例,并且它的初始化將是lazy并且線(xiàn)程安全的:

Manager.getInstance(context).doStuff()

當(dāng)三方庫(kù)生成單例實(shí)現(xiàn)并且Builder需要參數(shù)時(shí),您也可以使用這種方式,以下是使用Room 數(shù)據(jù)庫(kù)的示例:

@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object : SingletonHolder<UsersDatabase, Context>({
        Room.databaseBuilder(it.applicationContext,
                UsersDatabase::class.java, "Sample.db")
                .build()
    })
}

注意:當(dāng)Builder不需要參數(shù)時(shí),您只需使用lazy的屬性委托:

interface GitHubService {

    companion object {
        val instance: GitHubService by lazy {
            val retrofit = Retrofit.Builder()
                    .baseUrl("https://api.github.com/")
                    .build()
            retrofit.create(GitHubService::class.java)
        }
    }
}

我希望這些代碼能夠給您帶來(lái)一些啟發(fā)。如果您有建議或疑問(wèn),請(qǐng)不要猶豫,在評(píng)論部分開(kāi)始討論,感謝您的閱讀!

--------------------------廣告分割線(xiàn)------------------------------

關(guān)于我

Hello,我是卻把清梅嗅,如果您覺(jué)得文章對(duì)您有價(jià)值,歡迎 ??,也歡迎關(guān)注我的博客或者Github。

如果您覺(jué)得文章還差了那么點(diǎn)東西,也請(qǐng)通過(guò)關(guān)注督促我寫(xiě)出更好的文章——萬(wàn)一哪天我進(jìn)步了呢?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文是在學(xué)習(xí)和使用kotlin時(shí)的一些總結(jié)與體會(huì),一些代碼示例來(lái)自于網(wǎng)絡(luò)或Kotlin官方文檔,持續(xù)更新... 對(duì)...
    竹塵居士閱讀 3,494評(píng)論 0 8
  • 面向?qū)ο缶幊蹋∣OP) 在前面的章節(jié)中,我們學(xué)習(xí)了Kotlin的語(yǔ)言基礎(chǔ)知識(shí)、類(lèi)型系統(tǒng)、集合類(lèi)以及泛型相關(guān)的知識(shí)。...
    Tenderness4閱讀 4,628評(píng)論 1 6
  • 第3章 基本概念 3.1 語(yǔ)法 3.2 關(guān)鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類(lèi)型 5種簡(jiǎn)單數(shù)據(jù)類(lèi)型:Unde...
    RickCole閱讀 5,543評(píng)論 0 21
  • Kotlin的優(yōu)勢(shì) 代碼簡(jiǎn)潔高效、強(qiáng)大的when語(yǔ)法,不用寫(xiě)分號(hào)結(jié)尾,findViewById光榮退休,空指針安全...
    Windy_816閱讀 1,399評(píng)論 1 6
  • 我用你的的指甲 刻了車(chē)窗 又用你的頭發(fā) 織了衣裳。 也把你的眼睛 縫在了我的抱枕上 還把你的雙腳 藏在了我沉重的行...
    一個(gè)博閱讀 203評(píng)論 1 3

友情鏈接更多精彩內(nèi)容