单例模式
单例模式用于确保一个类在整个应用中仅有一个实例。它在使用它的应用用引入了一个全局状态。
一个单例对象可以由不同的策略来初始化——懒初始化或热(eager)初始化。这都基于期望的用途,以及对象需要被初始化的时机,等等。
类图
单例是另一种由 Scala 语言语法所支持的设计模式。我们通过object
关键字来实现它,并没有必要提供一个类图,因此我们下一小节直接进入一个实例。
实例
这个实例的目的是展示如果在 Scala 中创建一个单例对象,以及理解在 Scala 中对象是何时被创建的。我们将会看到一个名为StringUtils
的类,提供了一些字符串相关的工具方法:
object StringUtils{
def countNumberOfSpecs(text:String):Int = text.split("\\+s").length -1
}
这个类的用法也很直接。Scala 会管理对象的创建过程、线程安全,等等:
object UtilsExample extends App{
val sentence = "Hello there! I am a utils example."
println(s"The number of spaces in '$sentence' is:
${StringUtils.countNumberOfSpaces(sentence)}")
}
尽管StringUtils
对象是一个单例实例,上面这个例子看起来仍然很清晰,类似于带有静态方法的类。这也就是在 Scala 中定义静态方法的方式。给一个单例类添加状态或许会更有意思。下面的例子展示了这种方式:
object AppRegistry{
println("Registry initialization block called.")
private val users: Map[String, String] = TrieMap.empty
def addUser(id: String, name: String): Unit = { users.put(id, name) }
def removeUser(id: String): Unit = { users.remove(id) }
def isUserRegistered(id: String): Boolean = users.contains(id)
def getAllUserNames(): List[String] = users.map(_._2).toList
}
AppRegistry
包含一个使用应用的当前用户所构成的并发 Map。这是我们的全局状态,同时提供了一些方法来支持用户操作它。同时我们有一个打印语句,它会在这个单例对象被创建时执行。我们可以在下面的应用中使用这个注册表:
object AppRegistryExample extends App{
System.out.println("Sleeping for 5 seconds.")
Thread.sleep(5000)
System.out.println("I woke up.")
AppRegistry.addUser("1", "Ivan")
AppRegistry.addUser("2", "John")
AppRegistry.addUser("3", "Martin")
System.out.println(s"Is user with ID=1 registered? ${AppRegistry.isUserRegistered("1")}")
System.out.println("Removing ID=2")
AppRegistry.removeUser("2")
System.out.println(s"Is user with ID=2 registered? ${AppRegistry.isUserRegistered("2")}")
System.out.println(s"All users registered are: ${AppRegistry.getAllUserNames().mkString(",")}")
}
从运行这段代码得到的输出你会发现,在 “I woke up” 之后会打印单例对象被初始化的信息,因为在 Scala 中,单例会被懒初始化。
优点
在 Scala 中,单例模式与静态方法的实现方式一样。这也是单例可以有效的用于创建没有状态的工具类的原因。Scala 中的单例也可以用于创建 ADT。
另一个在 Scala 中严格有效的事情是,单例会被以线程安全的方式创建,因此无需一些额外的操作来确保。
缺点
单例模式实际上通常被称为是“anti-pattern”(反面模式、反面教材)。很多人说全局状态不能以单例的方式来实现。也有人说如果你不得不使用单例模式,则你需要尝试重构你的代码。虽然这在有些场景中是对的,但有些场景很适合使用单例模式。通常首要的原则是:如果你能避免,那就避免。
对于 Scala 的单例需要特别指出的一点是它可以真的仅拥有一个实例。虽然这实际上是该模式的定义,在其他语言中,我们可以拥有一个预定义的数量而非仅仅一个单例对象,我们可以使用自定义的逻辑进行控制。
有一点对 Scala 没有影响,不过还是值得一提。当应用中的单例被延迟初始化时,为了能够提供线程安全,需要基于一种加锁机制,比如前面提到过的double-checked locking。访问应用中的单例时,无论是在 Scala 中还是别的语言,同样需要以线程安全的方式来完成,或者由单例内部来处理这个问题。