多继承和 MIXIN

C++ 提供的一个 OOP 的功能是多继承。单继承的概念是简单的,但是表述能力非常有限,那么看起来很简单的扩展就是多继承。

单 继承符合人类的认知习惯,鸡鸭鹅都有翅膀,都会飞,他们一定存在某种共性,这种归纳的能力让人们容易理解它们共同的属性可以形成一个父类,这个父类带有共 同的属性,它帮助我们可以从另一个更高的层次上理解这一类物体。多继承却有些让人觉得困难,一个东西为什么会存在两个父类?

  • 一种常见的多继承来自 policy,即 strategy pattern,每一类功能作为一个父类继承下来,实现这些类功能的共同体
  • 还有一种并不对等的多继承,其中一个需要另一个的某些实现提供额外的接口
  • 概念上实现某些混合功能的对象可以认为是实现特定功能的类的组合,但是它们又共有一些东西

后面两种情形在 C++ 中常见的是 CRTP 和 virtual 继承。多继承的时候如果两个父类存在相同 signature 的方法或者成员,就需要在子类里面通过表述父类名才能消除歧义。

加 上 C++ 复杂的权限控制,继承变得格外的复杂,protected/private 继承时出现的微妙的意思需要程序员对其有很深刻的理解才能比较准确的使用对。另外类似 private 继承通常也可以直接通过 aggregation 实现。C++ 继承和函数的 override 是分离的,只有 virtual 函数才会表现出多态。所以从这个角度来说 C++ 的类型系统常为人诟病。至于希望模仿 C++ 的 Java 采用了另外一个单继承 + 接口实现的模型。

和 C++ 类似的估计还有 python,但是其多继承永远是 virtual 的,且其 resolution 是依照父类定义的顺序使用 depth first search 原则解决的。

Java 的单根系统(所有的成员函数都是 virtual 的)从某个角度来说是很容易理解的,但是为了让对象互相的作用,比如要排序就得比较,那就意味着这些对象 需要提供某种方法供类似 sort、max 等抽象算法以调用的接口,但实际上肯定存在不能比较的也不需要比较的对象,因此这类功能不能在单根类(Object)上实现。Java 提供了一个个人感觉很不友好的“interface”,这个仅仅描述了 signature 的一组函数“声明”,往往很难定义清楚,另外由于不带有“实现”,所有 implements 接口的类必须提供实现(否则就是 abstract 类)。这往往意味着如果希望实现某一类基于某一组函数接口的功能,第一步是简化接口函数,这样 client code 在实现时才能比较轻松,这也导致了 Java 大多数接口都是 thin interface,但这意味着写 lib 的人将会有很多的限制。一个最简单的例子就是 Java 比较这个简单的功能,有 Comparable<T> 还有 java.util.Comparator<T后给程序员的印象是每个 lib 都会定义自己的 interface,很少或者几乎没有什么“重用”。因为 interface 就是一个声明,又不是实现,似乎也没有“重用”的必要。

当 然 Java 的世界里面也有“重名”问题,尽管只有一个 parent class,自然不会出现两个人都说要做主的情况,而 interface 也没有实现,所以就算重名,很明显谁实现了那就是用谁的了,这个过程通常又叫 linearization。Java 虽然有访问权限的关键字,但是并不作用在继承或者实现上也算是简化了吧。

那么是否能改进这样的类型系统?一种解决方案就是 mixin,所谓的 mixin 就是带有成员和方法实现的 interface,没有成员和方法实现的 mixin 其实就和 java 的 interface 是一样的东西了,这种概念在 scala 里面以 trait 的概念存在而在 ruby 里面以 module 的概念而存在。scala 和 ruby 仍然是单根系统,但允许 mixin 多个 trait/module,与 class 的区别在于 mixin 不能直接构造实例。

由于带有实现,通常 mixin 会导致 thick interface。一个简单的例子就是比较 Java 的 collection 和 scala 的 collection,基本的 list/set/map 在 Java 的 interface 里面都过于简洁以至于写一个简单的操作都需要导致循环,而 scala 的接口通过 mixin 就能将基本的方法扩展到很多常用的函数里面,一些遍历或者 filtering 的事情简单的用 map/reduce 之类的方法加上 closure 就 okay 了。当然就这个库的实现与 C++ 通过 iterator + generic algorithm 比较还是各有特点的。

scala 和 ruby 都实现了 linearization 来做 resolution。scala 的 linearization 比较有意思,顺序是将 with 的 mixin 倒过来,然后最后是实现的类(没有就是 AnyRef),然后递归的 linearize 这些,调用的方法就是依次搜索到的,比如某些 mixin 起到了 decorator 的作用,那么最后 with 的 mixin 就是最外层的 decorator,这也跟直观比较符合。

当然 mixin 的功能其实多继承是很容易实现的,比如 boost.iterators 提供实现 iterator 的 CRTP 你只需要提供简单的一两个方法就能获得和 STL 里面一样 powerful 并且该有的都定义好的 iterator 了,类似的技术在 expression template 里面也有体现。

最后谈一个所谓 duck typing 的问题。在 C++ 里面 template 衍生出来的 concept 概念其实就是 duck typing 的一种体现,即传递过来的对象并不见得“属于某个类”,但是只要它能做某些事情(提供了某些 signature 的实现)他就符合这个 concept,Java 是不提供这类功能的,即便一个类实现了 Comparator<T> 的函数只要没 implements Comparator<Self> 那么就不能用在依赖于这个接口的 lib 里面,这也是让人觉得 Java 设计类并不开放的一个因素吧。scala 在 JVM 上也支持 duck typing 但却与 C++ 是 compile-time check 不同,它是依赖 reflection 做 runtime checking 的,ruby 这类脚本语言对此支持毫不奇怪了。

——————
And Jacob loved Rachel; and said, I will serve thee seven years for Rachel thy younger daughter.

posted @ 2017-07-31 13:31  天涯海角路  阅读(531)  评论(0)    收藏  举报