[译]Object的局限性——Kotlin中的带参单例模式

5,014 阅读6分钟

原文: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进行声明,您可以获得安全有效的单例实现。

图:无尽的孤独——单例(译者:作者的描述让我想起了一个悲情的角色,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

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?