Traits 是什么

Traits 是面向对象编程中使用的概念,它表示一组用于扩展类的功能的方法 – wikipedia Trait

Traits 最初在 Smalltalk 里被发明出来,后来有不少编程语言都实现了它:

Traits 是由 Nathanael Schärli 和他的合作者设计和实施的,时间发生在 2000 年 他在 Alan Kay 手下的实习期间。我在同一时间也在那儿实习。我有机会看到这项工作的进展情况。
这个版本的 Traits 最初是在 Smalltalk 的背景下构思的,但是它简洁的特性吸引了很多其他的语言设计者 – Yoshiki Ohshima

最佳读物

理解 Traits 的最佳读物是 Traits: Composable Units of Behaviour, 这篇论文清晰而详细讨论了 Traits 试图解决的问题和它的基本想法。 以下是我阅读时做的摘录。

这些摘录主要供我自己日后复习用,如果你想获得对 Traits 的全面认识,建议去阅读论文原文。

Abstract

  • 尽管继承作为面向对象编程语言的基本重用机制具有无可争议的重要性,但其主要变体–单继承、多继承和混合继承–都存在着概念层面和实际层面的问题。

  • Traits 是一个简单的用于构造面向对象程序的组合模型。Traits 本质上是一组纯方法(译者注: 不包含状态(如实例变量)),作为类的构建模块,是代码重用的原始单元。

1 Introduction

  • 单继承的表现力不足以将复杂层次结构中的类所共享的共同特征(即实例变量和方法)考虑进去。因此,语言设计者们提出了各种形式的多重继承, 以及其他机制,如 mixins, 允许类从特征集中逐步组成。

  • 类经常不是最合适的重用代码的元素,因为类扮演着两个相互竞争的角色。类的主要作用是作为实例的生成者:因此它必须是完整的。但作为一个重用的单位,类应该是小的。这些特性经常发生冲突。此外,类作为实例生成器的作用要求每个类在类的层次结构中有一个独特的位置,而重用的单位应该在任意的位置适用。

  • 继承要求 mixins 以线性方式组成;这严重限制了人们指定 “胶水代码” 的能力,而 “胶水代码” 是调整混合体以使它们适合在一起所必需的。

  • Traits 的设计始于这样的观察:重用和可理解性之间的冲突比实际情况更明显。一般来说,我们认为,如果能够以多种形式来看待一个程序,那么理解该程序就会更容易。即使一个类可能是由一个复杂的层次结构中的小 Traits 组成的,也没必要要求以同样的方式来看待它。我们应该可以把类看作是一个扁平的方法集合,或者看作是一个由 Traits 构成的组合实体。扁平化的观点促进理解;组合的观点促进重用。只要这两种观点能够共存,就不会有冲突,这就要求组合只作为一种结构化工具,对类的意义没有影响。

  • Traits 有以下特性:

    • 一个 Trait 提供了一套实现行为的方法。

    • 一个 Trait 需要一组方法来作为它(Trait)所提供的行为的参数

    • Traits 不指定任何状态变量,Traits 提供的方法也从不直接访问状态变量。

    • 类和 Traits 可以由其他 Traits 组成,但组成顺序是不相关的。冲突的方法必须被明确解决。

    • Traits 组合不影响类的语义:如果从 Traits 中获得的所有方法都直接在类中定义,那么类的意义是一样的。

    • 同样地,Traits 的组合也不影响 Traits 的语义:一个复合 Traits 等同于一个包含相同方法的扁平化 Traits。

3 Traits

3.2 Specifying Traits

  • Traits 可能需要一组方法来作为它(Trait)所提供的行为的参数。Traits 不能指定任何状态,也不能直接访问状态。Traits 方法可以间接地访问状态,使用最终由访问器(accessors)(getter 和 setter 方法)满足的依赖方法(required methods)。

图3. Traits TDrawing 和 TCircle,左栏是提供的方法,右栏是依赖的方法(required methods)。
  • 在我们的例子中,每个图形对象可以被分解成两个方面–它的几何形状,以及它在画布上的绘制方式。对于一个圆,我们用 trait TCircle 表示其几何形状,用 trait TDrawing 表示其绘制行为。

3.3 Composing Classes from Traits

  • Traits 是对单一继承的补充,而不是对单一继承的取代。 继承是用来从另一个类中派生出一个类,而 Traits 是用来实现类定义中的结构和重用性

  • Class = Superclass + State + Traits + Glue

图4. Circle 类由 traits TCircle 和 TDrawing 组成。`TDrawing>>bounds` 的要求由 trait TCircle 来实现。所有其他的要求都由该类的访问器方法来实现。
  • 这意味着类从超类中派生出来,通过添加必要的状态变量,使用一组 traits,并实现连接 traits 的胶合方法,作为状态变量的访问器。为了使类变得完整,必须满足 traits 的所有要求,即必须提供具有适当名称的方法。这些方法可以在类本身、直接或间接的超类中实现,或者由类所使用的另一个 trait 实现。

  • 在类中定义的方法和由并入的 traits 定义的方法之间的冲突是通过以下两条优先规则解决的

    • 类方法优先于 trait 方法。

    • trait 方法优先于超类方法。这源于扁平化属性,它指出 traits 方法的行为就像它们被定义在类本身中一样。

3.4 Composite Traits

  • 就像类是由 traits 组成的一样,traits 也可以由其他 traits 组成。与类不同的是,大多数 traits 并不完整,这意味着它们没有定义其子 traits 所要求的所有方法。子 traits 中未满足的要求只是成为复合 trait 的必要方法。同样,组合顺序并不重要,复合 traits 中定义的方法优先于其子 traits 的方法。

  • trait TCircle 包含两个不同的方面:比较运算符和几何函数。为了分离这些方面并提高代码的重用性,我们把这个 trait 重新定义为 trait TMagnitude 和 TMGeometry 的组合

4 Discussion and Evaluation

  • 细粒度的重用很重要,因为整个类和单个方法之间的差距太大。traits 允许通过组合可重用的行为来构建类,而不是通过实现大量非结构化的方法集。层次独立是很重要的,因为它可以最大限度地提高重用性。因为类的主要作用是作为实例生成器,它们必须是完整的,因此通常被嵌入到一个层次结构中。这一特性使得类不适合于它们在传统语言中扮演的次要角色:可重用的方法库。

Squeak 中的 Traits

翻译自 Squeak by Example > 5.4 > Traits

简单例子

要定义一个新的 trait,只需将子类的创建模板替换为发送给 Trait 类的消息。

1
2
3
Trait named: #TAuthor
uses: { }
category: 'Trait-Demo'

Traits 可以包含方法,但不能包含实例变量

1
2
3
TAuthor»author
"Returns author initials"
^ 'taa' "the anonymous author"

现在我们可以在一个普通类中使用这个 trait

1
2
3
4
5
6
Morph subclass: #TestBook
uses: TAuthor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Trait-Demo'

实例化 TestBook 之后,就可以发送 author 消息

TestBook new author −→ 'taa'

常见问题

  1. 怎样找到系统中的所有 Traits ?
1
Smalltalk allTraits
  1. 怎样找到哪些类使用 Traits?
1
Smalltalk allClasses select: [:each | each hasTraitComposition]

附录

扩展类/对象的行为的机制

值得注意的是, Yoshiki 提到大约在 Traits 正在设计的时候, Etoys 也启动了,Etoys 中可插拔的方法的理念类似于 Traits,但有一些关键的区别, 最重要的是,它是基于实例而不是基于类的机制;它用于增强用户关心的具体对象(即,这是用户自己的绘图),而不是你用来构建抽象结构然后从中创建实例的东西。 如果你对构建灵活的终端用户编程环境感兴趣,Etoys 非常值得学习,它可能是迄今为止最出色的终端用户编程环境之一,Squeak 6.0 非常好地整合了 Etoys。

你可以从 Player 类入手来理解 Etoys 的设计, 这是我目前的做法。重要的不是阅读类的静态代码,这是极为低效的,抛弃过去在其他语言里养成的这些弱小习惯,试着融入 Smalltalk 文化,去跟活的(live)对象一起玩耍,然后深入它们。

参考