跳轉至內容

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>
    }
  }
}

在這裡,使用特質,“混入”模式使我們能夠用模擬替換程式碼中的黑盒方法。特質可以用於各種功能,並可以減少程式碼中的重複。

華夏公益教科書