首页 > 代码库 > 站在新语言平台上再谈"组合"与"继承"

站在新语言平台上再谈"组合"与"继承"

长久以来,OO编程思想的一个重要信条是:多用组合,少用继承,这被广为接受和认可。Scala引入Trait(特质)之后,这一点“似乎”受到了冲击,你可以看到,在很多Scala代码里出现了通过继承多个Trait为一个Class混入(追加)新功能的案例,而其中有不少案例是过去我们在传统OO语言(例如Java)中不会或不建议的做法,因为看上去那确实是在滥用继承。

举个简单的例子,日志功能是非常普遍的需求,传统的Java程序里是以“组合”的方式为一个类添加这一功能的,也就是在类里声明一个logger实例作为类的字段,然后就可以在类的各处使用它。这在Java这种单继承的编程语言里几乎是唯一的选择,很显然,你不可能让你的类仅仅为了一个日志打印功能就占用了唯一一个父类名额,即使你的类没任何父类,让一个类去继承一个Logging父类也是一件很怪异的事。

在Scala里,事情发生了一些变化,由于Trait的多继承特性让一个类去继承多个特质变得自然而普遍,这为程序员提供了一个新的“为一个类混入新功能”的途径。于是很多在Java里使用组合的地方都被基于特质的“继承”替代了。同样是上面的日志功能,在Scala里的普遍实现方式是这样的:

trait Logger {
  def log(msg: String): Unit = {
    println(msg)
  }
}

class DataAccess extends Logger{
    def query(in: String) = {
      log(in)
  }
}

new DataAccess().query("Test")

这里要首先解释一点的是:让DataAccess继承一个Logger看上去是有些突兀和怪异,如果DataAccess有其他的父类或特质,我们在with语句的最后append上Logger或许看上去会自然很多。

好的,回到Scala的这版实现上,一个从Java刚刚转到Scala的程序员看到这样的代码会感到很不舒服,他过去接受的传统教条告诉他这个地方应该使用组合而不是继承!但是Scala社区普遍这样去写是需要我们深思的。首先,我们需要搞清楚一个问题,在传统的OO语言里为什么要提倡多用组合少用继承,滥用继承的危害是什么?

一个普遍的认同是“滥用”继承会破环封装,请记住我们的限定条件是“滥用”。在支持多继承的语言里,继承一个类的成本极低,低到可以忽略不计的程度,这是造成继承被滥用的一个主要原因,滥用继承最直接的后果就是破坏封装,原因很简单,当子类继承父类之后就会“承袭”父类所有的类和方法,如果一个类从核心用意和设计初衷上天然是另一个类的子类时,并不存在破坏封装这种说法,真正出问题的场景是:一个类从用意和设计初衷上不应是某个类的一个子类,但又需要用到或依赖到这个类的一部分功能,此时使用继承就会将父类全部的字段和方法暴露给子类。如果使用组合则会在一定程度上限制这种情形的发生。

上述我们分析到了两个判定是否是滥用继承的重要依据:

  1. 如果一个类从核心用意和设计初衷上天然是另一个类的子类,这种继承是天经地义的,并不存在破坏封装的说法。
  2. 在允许多继承的语言里,如果一个类需要使用到来自另一个父类(特质)的“全体”字段和方法,或都反过来说,把某个父类(特质)的全体成员赋予另一个类时,如果从这两个类的设计用意和代表的概念上没有任何的违和感,那么,这时候使用继承也是正当的,没有破坏封装的嫌疑。

对于第二点实际上还有很多的潜台词,我们强调了“全体”字段和方法,这是体现”is-a”关系的一个重要标志,若不需要全体,那这种继承就值得怀疑,通常,这有两种可能:

  1. 你根本不应去继承
  2. 你的父类(特质)职责是否不够单一?是否需要重构成多个职责更单一的父类(特质),然后再从中选择合适的父类(特质)进行继承呢?

基于上述原则,我们分析两个案例,第一个就是前面实例代码中的Logger特质,我们可以说这是一个职责绝对单一的特质,所有想要继承它的类目的也很单一:获得日志输出的能力,在这种情况下使用继承没有任何副作用,尽管这对从单继承语言刚刚转过来的程序员而言还是会感觉有些“心里不踏实”,但是仔细分析一翻就会发现并未触碰到什么红线,所以应该可以很快适应这种写法。

另一个则是与Logger极为类似但在我个人看来却是一个反面案例,就是在很多代码里看到的对Config类的继承:

trait Config {
  private val config = ConfigFactory.load()

  private val httpConfig = config.getConfig("http")
  private val databaseConfig = config.getConfig("database")

  val httpHost = httpConfig.getString("interface")
  val httpPort = httpConfig.getInt("port")

  val jdbcUrl = databaseConfig.getString("url")
  val dbUser = databaseConfig.getString("user")
  val dbPassword = databaseConfig.getString("password")
}

class HttpService extends Config {
    ...
}

class DatabaseService extends Config {
    ...
}

类似上面的代码在很多程序里出现过,设计者寄希望通过继承Config让一个类能方便的获取配置项的值,不同于Logger, Config包含了整个应用的所有配置项,没有哪一个类需要并应该继承它的所有字段,这与我们前文提及第2个重要原则相违背,这也严重的破坏了Config维护的封装。代码中的HttpService和DatabaseService也能从侧面说明这一点,它们各自关心的是Http和Database相关的配置,对于其他的配置项没有理由也暴露给它们。相对优雅的做法应该是把这些配置项封装到一个object中,在需要使用某个配置项时以变量的方式获取即可。

小结一下吧:

对于那些从传统单继承OO语言转到支持多继承语言的程序员来说,你应该“想开点”:),花开堪折直须折,如果一个类就是想要获得某方面的“特质”,多了不要,少了不行,那就放心大胆地去继承那个“特质”吧。除此以外,你还是要审慎地看待每一个继承,继承始终是一件需要警惕的事,特别是在允许多继承的语言里。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    站在新语言平台上再谈"组合"与"继承"