Scala 3 — 書籍

OOP 建模

語言

本章節提供使用 Scala 3 中的物件導向程式設計 (OOP) 進行網域建模的簡介。

簡介

Scala 提供物件導向設計所需的所有工具

  • 特質讓您可以指定 (抽象的) 介面,以及具體的實作。
  • 混合組成提供工具,讓您可以從較小的部分組成元件。
  • 類別可以實作特質所指定的介面。
  • 類別的執行個體可以擁有自己的私有狀態。
  • 子類型化讓您可以在需要超類別執行個體的地方使用一個類別的執行個體。
  • 存取修飾子讓您可以控制類別的哪些成員可以被程式碼的哪些部分存取。

特質

與支援 OOP 的其他語言(例如 Java)不同,Scala 中分解的主要工具不是類別,而是特質。它們可以用來描述抽象介面,例如

trait Showable {
  def show: String
}
trait Showable:
  def show: String

也可以包含具體的實作

trait Showable {
  def show: String
  def showHtml = "<p>" + show + "</p>"
}
trait Showable:
  def show: String
  def showHtml = "<p>" + show + "</p>"

您可以看到我們定義方法 showHtml 根據抽象方法 show

Odersky 和 Zenger 提出服務導向元件模型和觀點

  • 抽象成員視為必需服務:它們仍需要由子類別實作。
  • 具體成員視為提供服務:它們提供給子類別。

我們已經可以在 Showable 的範例中看到這一點:定義一個擴充 Showable 的類別 Document,我們仍然必須定義 show,但會提供 showHtml

class Document(text: String) extends Showable {
  def show = text
}
class Document(text: String) extends Showable:
  def show = text

抽象成員

抽象方法並非特質中唯一可以保留為抽象的內容。特質可以包含

  • 抽象方法 (def m(): T)
  • 抽象值定義 (val x: T)
  • 抽象類型成員 (type T),可能具有界限 (type T <: S)
  • 抽象給定 (given t: T) 僅限 Scala 3

上述每個功能都可用於指定特質實作者的某種形式需求。

混合組成

Scala 不僅可以包含抽象和具體定義的特質,還提供一種強大的方式來組成多個特質:一種通常稱為混合組成的功能。

讓我們假設以下兩個(可能獨立定義)的特質

trait GreetingService {
  def translate(text: String): String
  def sayHello = translate("Hello")
}

trait TranslationService {
  def translate(text: String): String = "..."
}
trait GreetingService:
  def translate(text: String): String
  def sayHello = translate("Hello")

trait TranslationService:
  def translate(text: String): String = "..."

若要組成兩個服務,我們可以簡單地建立一個新的特質來擴充它們

trait ComposedService extends GreetingService with TranslationService
trait ComposedService extends GreetingService, TranslationService

一個特質中的抽象成員(例如 GreetingService 中的 translate)會自動與另一個特質中的具體成員相匹配。這不僅適用於此範例中的方法,也適用於上述所有其他抽象成員(也就是類型、值定義等)。

類別

特質非常適合模組化元件和描述介面(必需和提供的)。但在某個時間點,我們會想要建立它們的執行個體。在 Scala 中設計軟體時,通常只考慮在繼承模型的葉節點使用類別會很有幫助

特質 T1T2T3
組合特質 S1 擴充 T1 使用 T2S2 擴充 T2 使用 T3
類別 C 擴充 S1 使用 T3
實例 new C()
特質 T1T2T3
組合特質 S1 擴充 T1, T2S2 擴充 T2, T3
類別 C 擴充 S1, T3
實例 C()

在 Scala 3 中更是如此,其中特質現在也可以使用參數,進一步消除了類別的需求。

定義類別

與特質一樣,類別可以擴充多個特質(但只有一個超級類別)

class MyService(name: String) extends ComposedService with Showable {
  def show = s"$name says $sayHello"
}
class MyService(name: String) extends ComposedService, Showable:
  def show = s"$name says $sayHello"

子類型化

我們可以透過以下方式建立 MyService 的實例

val s1: MyService = new MyService("Service 1")
val s1: MyService = MyService("Service 1")

透過子類型化的方式,我們的實例 s1 可以用於預期任何擴充特質的任何地方

val s2: GreetingService = s1
val s3: TranslationService = s1
val s4: Showable = s1
// ... and so on ...

擴充規劃

如前所述,可以擴充另一個類別

class Person(name: String)
class SoftwareDeveloper(name: String, favoriteLang: String)
  extends Person(name)

然而,由於特質被設計為分解的主要方式,因此不建議從另一個檔案擴充在一個檔案中定義的類別。

開放類別 僅限 Scala 3

在 Scala 3 中,限制擴充其他檔案中的非抽象類別。為了允許這樣做,基本類別需要標記為 open

open class Person(name: String)

使用 open 標記類別是 Scala 3 的新功能。必須明確將類別標記為開放,可以避免物件導向設計中的許多常見陷阱。特別是,它要求程式庫設計人員明確規劃擴充,並說明標記為開放的類別與額外擴充合約的實例文件。

實例和私有可變狀態

與支援 OOP 的其他語言一樣,Scala 中的特質和類別可以定義可變欄位

class Counter {
  // can only be observed by the method `count`
  private var currentCount = 0

  def tick(): Unit = currentCount += 1
  def count: Int = currentCount
}
class Counter:
  // can only be observed by the method `count`
  private var currentCount = 0

  def tick(): Unit = currentCount += 1
  def count: Int = currentCount

類別 Counter 的每個實例都有其自己的私有狀態,只能透過方法 count 觀察,如下面的互動所示

val c1 = new Counter()
c1.count // 0
c1.tick()
c1.tick()
c1.count // 2
val c1 = Counter()
c1.count // 0
c1.tick()
c1.tick()
c1.count // 2

存取修飾詞

預設情況下,Scala 中的所有成員定義都是公開可見的。若要隱藏實作細節,可以定義成員(方法、欄位、類型等)為 privateprotected。這樣就可以控制存取或覆寫方式。私有成員僅對類別/特質本身及其伴隨物件可見。受保護的成員也對類別的子類別可見。

進階範例:服務導向設計

以下,我們說明 Scala 的一些進階功能,並展示如何使用這些功能來建構較大的軟體元件。這些範例改編自 Martin Odersky 和 Matthias Zenger 的論文 “可擴充元件抽象”。如果您不了解範例中的所有細節,不用擔心;其主要目的是展示如何使用多個類型功能來建構較大的元件。

我們的目標是定義一個軟體元件,其中包含一個類型家族,可以在元件的實作中進一步精煉。具體來說,下列程式碼將元件 SubjectObserver 定義為一個特質,其中包含兩個抽象類型成員,S(用於主體)和 O(用於觀察者)

trait SubjectObserver {

  type S <: Subject
  type O <: Observer

  trait Subject { self: S =>
    private var observers: List[O] = List()
    def subscribe(obs: O): Unit = {
      observers = obs :: observers
    }
    def publish() = {
      for ( obs <- observers ) obs.notify(this)
    }
  }

  trait Observer {
    def notify(sub: S): Unit
  }
}
trait SubjectObserver:

  type S <: Subject
  type O <: Observer

  trait Subject:
    self: S =>
      private var observers: List[O] = List()
      def subscribe(obs: O): Unit =
        observers = obs :: observers
      def publish() =
        for obs <- observers do obs.notify(this)

  trait Observer:
    def notify(sub: S): Unit

有幾件事需要說明。

抽象類型成員

宣告 type S <: Subject 表示在特質 SubjectObserver 中,我們可以參照某個未知(也就是抽象)類型,我們稱之為 S。不過,這個類型並非完全未知:我們至少知道它是特質 Subject某個子類型。所有延伸 SubjectObserver 的特質和類別都可以自由選擇 S 的任何類型,只要所選類型是 Subject 的子類型即可。宣告中的 <: Subject 部分也稱為S 的上限

巢狀特質

特質 SubjectObserver ,我們定義了另外兩個特質。讓我們從特質 Observer 開始,它只定義一個抽象方法 notify,其參數類型為 S。正如我們稍後將看到的,參數類型為 S 而不是 Subject 非常重要。

第二個特質 Subject 定義了一個私有欄位 observers,用於儲存所有訂閱這個特定主體的觀察者。訂閱主體只會將物件儲存在這個清單中。同樣地,參數 obs 的類型是 O,而不是 Observer

自訂類型註解

最後,你可能會好奇特徵 Subject 中的 self: S => 是什麼意思。這稱為自訂類型註解。它要求 Subject 的子類型也必須是 S 的子類型。這是必要的,才能使用 this 作為引數呼叫 obs.notify,因為它需要一個 S 類型的值。如果 S具體類型,則自訂類型註解可以替換為 trait Subject extends S

實作元件

我們現在可以實作上述元件,並將抽象類型成員定義為具體類型

object SensorReader extends SubjectObserver {
  type S = Sensor
  type O = Display

  class Sensor(val label: String) extends Subject {
    private var currentValue = 0.0
    def value = currentValue
    def changeValue(v: Double) = {
      currentValue = v
      publish()
    }
  }

  class Display extends Observer {
    def notify(sub: Sensor) =
      println(s"${sub.label} has value ${sub.value}")
  }
}
object SensorReader extends SubjectObserver:
  type S = Sensor
  type O = Display

  class Sensor(val label: String) extends Subject:
    private var currentValue = 0.0
    def value = currentValue
    def changeValue(v: Double) =
      currentValue = v
      publish()

  class Display extends Observer:
    def notify(sub: Sensor) =
      println(s"${sub.label} has value ${sub.value}")

具體來說,我們定義一個擴充 SubjectObserver單例物件 SensorReader。在 SensorReader 的實作中,我們表示類型 S 現在定義為類型 Sensor,而類型 O 定義為等於類型 Display。兩個 SensorDisplay 都定義為 SensorReader 中的巢狀類別,分別實作特徵 SubjectObserver

此外,這段程式碼是服務導向設計的範例,也突出了物件導向程式設計的許多面向

  • 類別 Sensor 導入自己的私有狀態 (currentValue),並將狀態修改封裝在方法 changeValue 的背後。
  • changeValue 的實作使用擴充特徵中定義的方法 publish
  • 類別 Display 擴充特徵 Observer,並實作遺失的方法 notify

重要的是指出,notify 的實作只能安全地存取 sub 的標籤和值,因為我們最初宣告引數的類型為 S

使用組件

最後,以下程式碼說明如何使用我們的 SensorReader 組件

import SensorReader._

// setting up a network
val s1 = new Sensor("sensor1")
val s2 = new Sensor("sensor2")
val d1 = new Display()
val d2 = new Display()
s1.subscribe(d1)
s1.subscribe(d2)
s2.subscribe(d1)

// propagating updates through the network
s1.changeValue(2)
s2.changeValue(3)

// prints:
// sensor1 has value 2.0
// sensor1 has value 2.0
// sensor2 has value 3.0

import SensorReader.*

// setting up a network
val s1 = Sensor("sensor1")
val s2 = Sensor("sensor2")
val d1 = Display()
val d2 = Display()
s1.subscribe(d1)
s1.subscribe(d2)
s2.subscribe(d1)

// propagating updates through the network
s1.changeValue(2)
s2.changeValue(3)

// prints:
// sensor1 has value 2.0
// sensor1 has value 2.0
// sensor2 has value 3.0

在下一節,我們將展示如何使用我們掌握的所有物件導向程式設計工具,以函式風格設計程式。

此頁面的貢獻者