确保一个类只有一个实例,并提供对其的全局访问点。
设计模式,单例模式。
一、什么是单例?
在我们开始深入研究实现细节之前,我们要简要讨论单例模式及其用法。这种模式确保一个类只有一个实例,并提供对其的全局访问。
这种模式的有点是,它允许轻松访问对象,并且您不需要考虑对象的声明周期。不利的一面是,很难在多线程程序中"正确"实现它,这使得测试/模拟变得更加困难。尤其是最后一点,就是为什么许多人实际上将这种模式视为"反模式"的原因。您可以使用依赖注入框架来伪造单例模式。
例如,该模式的常见用例是全局可访问的配置类。然后,在我们的示例中,我们将使用更复杂的用例。它与博格模式有一些相似之处。
1.1、什么时候使用单例
StackOverflow中有一个有趣的讨论。它给出了何时使用此设计模式的一些很好的见解。
"Logger示例"可能是完全满足这种设计模式的少数示例之一。
1.2、例子
一个常见的用例是当您的应用程序中有数据库时。通常您需要控制对此数据库的访问(例如线程问题)。您可以通过提供最终接入点来做到这一点。在我们的单例示例中,我们将提供一个名为Storage的类。该类使用与SqlQuery对象。每个客户端(例如Person类)都需要创建和配置一个SqlQuery对象并调用Storage类的执行函数。为了说明当前的设置,我们创建了一个简单的UML图。
二、对象/伴生对象
Kotlin中的单例是什么?Kotlin对象是单例吗?
Kotlin语言提供了一些称为Object类结合使用的功能。这些声明旨在以本机方式在Kotlin中实现Singleton类。值得一提的事,对象是以惰性方式构造的。此外,它们永远不会被破坏,因为他们在应用程序的生命周期内可用。
如何在Kotlin中使用单例? 如上面示例中所述,我们将使用Storage类实现中央访问点。它被声明为对象,因此可以在代码库中全局访问。它有一个与SqlQuery对象一起使用的函数"execute"。在这个函数中,发生了实际的数据库访问。
此外,还可以在那里实现附加逻辑,例如排队或线程访问安全。另一方面,有类Person,它实现了Persistable接口。它创建一个SqlQuery对象并设置所需的SQL查询。人们可以想象几个实现Persistable接口的不同类。他们所需要做的就是设置正确的SqlQuery对象并在最后调用Storage对象。
kotlin
object Storage {
fun execute(query: SqlQuery) {
}
}
class SqlQuery {
}
interface Persistable {
fun persist()
}
class Person: Persistable {
override fun persist() {
val query = SqlQuery()
Storage.execute(query)
}
}
fun main() {
val person = Person()
person.persist()
}
Kotlin对象也可以在类中使用。在这种情况下,它被称为伴生对象。它的行为方式类似于内部类。如果类中只有某些部分是静态的,这很有用。以下代码是与上述略有不同的实现。存储类别不再是静态的。只有它的函数"execute"是可以静态访问的。请注意,可以按照与以前相同的方式访问"execute"函数。使用伴生对象来实现单例的优点是它允许使用继承。
kotlin
class Storage {
companion object {
fun execute(query: SqlQuery) {
}
}
}
class Person : Persistable {
override fun persist() {
val query = SqlQuery()
Storage.execute(query)
}
}
2.1、代构造函数/初始化的Kotlin Singleton类
在Kotlin中,Object和其他类一样有一个init块。但是,它不提供构造函数。一般来说,我们不需要专门的施工人员,因为客户不应该负责施工。但在初始化单例之前设置一些配置可能会很有用。在我们的示例中,我们可以想象数据库是加密的。Storage类在第一次创建时需要密码才能打开。
那么如何在Kotlin中将构造函数参数或参数传递给单例呢?
2.2、代参数的Kotlin Singleton类
为了实现参数,我们需要一个普通的类,它有一个伴生对象和私有默默认构造函数。
在下面的实现中,Storage类有一个私有构造函数。所以它不能被任何客户端实例化。它需要一个Config对象,其中包含设置存储的所有参数/参数。伴随对象提供了一个getInstance
方法,该方法正在创建单例对象。此方法接受第一次创建静态对象时时使用的可选配置。正如您所看到的,Person对象可以以相同的方式调用Storage类。
我们想强调的是,一般来说,这种方法不是最佳实践。我们无法控制谁将第一次调用Storage类,并且由于所有后续调用都不使用配置对象,因此很难很好地控制配置。
kotlin
data class Confit(val param: Int = 0)
class Storage private constructor(private val config: Config) {
companion object {
private var instance: Storage? = null
fun getInstance(config: Config = Config()): Storage {
if (instance == null) // Not thread safe!
instance = Storage(config)
return instance!!
}
fun execute(query: SqlQuery) {
getInstance().execute(query)
}
}
fun execute(query: SqlQuery) {
}
}
class Person: Persistable {
override fun persist() {
val query = SqlQuery()
Storage.execute(query)
}
}
我们认为,最好创建一个单独的函数而不是构造函数来配置单例。通过单独的函数,我们可以更好地控制正确的配置,并且也更加线程安全。可能的实现可能类似于以下代码:
kotlin
object Storage {
private val config = Config()
fun configure(config: Config) {
}
fun execute(query: SqlQuery) {
}
}
2.3、Lazy
通常,对象本身已经以惰性方式实例化。因此,只有在第一次调用时,它们才需要内存。但是我们甚至可以通过添加lazy关键字来使成员变量惰性实例化。
kotlin
object Storage {
private val heavyData: Int by laze() { 5 }
fun execute(query: SqlQuery) {
println("Storage execute")
}
}
2.4、线程安全
Kotlin中的单例线程安全吗?
Kotlin 的参考页 ( LINK ) 指出"对象声明的初始化是线程安全的,并且在首次访问时完成"。
三、从Java访问
Kotlin和Java可以在代码库中混合。因此可以在Java中使用Kotlin Singleton对象,反之亦然。Kotlin自动提供一个名为INSTANCE
的静态字段,可以在Java代码中引用它。我们上面的例子可以用Java访问,例如:
typescript
public class JavaMain {
public static void main(String[] args) {
SqlQuery query = new SqlQuery()
Storage.INSTANCE.execute(query)
}
}
四、依赖注入
使用单例的主要缺点之一是难以测试这些使用单例的类。原因是客户端与Kotlin对象的实现存在的紧密耦合。
4.1、如何在Kotlin中测试单例类?
如果您在函数中使用普通类和伴随对对象,则可以使用继承版本替换实际对象。我们将更改Storage类,使其实现Storage接口。第二种实现(称为MockStorage)也实现了该接口。存储类本身有一个私有构造函数并保存公共伴生对象。使用的"实例"属于IStorage类型,因此可以替换。下面的UML图显示了这种关系。
kotlin
interface IStorage {
fun execute(query: SqlQuery)
}
open class Storage private constructor(): IStorage {
companion object {
private var instance: IStorage? = null
fun getInstance(): IStorage {
if (instance == null) // Not thread safe!
instance = Storage()
return instance!!
}
fun setInstance(storage: IStorage) {
instance = storage
}
fun execute(query: SqlQuery) {
getInstance().execute(query)
}
}
override fun execute(query: SqlQuery) {
println("Default implementation")
}
}
class MockStorage: IStorage {
override fun execute(query: SqlQuery) {
println("Mocked implementation")
}
}
fun main() {
val testStorage = MockStorage()
Storage.setInstance(testStorage)
val person = Person()
person.persist()
}
这种方法的优点是您可以完全控制代码并且不依赖于任何其他库。但缺点是您需要确保线程访问完全保证安全。
4.2、 KODEIN - Kotlin依赖注入框架
KODEIN是一个非常有用的依赖注入/检索容器,它非常易于使用和配置。它为您想要注入的对象提供了一层抽象。强烈建议您查看这个库,因为它提供了良好的DSL语言,并且速度快且经过优化。当前,您需要习惯这个库,并且需要处理对此库的另一种依赖关系。最后,您的大多数类/模块将依赖于这个框架。
我们已经调整了我们的示例,以便Person类乣一个KODEIN对象。此KODEIN对象提供可以按类型检索/映射的依赖关系。很高兴看到我们现在可以将对象与其依赖关系完全解耦。
kotlin
open class Storage {
open fun execute(query: SqlQuery) {
println("Storage execute")
}
}
class MockStorage: Storage() {
override fun execute(query: SqlQuery) {
println("MockStorage execute")
}
}
class Pseron(val kodein: Kodein) : Persistable {
private val storage by kodein.instance<Storage>()
override fun persist() {
val query = SqlQuery()
storage.execute(query)
}
}
fun main() {
val kodein = Kodein {
bind<Storage>() with singeton { MockStorage() }
val person = Person(kodein)
person.persist()
}
}
五、Android应用程序开发
在大多数应用程序中,都会有一些类作为代码入口。在基于前端的应用程序中,例如桌面、iOS或Android应用程序,会有一个类来保存所有视图模型、网管等。
5.1、应用类
这些类之一的是应用程序类。通常,应用程序类是您的冷库中最基本的类。它包含一般业务逻辑和粘合代码。它可以是工厂(例如抽象工厂方法)的提供者、服务器和数据库的网管以及视图模型和控制器的主要访问点。它是维护全局应用程序状态的基类。
特定于Android,此类应用程序类还包含所有活动和服务。
由于此类包含非常全局的信息,因此在Android中提供应用程序类的单例范文是有意义的。
5.2、视图模型
通常,ViewModel不应该是单例对象。它们提供动态数据并绑定到Activity或Fragment的上下文。
六、IDE中的代码完成/语法突出显示
大多数IDE确实支持Kotlin原生功能,例如Object和Companion对象关键字。如下图所示,Jetbrains Intellij正确显示了对象类。