Dart学习笔记(33):Mixin混合模式

发表于2018-07-04 13:15 阅读(59)

多重继承有很多不足,例如,结构复杂化,优先顺序模糊,功能冲突等问题,而Mixin则是为了解决多继承的问题而出现。

对于Mixin并没有一个准确的概念,有人理解为Mix in混入。它类似于多继承,但通常混入Mixin的类和Mixin类本身并不是is-a的关系。

本文地址:http://www.cndartlang.com/912.html

实质上,Mixin是通过语言特性,来更简洁地实现组合模式。因此,Mixin可以灵活地添加某些功能。传统的接口概念中,并不包含实现部分,而Mixin包含实现。

本节将介绍在Dart中使用混合模式的一些细节。近来,Dart开放了一些早期关于实现部分的限制,当前状态如下:
  · Dart 1.13以及更高版本支持Mixin,可extend继承类(不仅是Object),并且可以调用super()。
  · Dart 1.12或低版本支持Mixin,但只能继承Object,并且不能调用super()。

1、基本概念

如果你熟悉关于Mixin的学术文献资料,你可以跳过这一部分。否则,建议你了解一下,因为它定义了重要概念和注解。如果你希望深入探究,可以从该文章开始:Mixins in Strongtalk

注:Smalltalk被公认为历史上第二个面向对象的程序设计语言和第一个真正的集成开发环境 (IDE)。并且,它对其它众多的程序设计语言的产生起到了极大的推动作用,主要有:Objective-C,Actor, Java 和Ruby等。90年代的许多软件开发思想得利于Smalltalk,例如Design Patterns, Extreme Programming(XP)和Refactoring等。Strongtalk的最独特之处是支持渐进式的类型注解,这种思想在Dart、PHP、Python 3和TypeScript等语言中都有体现。Gilad Bracha是Dart开发团队的一员,在20世纪90年代,Gilad同Urs Hölzle和Lars Bak等人一起创建了语言Smalltalk的一个高性能版本即Strongtalk。但是随着Java的流行,Sun停止了Strongtalk的投入,并将团队成员重新分配来优化Java的性能,而Strongtalk演变成了官方JVM即Hotspot。

作为一个支持类和继承的语言,类隐式地定义了Mixin。Mixin隐式地通过类主体(Class Body)进行定义,并建立子类和父类之间的变量增量(Delta)。而Class类实际上则是一个Mixin应用,即是通过隐式定义的Mixin应用于父类的结果。

Mixin Application混合应用类似于Function Application函数应用。在数学中,混合类M可以视作从父类到子类新增的一个功能,将M注入超类S,并且返回一个S的子类。在研究文献中,这通常写作:M |> S

基于函数应用的概念,可以定义复合函数(即函数组合)。该概念贯穿于混合组合中。我们定义混合M1和M2的组合,写作M1*M2,如:(M1 * M2) |> S = M1 |> (M2 |> S)

函数非常有用,因为他们可以应用于不同的参数。同样,Class隐式定义的Mixin,通常仅在类声明给出的父类中应用一次。为允许Mixin应用于不同的父类,我们要么声明Mixin不依赖于特定的父类,要么脱离于Class隐式的Mixin,然后重用外部的原始定义。

2、语法和语义

Mixin通过正常的类声明被隐式定义。原则上,每个类都定义了一个Mixin,并可以从类中提取出来。然而,Mixin只能从未定义构造函数的类中提取。由于沿着继承链传递构造函数参数的需要,该约束能避免出现新的连锁问题。例如:

abstract class Collection<E> {
  Collection<E> newInstance();

  void forEach(void f(E element)) {
    // ...
  }

  void add(E element) {
    // ...
  }

  // ...
}

abstract class DOMElementList<E> = DOMList with Collection<E>;
abstract class DOMElementSet<E> = DOMSet with Collection<E>;

这里,Collection<E>是一个标准的类,并用来声明一个Mixin。另外,DOMElementList和DOMElementSet也是混合应用。这两个Mixin在类声明的时候,通过特殊的形式来定义。在类声明中包含一个名字,并用with语句来声明他们与父类中的混合应用相同。Collection<E> 是抽象类,因为它并没有实现类中定义的抽象方法newInstance();。

上述中,事实上DOMElementList混合为Collection mixin |> DOMList,而DOMElementSet则是 Collection mixin |> DOMSet

这样做的好处是,Collection中的代码可以在类的多个继承层次中被共享。因此,无论DOMList还是DOMSet,都不需要重复、复制Collection中的代码,并且任何变化都会使Collection传递到这两个继承结构中,大大简化了维护代码。上面的代码介绍了Mixin应用的一种方式:混合应用指定应用的Mixin和父类,以及混合应用的名称。

另外一种情况,混合应用出现在类声明的with语句中,以逗号来分隔标识符列表的时候。此时,所有的标识符代表Class。在这种情况下,多混合在extends语句中可以构成及应用于父类名称,生成一个匿名的父类。再以同样的例子:

class DOMElementList<E> extends DOMList with Collection<E> {
  DOMElementList<E> newInstance() => new DOMElementList<E>();
}

class DOMElementSet<E> extends DOMSet with Collection<E> {
  DOMElementSet<E> newInstance() => new DOMElementSet<E>();
}

这里,DOMElementList并不是应用Collection mixin |> DOMList。相反,它是一个父类为应用的新定义的类,DOMElementSet同样如此。注意,在每一种情况下,抽象函数newInstance()必须单独实现,以便类能够直接被实例化。

想象一下,如果DOMList有一个带参数的构造函数:

class DOMElementList<E> extends DOMList with Collection<E> {
  DOMElementList<E> newInstance() => new DOMElementList<E>(0);
  DOMElementList(size): super(size);
}

构造函数可以为各个字段以及泛型参数设置值。每个Mixin都有一个自定义的构造函数被单独调用,父类也是如此。因为Mixin的构造函数不能够被声明,所以调用函数可以省略。在底层的实现中,调用总是放在初始列表之前。

第二种是以方便实用的语法糖形式,将多个Mixin混入类中,而不需要引入多个中间声明。例如:

class Person {
  String name;
  Person(this.name);
}

class Maestroends P
 with Musical, Aggressive, Demented {
  Maestro(name):super(name);
}

这里,父类是一个混合应用:Demented mixin |> Aggressive mixin |> Musical mixin |> Person

假设仅Person有带参数的构造函数,则 Musical mixin |> Person 将继承Person的构造函数。以此类推,一直到Maestro实际的父类Person,它由一系列的Mixin应用组成。

3、细节描述

Privacy私有

一个混合应用很可能是在外部最初声明Class的库中被声明,这对访问混合应用的成员没有任何影响。根据混合应用的语义可知,访问成员取决于库最初声明的位置。这与普通的继承一样,是由底层语言(C++)的继承语义决定的。

Statics静态

是否可以通过混合应用使用最初Class的静态值?同样,由继承的语义进行分析,在Dart中静态成员不会被继承。

Types类型

混合应用的实例是什么类型?通常,它是父类的子类型,以及通过Mixin名称表示的类型的子类型。换句话说,它与最初Class的类型相同。

最初的Class有它自身的父类。为确保特定的混合应用与最初进行混入的Class兼容,需要使用with语句。例如,如果通过with语句定义了Class A,并应用了一个混合M,M源自Class K,那么A必须支持K定义的接口。

class S {
  twice(int x) => 2 * x;
}

abstract class I {
  twice(x);
}

abstract class J {
  thrice(x);
}
class K extends S implements I, J {
  int thrice(x) => 3* x;
}

class B {
  twice(x) => x + x;
}
class A = B with K;

需注意的是,A必须支持K的父类S的隐式接口。这确保A实际上是M的子类型,尽管它的继承结构并不相同。在上面的例子中,K必须实现twice()来满足I的需求,以及实现J中声明的thrice()。K满足这两个条件,因为它直接定义了thrice(),以及继承了S的twice()。

现在我们定义A,得到来自K的Mixin的thrice()实现。然而,虽然K继承了S,但Mixin并没有提供twice()的实现。幸运的是,B实现了该接口。所以总的来说,A满足I、J、S的要求。

相比之下,下面定义Class D:

class D {
  double(x) => x+x;
}

class E = D with K;

该代码运行的时候会得到一个警告,因为Class E没有实现I声明的twice()函数。