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

卻把清梅嗅發表於2019-02-12

原文:Kotlin singletons with argument ——object has its limits
作者:Christophe Beyls
譯者:卻把清梅嗅

Kotlin中,單例模式被用於替換該程式語言中不存在的static成員和欄位。 你通過簡單地宣告object以建立一個單例:

object SomeSingleton
複製程式碼

與類不同,object 不允許有任何建構函式,如果有需要,可以通過使用 init 程式碼塊進行初始化的行為:

object SomeSingleton {
    init {
        println("init complete")
    }
}
複製程式碼

這樣object將被例項化,並且在初次執行時,其init程式碼塊將以執行緒安全的方式懶惰地執行。 為了這樣的效果,Kotlin物件實際上依賴於Java靜態程式碼塊 。上述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上實現單例的首選方式,因為它可以線上程安全的情況下懶惰地進行初始化,同時不必依賴複雜的雙重檢查加鎖(double-checked locking)等加鎖演算法。 通過在Kotlin中簡單地使用object進行宣告,您可以獲得安全有效的單例實現。

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

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

傳遞一個引數

但是,如果初始化的程式碼需要一些額外的引數呢?你不能將任何引數傳遞給它,因為Kotlinobject關鍵字不允許存在任何建構函式。

有些情況下,將引數傳遞給單例初始化程式碼塊是被推薦的方式。 替代方法要求單例類需要知道某些能夠獲取該引數的外部元件(component),但違反了關注點分離的原則並且使得程式碼不可被複用。

為了緩解這個問題,該外部元件可以是 依賴注入系統。這的確是一個具有可行性的解決方案,但您並不總是希望使用這種型別的庫——並且,在某些情況下您也無法使用它,就像在接下來的Android示例中我將會所提到的。

在Kotlin中,您必須通過不同的方式去管理單例的另一種情況是,單例的具體實現是由外部工具或庫(比如RetrofitRoom等等)生成的,它們的例項是通過使用Builder模式或Factory模式來獲取的——在這種情況下,您通常將單例通過interfaceabstract class進行宣告,而不是object

一個Android示例

Android平臺上,您經常需要將Context例項作為引數傳遞給單例元件的初始化程式碼塊中,以便它們可以獲取 檔案路徑讀取系統設定開啟Service等等,但您還希望避免對其進行靜態引用(即使是Application的靜態引用在技術上是安全的)。 有兩種方法可以實現這一目標:

  • 提前初始化:在執行任何(幾乎)其他程式碼之前,通過在Application.onCreate()中呼叫初始化所有元件,此時Application是可用的——這個簡單的解決方案的主要缺點是它是通過阻塞主執行緒的方式來減慢應用程式啟動,並初始化了所有元件,甚至包括那些不會立即使用的元件。另一個鮮為人知的問題是,在呼叫此方法之前,Content Provider也許已經被例項化了(正如文件中所提到的),因此,若Content Provider使用全域性的相關元件,則您必須保證能夠在Application.onCreate()之前初始化該元件,否則您的申請依然可能會導致應用崩潰。
  • 延遲初始化:這是推薦的方法。元件是單例,返回其例項的函式持有Context引數。該單例將在第一次呼叫該函式時使用此引數進行建立和初始化操作。這需要一些同步機制才能保證執行緒的安全。使用此模式的標準Android元件的示例是LocalBroadcastManager
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
複製程式碼

可複用的Kotlin實現方式

我們可以通過封裝邏輯來懶惰地在SingletonHolder類中建立和初始化帶有引數的單例。

為了使該邏輯的執行緒安全,我們需要實現一個同步演算法,它是最有效的演算法,同時也是最難做到的——它就是 雙重檢查鎖定演算法(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
            }
        }
    }
}
複製程式碼

請注意,為了使演算法正常工作,這裡需要將@Volatile註解對instance成員進行標記。

這可能不是最緊湊或優雅的Kotlin程式碼,但它是為雙重檢查鎖定演算法生成最行之有效的程式碼。請信任Kotlin的作者:實際上,這些程式碼正是從Kotlin標準庫中的 lazy() 函式的實現中直接借用的,預設情況下它是同步的。它已被修改為允許將引數傳遞給creator函式。

有鑑於其相對的複雜性,它不是您想要多次編寫(或者閱讀)的那種程式碼,實際上其目標是,讓您每次必須使用引數實現單例時,都能夠重用該SingletonHolder類進行實現。

宣告getInstance()函式的邏輯位置在singleton類的伴隨物件內部,這允許通過簡單地使用單例類名作為限定符來呼叫它,就好像Java中的靜態方法一樣。Kotlin的伴隨物件提供的一個強大功能是它也能夠像任何其他物件一樣從基類繼承,從而實現與僅靜態繼承相當的功能。

在這種情況下,我們希望使用SingletonHolder作為單例類的伴隨物件的基類,以便在單例類上重用並自動公開其getInstance()函式。

對於SingletonHolder類構造方法中的creator引數,它是一個函式型別,您可以宣告為一個內聯(inline)的lambda,但更常用的情況是 作為一個函式引用的依賴交給構造器,最終其程式碼如下所示:

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

    companion object : SingletonHolder<Manager, Context>(::Manager)
}
複製程式碼

現在可以使用以下語法呼叫單例,並且它的初始化將是lazy並且執行緒安全的:

Manager.getInstance(context).doStuff()
複製程式碼

當三方庫生成單例實現並且Builder需要引數時,您也可以使用這種方式,以下是使用Room 資料庫的示例:

@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()
    })
}
複製程式碼

注意:當Builder不需要引數時,您只需使用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)
        }
    }
}
複製程式碼

我希望這些程式碼能夠給您帶來一些啟發。如果您有建議或疑問,請不要猶豫,在評論部分開始討論,感謝您的閱讀!

--------------------------廣告分割線------------------------------

關於我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github

如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章