跳转至内容

Scala/特质

来自维基教科书,开放的书籍,开放的世界

在 Java 和许多其他语言中,有一个接口的概念:接口描述了实现类必须具有的方法和属性集。

例如,假设您正在创建类来表示不同类型的人。您决定所有人都必须可以通过电子邮件联系,并拥有姓名和性别。您可以定义一个名为 Person 的接口

interface Person {
    String name;
    Gender gender;
    void sendEmail(String subject, String body);
}

创建类时,您可以实现 Person 接口。

class Student implements Person {
    public String name;
    public Gender gender;
    public void sendEmail(String subject, String body) {
        // ...
    }
}

为什么要这样做?很简单:在其他地方,您可以将此 Person 接口用作类型。您可以创建一个以 Person 类型作为参数的方法。这基本上是在表达一个契约:您不关心对象的,只关心它是否满足接口的要求。

以下是 Scala 与 Java 的一个主要区别:没有接口。没有 interface 关键字。

是的,即使 Scala 主要是一种 JVM 语言,并且通常被吹捧为“更好的 Java”,它也没有接口。

Scala 有更好的东西:特质。

什么是特质?

[编辑 | 编辑源代码]

要理解特质,您需要理解为什么 Java 有接口。其他语言,如 Python 和 C++,不需要接口,因为它们具有多重继承。但多重继承会受到菱形问题的影响:如果您正在定义一个从两个不同类继承了具有相同类型签名的两个方法的类,该怎么办?应该使用哪一个?为了避免多重继承带来的复杂性和错误,Java 的设计者决定不实现多重继承,而是使用带有接口的单继承来弥补不足,并使类型系统稍微灵活一些。

Scala 通过为您提供特质,在完全多重继承和 Java 的单继承与接口模型之间提供了一个折衷方案。

特质使您能够像 Java 一样重新创建接口,但也允许您更进一步。

特质作为接口

[编辑 | 编辑源代码]

在最基本的层面上,特质允许您重新创建 Java 的接口和实现模型。以下是如何使用特质在 Scala 中创建上面的示例。

trait Person {
  var name: String
  var gender: Gender
  def sendEmail(subject: String, body: String): Unit
}

(Unit 是 Scala 的 void 版本,请记住。)

您现在可以实现 Person 特质

class Student extends Person {
  var name: String
  var gender: Gender
  def sendEmail(subject: String, body: String): Unit = {
    // ...
  }
}

也实现它!

[编辑 | 编辑源代码]

您也可以使用特质来提供接口的默认实现。尽管“带有实现的接口”这个想法听起来有点矛盾:如果您正在定义实现,您怎么能说您只是在定义接口呢?答案很简单:特质遵循 Java 中接口的语义,但通过提供默认实现来扩展它们。

trait Quacking {
  def quack() = {
    println("Quack quack quack!")
  }
}

如果需要,类可以覆盖 quack 方法,但也可以不覆盖。我们可以在下一节中看到 Quacking 特质的使用。

单例也可以使用特质

[编辑 | 编辑源代码]

Scala 在实例化对象或使用单例时可以使用特质。使用特质实例化类基本上会创建一个一次性的单例。这使您能够拥有与 Ruby 的模块混入非常相似的东西,但具有静态类型检查。

class Duck {
  val version = "ACME Inc. Generic Duck v1.0"
}
val aDuck = new Duck with Quacking
aDuck.quack()

这里我们创建了一个具有 Duck 类的对象,但也“混合了”Quacking 特质。它具有两种类型,因此可以传递给以 'Quacking' 作为类型的方法。

更实用的重申

[编辑 | 编辑源代码]

如果您是 Java 或 C# 程序员,您应该能够想到很多可能需要接口的情况。在这些情况下,您可能可以使用特质。

但是,您可能想使用这种混入模式的示例是什么呢?

一个简单的例子:为测试创建伪对象。如果您正在创建单元测试,有时您想测试一个对象的接口,该对象的作用是调用外部服务,如数据库、Web 服务等。您仍然想知道特定类的 API 是否有效,而无需将昂贵的计算发送到您的数据库或将昂贵的交易发送到您的信用卡支付处理器。传统上,这个问题的答案是使用模拟和存根模式,可能是像 JMock 这样的模拟库。

但 Scala 为此提供了一个更简单的解决方案。

想象一个调用名为 FooApi 的 Web 服务的类。

class FooApi {
  def getFromApi() = http(blargh)
}

getFromApi 使用 HTTP 从互联网上的远程服务器获取一些 XML。

测试 getFromApi 方法很昂贵,因为它会进行跨互联网的调用。您的单元测试不是为了测试互联网是否正常工作或服务器是否正常工作。我们可能应该让它能够在不需要互联网连接的情况下测试它。

在我们的测试文件中,我们可以定义一个特质来做到这一点。

trait FooMocker extends FooApi {
  override def getFromApi() = <response><from /><the /><api /></response>
}

您可以用您期望从 API 返回的任何内容替换 XML 响应。

然后,您可以编写测试(示例代码使用 Specs 测试库,但您可以使用任何您喜欢的单元测试库)

object FooApiTest extends Specification {
  "FooApi" should {
    "return from the API" in {
      var foo = new FooApi with FooMocker
      foo.getFromApi() mustBe <response><from /><the /><api /></response>
    }
  }
}

在这里,使用特质,“混入”模式使我们能够用模拟替换代码中的黑盒方法。特质可以用于各种功能,并可以减少代码中的重复。

华夏公益教科书