Pharo By Example - 第十一章 Traits: 可重用的类片段

标签: pharo ; smalltalk ;


尽管 Pharo 提供了类之间的单继承,但它支持一种称为 Trait 的机制,用于在不相关的类之间共享类片段(行为与状态)。Trait 是方法的集合,可以被多个不受继承关系约束的类重用。自 Pharo 7.0 起,Trait 也可以包含状态。

使用 Trait 可以在不同类之间共享代码而无需重复代码。这使得类可以轻松地在多个类之间重用有用的行为。

正如我们稍后将展示的,Trait 提供了一种以规范方式组合和解决冲突的方法。在 Pharo 中,冲突的解决方式并非像其他语言那样由最后加载的方法决定。相反,组合者(无论是类还是 Trait)始终具有优先权,并可以在其上下文中决定如何解决冲突:方法可以被排除,但在组合时仍然可以通过新名称访问。

11.1 一个简单的Trait

以下代码定义了一个 Trait。uses: 子句是一个空数组,表示该 Trait 不包含其他 Trait。

清单11-1 一个简单的Trait

Trait named: #TFlyingAbility
    uses: {}
    package: 'Traits-Example'

Trait可以定义方法。TraitTFlyingAbility定义了单个方法fly

TFlyingAbility >> fly
    ^ 'I''m flying!'
    

现在我们定义一个名为Bird的类。它使用了TraitTFlyingAbility,因此类包含了fly方法。

Object subclass: #Bird
  uses: TFlyingAbility
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'

Bird类的实例可以执行fly消息

Bird new fly
>>> 'I''m flying!'

11.2 使用必需的方法

Trait方法不必定义一个完整的行为。Trait方法可以调用使用它的类上可用的方法。

在这里,greeting方法调用了并未在TraitTGreetable中定义的name方法。在这种情况下,使用Trait的类必须实现这样一个必需的方法。

Trait named: #TGreetable
  uses: {}
  package: 'Traits-Example'
  
TGreetable >> greeting
  ^ 'Hello ', self name
  

请注意,Trait中的self代表消息的接收者。与类和默认方法相比,没有什么变化。

Object subclass: #Person
  uses: TGreetable
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'
  

我们定义了使用TraitTGreetablePerson类。在Person类中,我们定义了name方法。greeting方法将调用它。

Person >> name
  ^ 'Bob'
  
Person new greeting
>>> 'Hello Bob'

11.3 Trait里的self代表消息接收者

人们可能会想,在Trait中self指的是什么。但是,在类中定义的方法中使用的self和在Trait中定义的方法之中使用的self之间没有区别。self总是代表着接收者。事实上,无论方法是在一个类或一个Trait中定义的,对self没有影响。

我们定义了一个小Trait来证明这一点:方法WhisAmi只返回self

Trait named: #TInspector
  uses: {}
  package: 'Traits-Example'
  
TInspector >> whoAmI
  ^ self
  
Object subclass #Foo
  uses: TInspector
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'
  

下面的代码片段显示self是接收者,即使从Trait的方法中返回时也是如此。

| foo |
foo := Foo new.
foo whoAmI == foo
>>> true

11.4 Trait的状态

从Pharo 7.0开始,Trait也可以定义实例变量。这里,TraitTCounting定义了一个名为#count的实例变量。

Trait named: #TCounting
  instanceVariableNames: 'count'
  package: 'Traits-Example'
  

Trait可以通过定义一个后面跟着类名的initialize方法来初始化其状态。请注意,这是一个编码约定。这里,TraitTCounting定义了方法initializeTCounting

TCounting >> initializeTCounting
  count := 0
  
TCounting >> increment
  count := count + 1
  ^ count
  

Counter类使用TraitTCounting:它的实例将有一个名为count的实例变量。

Object subclass: #Counter
  uses: TCounting
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'

若要正确地初始化计数器实例,Counter类的initialize方法应该调用先前定义的Trait方法initializeTCounting

Counter >> initialize
  self initializeTCounting
  

下面的代码显示我们创建了一个计数器,并且它有一个初始化良好的实例变量。

Counter new increment; increment
>>> 2

11.5 一个类可以使用两个Trait

一个类并不局限于只能使用一个Trait。它可以使用多个Trait。假设我们定义了另一个称为TSpeakingAbility的Trait。

Trait named: #TSpeakingAbility
  uses: {}
  package: 'Traits-Example'

该Trait定义了一个名为speak的方法

TSpeakingAbility >> speak
  ^ 'I''m speaking!'
  

现在,我们定义了第二个TraitTFlyingAbility

Trait named: #TflyingAbility
  instanceVariableNames: ''
  package: 'Traits-Example'
  

这个Trait定义了一个fly方法.

TFlyingAbility >> fly
  ^ 'I''m flying'
  

现在,Duck类可以同时使用TFlyingAbilityTSpeakingAbility,如下所示:

Object subclass: #Duck
  uses: TFlyingAbility + TSpeakingAbility
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'
  

Duck类的实例从这两个Trait中获得所有行为。

| d |
d := Duck new.
d speak
>>> 'I''m speaking!'
d fly
>>> 'I''m flying!'

11.6 重写方法优先于Trait方法

源自 Trait 的方法表现得就像它是在使用该 Trait 的类(或 Trait)中定义的一样。现在,Trait 的使用者(无论是类还是另一个 Trait)总是可以重新定义源自 Trait 的方法,并且在使用者中的重新定义优先于 从 Trait 继承的方法。

让我们举例说明一下。在Duck类中,我们可以重新定义方法speak来做其他事情,例如发送消息quack

Duck >> quack
  ^ 'QUACK'
  
Duck >> speak
  ^ self quack
  

这意味着

  • Trait方法speak不能再从类中访问,并且

  • 取而代之的是使用新方法,即使是使用了speak的方法也是如此。

Duck new speak
>>> 'QUACK'

我们定义了一个新的名为doubleSpeak的方法如下:

TSpeakingAbility >> doubleSpeak
  ^ 'I double: ', self speak, ' ', self speak
  

下面的示例显示,Duck类的方法speak的本地重新定义版本优先于TraitTSqueakingAbility的版本。

Duck new doubleSpeak
>>> 'I double: QUACK QUACK'

11.7 访问被重写的Trait方法

有时,您希望重写Trait中的方法,同时仍然希望能够访问被重写的方法。这可以通过使用@->运算符在类Trait组合子句中为被覆盖的方法创建别名来实现,如下所示:

Object subclass: #Duck
  uses: TFlyingAbility + TSpeakingAbility @{#originalSpeak -> #speak}
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'
  

请注意,箭头表示新方法与旧方法相同:它只是有了一个新名字。在这里,我们说originalSpeakspeak的新名字。

与其他方法一样,可以通过发送包含新名字的消息来访问被重写的方法。在这里,我们定义了differentSpeak方法,它发送的是originalSpeak消息。

Duck >> differentSpeak
  ^ self originalSpeak, ' ', self speak
  
Duck new differentSpeak
>>> 'I''m speaking! QUACK'

请注意,别名不是完整的方法重命名。事实上,如果被重写的方法是递归的,它将不会调用新名称,而是调用旧的名称。别名只是为现有方法指定一个新名称,它不会更改其定义:方法体中的任何内容都不会更改。

11.8 处理冲突

在同一个类中使用的两个Trait可能定义了同名的方法。这种情况是一种冲突。要解决这样的冲突,有两种策略:

  1. 使用 exclude 操作符(-),你可以从定义该方法的 Trait 中排除冲突的方法。在这种情况下,类中将使用另一个方法。

  2. 在类中本地重新定义冲突的方法。在这种情况下,冲突的方法会被覆盖,新的重新定义的行为将成为类中可用的行为。请注意,被覆盖的方法可以通过之前提到的 @ 操作符使其仍然可访问。

这里有一个例子。让我们定义另一个名为THighFlyingAbility的Trait。

Trait named: #THighFlyingAbility
  instanceVariableNames: ''
  package: 'Traits-Example'

该Trait定义了一个fly方法

THighFlyingAbility >> fly
  ^ 'I''m flying high'
  

当我们定义使用两个 Traits THighFlyingAbilityTFlyingAbility 的类 Eagle 时,发送 fly 消息时会发生冲突,因为运行时不知道要执行哪个方法。

Object subclass: #Eagle
  uses: THighFlyingAbility + TFlyingAbility
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'
  
Eagle new fly
>>> 'A class or trait does not properly resolve a conflict between
     multiple traits it uses.'
     

11.9 解决冲突:排除方法

为了解决冲突,在合成过程中,我们可以简单地从TraitTFlyingAbility中排除fly方法,如下所示:

Object subclass: #Eagle
  uses: THighFlyingAbility + (TFlyingAbility - #fly)
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'

目前只有一个fly方法,来自THighFlyingAbility的方法。

Eagle new fly
>>> 'I''m flying high'

11.10 解决冲突:重新定义方法

解决冲突的另一种方法是使用Trait简单地重新定义类中冲突的方法。

Object subclass: #Eagle
  uses: THighFlyingAbility + TFlyingAbility
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'Traits-Example'
  
Eagle >> fly
  ^ 'Flying and flying high'
  

现在只有一个fly方法:在Eagle类中定义的方法。

Eagle new fly
>>> 'Flying and flying high'

您还可以通过创建与Trait相关联的别名来访问被覆盖的方法,如前所述。

11.11 小结

Trait是可以在不同用户(类或Trait)中重用的一组方法和状态,支持在单一继承语言的上下文中以这种方式进行多重继承。一个类或Trait可以由几个Trait组成。Trait可以定义实例变量和方法。当类中使用的Trait定义具有相同名称的方法时,这会导致冲突。

有两种方法可以解决冲突:

  • 用户(类或Trait)可以在本地重新定义冲突的方法:本地方法优先于Trait方法。

  • 用户可以排除冲突的方法。

此外,可以通过Trait组合子句中定义的别名访问被覆盖的方法。