1. 概述

在本教程中,我们将深入探讨 Scala 中的伴生对象(Companion Object),包括其定义、作用以及如何利用它来实现工厂模式和提取器(Extractor)。

2. 类与对象

假设我们有一个 Task 类:

class Task(val description: String) {
  private var _status: String = "pending"

  def status(): String = _status
}

这是一个基础的 Scala 类。如果我们想创建一个 Task 实例,可以通过调用其主构造函数实现:

val task = new Task("do something")
assert(task.description == "do something")

现在我们给这个类增加一个辅助构造函数,接受 status 参数:

class Task(val description: String) {
  private var _status: String = "pending"

  def this(description: String, status: String) = {
    this(description)
    this._status = status
  }

  def status(): String = _status
}

这样就可以创建带有指定状态的 Task 实例了:

val task = new Task("do something", "started")
assert(task.status == "started")

✅ 我们可以继续添加更多的辅助构造函数,但存在以下两个问题:

  • 每次创建对象都要使用 new 关键字,虽然不是大问题,但能省则省;
  • 所有辅助构造函数都必须首先调用已存在的构造函数,容易导致构造函数“ telescoping(链式调用)”问题;

⚠️ 如果我们想提供更灵活的对象创建方式,比如通过工厂方法来构建对象,是不是会更好?工厂方法相比构造函数有很多优势。

但由于 Scala 没有 static 关键字,因此无法像 Java 那样定义静态工厂方法。不过别急,Scala 提供了另一种机制:单例对象(Singleton Object)

3. 伴生对象

当一个单例对象与某个类同名定义在同一文件中时,该对象就被称为这个类的伴生对象(Companion Object)

让我们为前面定义的 Task 类创建一个伴生对象:

class Task(val description: String) {
  private var _status: String = "pending"
  def status(): String = _status
}

object Task {
  def apply(description: String): Task = new Task(description)
}

在这个伴生对象中,我们定义了一个 apply 方法。在 Scala 中,apply 是一个特殊的方法,允许我们在不显式写出方法名的情况下调用它。

有了这个方法后,我们可以这样创建对象:

val task = Task("do something")
assert(task.description == "do something")

这实际上等价于:

val task = Task.apply("do something")
assert(task.description == "do something")

✅ 更进一步地,我们可以重载 apply 方法,提供多个不同参数的工厂方法:

class Task(val description: String) {
  private var _status: String = "pending"

  def status(): String = _status
}

object Task {
  def apply(description: String): Task = new Task(description)

  def apply(description: String, status: String): Task = {
    val task = new Task(description)
    task._status = status
    task
  }
}

然后就可以根据需要选择不同的创建方式:

val task = Task("do something", "started")
assert(task.status == "started")

⚠️ 注意:在这个例子中,伴生对象访问了类中的私有字段 _status。这正是伴生对象的一个重要特性:类和它的伴生对象可以互相访问彼此的私有成员

此外,伴生对象还可以实现更复杂的 apply 方法,用于创建整个类层次结构中的不同类型实例。例如父类的伴生对象可以根据参数决定返回哪个子类型,对外隐藏具体逻辑,提供统一接口。

❌ 这一点是辅助构造函数做不到的,因为它们只能返回当前类的实例。

4. 提取器(Extractors)

除了 apply 方法,我们还可以在伴生对象中定义 unapply 方法,它作为提取器,可以从对象中提取信息。

举个例子,我们定义一个提取器,返回 Task 的描述和状态:

object Task {
  def unapply(task: Task): Tuple2[String, String] = (task.description, task.status())
}

val task = Task("do something")
val (description, status) = Task.unapply(task)
assert(description == "do something")
assert(status == "pending")

💡 unapply 可以返回任意类型的数据,使得我们可以写出强大的模式匹配表达式。

5. 总结

在本文中,我们学习了 Scala 中的伴生对象,并了解了如何使用它来创建灵活的工厂方法。

我们还看到了类和它的伴生对象之间可以互相访问私有成员,以及如何通过 unapply 实现提取器功能。

✅ 伴生对象不仅让代码更简洁优雅,还能帮助我们构建更灵活、更具扩展性的 API。

如需查看完整代码示例,请访问 GitHub 项目地址


原始标题:Building DSLs in Kotlin