本章介绍了在 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 中设计软件时,通常只考虑在继承模型的叶子节点使用类会很有帮助
特质 | T1 、T2 、T3 |
组合特征 | S1 extends T1 with T2 、S2 extends T2 with T3 |
类 | C 用 T3 扩展 S1 |
实例 | new C() |
特质 | T1 、T2 、T3 |
组合特征 | S1 扩展 T1、T2 ,S2 扩展 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 的一项新特性。必须明确地将类标记为开放类可以避免 OO 设计中的许多常见陷阱。特别是,它要求库设计人员明确地规划扩展,并例如使用额外的扩展契约记录标记为开放类的类。
实例和私有可变状态
与支持 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 中的所有成员定义都是公开可见的。为了隐藏实现细节,可以将成员(方法、字段、类型等)定义为 private
或 protected
。这样,你可以控制如何访问或覆盖它们。私有成员仅对类/特征本身及其伴随对象可见。受保护的成员对类的子类也可见。
高级示例:面向服务的架构
在下面,我们展示了 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
。
自类型注释
最后,你可能想知道 self: S =>
在特征 Subject
中的含义。这称为自类型注释。它要求 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
。 Sensor
和 Display
都被定义为 SensorReader
中的嵌套类,分别实现了特征 Subject
和 Observer
。
此外,作为面向服务设计的示例,此代码还突出了面向对象编程的许多方面
- 类
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
在掌握了所有面向对象编程实用工具后,我们将在下一部分演示如何以函数式风格设计程序。