在业务设计过程中,除了继承这种增量进化,有些时候我们只需要给类添加功能而不是想变成某种类型,那么我们可以选择组合。在这篇文章会先介绍Python的多继承和Scala的trait对组合的实现,最后再来讨论两者的优劣和如何更好的使用它们。 python 那么从一段Python代码开始,看看Python如何处理组合的问题,以及我们要如何避免多继承的问题。
class A:
def a(self):
return 'a'
class B:
def b(self):
return 'b'
class C(A,B):
pass
这里的C同时继承了A,B的方法a,b(为了页面展示省去了内置方法),bases方法给出了C继承的父类A和B。
dir(C)
Out[11]:
[...,'a','b']
C.__bases__
Out[12]: (__main__.A, __main__.B)
对于多继承,还需要解决子类继承的多个父类的实现了同一个方法的问题,这个问题就是“菱形问题”,对上面的代码再做一定的修改:
class A:
def a(self):
return 'a'
class B(A):
def b(self):
return 'b'
class NewB(A):
def b(self):
return 'NewB'
class C(NewB,B):
pass
NewB和B都继承了A,那么继承了B和NewB的C调用的是哪个方法?Python对于多继承同名方法的调用是基于C3算法实现的,这篇文章不对C3算法做深入的了解,简单来说就是: 深度优先,从左到右,移除继承列表中重复类型,保留最后一个。 在实际过程中,可以使用__mro__方法查看这个类的方法解析顺序。下面的代码也证明了C中的b方法来源于NewB这个类。
C.__mro__
Out[14]: (__main__.C, __main__.NewB, __main__.B, __main__.A, object)
C().b()
Out[15]: 'NewB'
当然我们也可以跳过方法继承顺序直接使用我们想要的类,唯一要做的就是显性的传入self这个参数。例如:
class C(NewB,B):
def b(self):
return B.b(self)
C().b()
Out[17]: 'b'
不过最好的办法是使用super(),遵循方法继承顺序,不容易引起混乱。
class C(NewB,B):
def b(self):
return super().b()
C().b()
Out[21]: 'NewB'
注意的是,使用Python的多继承的过程中,极力避免子类继承多个不同类型的类,如果某个类是一个混入类,在其后面加上Mixin。
Scala 了解完Python的多继承,再来讨论Scala的trait的使用。
scala> trait A {
| def a:String = "A"
| }
defined trait A
一个简单的特质A实现了,我们可以使用extends或者是with加入到类B当中:
scala> class B extends A {
| def b: String = "b"
| }
defined class B
用extends关键字实际上是表示B隐性的继承了特质A
scala> val b = new B
b: B = B@169bb4dd
scala> b.a
res0: String = A
特质也可以作为一个类型来使用,例如:
scala> val a:A = b
a: A = B@169bb4dd
scala> a.a
res1: String = A
在某个类已经继承了某个父类的时候,就需要使用with关键字来混入trait了。(可以连续使用with来混入多个特质)
scala> class NewA
defined class NewA
scala> class C extends NewA with A {
| def b: String = "b"
| }
defined class C
Scala的特质可以像类一样的定义,但是不能传入任何构造参数,例如:
scala> trait D(x:Int)
<console>:1: error: traits or objects may not have parameters
trait D(x:Int)
同样的trait也面临着继承顺序的问题,于Python不同的是,Scala的继承顺序关键在于super方法的调用,而不是同名方法的调用顺序的确定,因为添加进子类的两个trait的同名方法在编译期便会报错。
scala> trait A {
| def name:String = "A"
| }
defined trait A
scala> trait B{
| def name:String = "B"
| }
defined trait B
scala> class C extends NewA with A with B{
| def b: String = "b"
| }
<console>:10: error: class C inherits conflicting members:
method name in trait A of type => String and
method name in trait B of type => String
(Note: this can be resolved by declaring an override in class C.)
我们设计这么一个类来展示Scala如何确定super的调用方法:
scala> abstract class Basic{
| def get():Unit
| def put(x:String)
| }
defined class Basic
scala> class A extends Basic{
| private val buf = new ArrayBuffer[String]
| def get() = println(buf.result())
| def put(x:String) = {buf += x}
| }
defined class A
scala> trait B extends Basic{
| abstract override def put(x:String) = {super.put("B"+x)}
| }
defined trait B
scala> trait C extends Basic{
| abstract override def put(x:String) = {super.put("C"+x)}
| }
defined trait C
scala> val test = new A with B with C
test: A with B with C = $anon$1@497ed877
scala> test.put("A")
scala> test.get()
ArrayBuffer(BCA)
从代码中可以看出来首先起作用的是特质C,其次是B,最后A,再将BCA放入buf列表当中,Scala将这种调用顺序称为线性化,它将所有类和它继承的类以及特质按照一定顺序排列起来,从右至左开始执行。
如何评价: 继承可以让新手顺利的使用专家设计出来的框架,但是其本身基于依赖的实现方式会导致耦合的问题。所以很重要的一点就是,在需求的实现过程中,应该区分我们要做的是功能扩展还是使用某些功能,如果仅仅只是使用某些功能(组合),所以Scala和Python给出了两种不同的实现方式,Scala选择了trait(特质),Python因为历史原因,保持了多继承的方式。它们都提供了一种混入(mix-in)的机制,让某一种类型的扩展出一种其它类型的功能。多继承的问题在于,它会导致写出来的类产生混乱,无法判断你继承的类到底属于哪一种类型。Python的多继承在一定程度上并没有Scala的灵活,它的多继承在处理同名方法时采用的是覆盖的方式,而组合的核心在于“能做什么”,而不是“是什么”,功能的混入不应该像类的继承,而是相对独立,正因为如此,trait逐渐成为了现在语言的发展趋势。