Kotlin设计模式之Singleton

确保一个类只有一个实例,并提供对其的全局访问点。
设计模式,单例模式。

一、什么是单例?

在我们开始深入研究实现细节之前,我们要简要讨论单例模式及其用法。这种模式确保一个类只有一个实例,并提供对其的全局访问。

这种模式的有点是,它允许轻松访问对象,并且您不需要考虑对象的声明周期。不利的一面是,很难在多线程程序中"正确"实现它,这使得测试/模拟变得更加困难。尤其是最后一点,就是为什么许多人实际上将这种模式视为"反模式"的原因。您可以使用依赖注入框架来伪造单例模式。

例如,该模式的常见用例是全局可访问的配置类。然后,在我们的示例中,我们将使用更复杂的用例。它与博格模式有一些相似之处。

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正确显示了对象类。

相关推荐
CocoaAndYy3 分钟前
设计模式-适配器模式
设计模式·适配器模式
刷帅耍帅3 分钟前
设计模式-适配器模式
设计模式·适配器模式
拥有一颗学徒的心2 小时前
设计模式——命令模式
设计模式·命令模式
拉里小猪的迷弟5 小时前
设计模式-结构型-常用:代理模式、桥接模式、装饰者模式、适配器模式
设计模式·代理模式·桥接模式·适配器模式·装饰器模式
CocoaAndYy7 小时前
设计模式-单例模式
单例模式·设计模式
bobostudio19959 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
ok!ko13 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
人间有清欢14 小时前
十、kotlin的协程
kotlin
吾爱星辰14 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer14 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin