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

原文:Kotlin singletons with argument ——object has its limits
做者:Christophe Beyls
译者:却把清梅嗅html

Kotlin中,单例模式被用于替换该编程语言中不存在的static成员和字段。 你经过简单地声明object以建立一个单例:java

object SomeSingleton
复制代码

与类不一样,object 不容许有任何构造函数,若是有须要,能够经过使用 init 代码块进行初始化的行为:android

object SomeSingleton {
    init {
        println("init complete")
    }
}
复制代码

这样object将被实例化,而且在初次执行时,其init代码块将以线程安全的方式懒惰地执行。 为了这样的效果,Kotlin对象实际上依赖于Java静态代码块 。上述Kotlin的 object 将被编译为如下等效的Java代码:git

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进行声明,您能够得到安全有效的单例实现。github

图:无尽的孤独——单例(译者:做者的描述让我想起了一个悲情的角色,Maiev Shadowsong算法

传递一个参数

可是,若是初始化的代码须要一些额外的参数呢?你不能将任何参数传递给它,由于Kotlinobject关键字不容许存在任何构造函数。数据库

有些状况下,将参数传递给单例初始化代码块是被推荐的方式。 替代方法要求单例类须要知道某些可以获取该参数的外部组件(component),但违反了关注点分离的原则而且使得代码不可被复用。编程

为了缓解这个问题,该外部组件能够是 依赖注入系统。这的确是一个具备可行性的解决方案,但您并不老是但愿使用这种类型的库——而且,在某些状况下您也没法使用它,就像在接下来的Android示例中我将会所提到的。api

在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

若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?

相关文章
相关标签/搜索