浅谈ruby core library 与 Liskov Substitution Principle原则

Liskov Substitution Principle原则,简称LSP原则,是OOP软件方法中的一个设计原则,其大意是:如果S是T的子类,那么代码中所有用到T的地方,都可以通过S替代。这个原则是传统的强类型面向对象语言(如Java)必须遵守的一条原则。关于LSP原则的更多介绍,参考wiki

在ruby的基础类库中,Kernel Module是其核心,Object就是Kernel Module中的一个class,Object也是ruby中所有类的基类。在Object类中定义了一个dup方法,用来dump 对象信息(包括对象名称、ID等)。

现在的问题是,ruby中,一些继承Object的subClass,会抛出异常,当你调用dup方法时。
特别是在一些常量类(NilClass, FalseClass, TrueClass, Fixnum, 和 Symbol)会存在此问题。
看下面这个例子:

irb 1:0> 5.respond_to? :dup
=> true
irb 2:0> 5.dup
TypeError: can't dup Fixnum
from (irb):1:in `dup'
from (irb):1
irb 3:0>

如果你还不熟悉ruby,这里解释一下。第一行意思是测试类型为Fixnum的对象5是否存在dup方法(ruby中方法都是以Symbol对象保存的,因此 :dup 表示 名称为dup的Symbol对象),测试结果是true,因为Fixnum继承自Object类。但是实际情况是调用dup方法抛异常。
所以,这就违背了LSP原则。有一个术语称呼这种代码:Refused Bequest,通常解决这种问题的办法是使用组合替代继承

在ruby社区有一个关于这个问题的讨论,里面提出了几个解决方案。其中一种是将dup方法从常量类(如Fixnum)中删除,这样可以避免上面例子代码中的异常,因为5.respond_to? :dup返回的是false,但是这个还是违背了LSP原则。

这种行为在你做任意对象拷贝的场景下会出问题,可能你期望dup返回常量类自身,因为他是常量类,对吧?但是实际上这么说也不是很准确,因为在ruby中你可以re-open一个类或对象,往其中添加或删除方法。

因此,LSP原则到底意味着什么?当我在blog中讨论这个问题时候,Robert Dober,一位响应者,发表了观点:

我想说的是LSP原则不适合ruby因为ruby中没有提供类似这种的约定。为了阐述LSP我举个例子,我们有一个Base类(请原谅我用Java)

void aMethod(final Base b){
....
}

然后我期望这个方法在任何我传递Base类型参数的场景下都运行正常,否则会编译错误。

SubClass sc; // subclassing of Base
aMethod( sc ); // 这里应当要运行正常

上面所说的约束(其实就是LSP),在ruby中并不存在。我相信ruby给我传递的信息是:

  • OO语言是一种面向类的语言。
  • 动态语言是是一种面向对象的语言。

恕我直言,ruby改变了我很多面向类的观点,更多的转向了面向对象。

这位读者的观点是错的,因为面向类只是Java编译器做了检查,实际上我们运行时候还是会有类似的异常,比如ClassCastException,你也可以理解Java为面向对象的。

作为一个Java程序员,对违背了LSP原则的做法总是会觉得很不爽,但是Ruby提供了很多方便又容易使用(相对与Java)的API,那违背一点LSP原则是不是也没啥?

就像Robert Dober所说的,动态语言和静态语言在设计上存在不少差异,在Ruby中你永远不会使用is_a? 和 kind_of? api来检查类型,而是遵循Duck Typing 哲学(其核心思想是:一个对象是看它能做什么,而不是看它是什么),所以ruby中通常依赖respond_to? api来判定一个对象是否可以做某个操作。

在这个dup例子中,更好的实现方法是常量类不实现dup方法,而不是抛异常,但是这个还是违背了LSP原则。

因此,我们能做到既遵守LSP原则,同时又有丰富的接口的基础类和模块吗?
现实中有很多例子来回答上面的问题:有些特性可以,有的不行。如你可能会问题,Java中为什么每个class都要实现toString而不是toXML?(意思是假如Java中提供了和toString同等位置的toXML,相当于提供了更丰富的接口,但必定会违背LSP原则)。

从AOP(Aspect Oriented Programming)的角度来看,我更愿意看到的结构是dup仅在支持此特性的类中有,不支持此特性的类没有此方法。dup不应当是Kernel模块的基本特性,但是当需要使用它的时候必须是工作正常的。

实际上,在ruby的世界里实现这种AOP很容易,或许Kernel、Module、Object应当拆分为很小的块,然后根据实际的场景,用ruby中的混合(mixin)特性来组合他们。如下:

irb 1:0> my_obj.respond_to? :dup
=> false
irb 2:0> include 'DupableTrait'
irb 2:0> my_obj.respond_to? :dup
=> true
irb 4:0> def dup_if_possible items
irb 5:1> items.map {|item| item.respond_to?(:dup) ? item.dup : item}
irb 6:1> end
...

上面例子代码要表达的意思是:在Object类中不提供dup的抽象,而是在DupableTrait给每个可以支持dup的类加上dup方法(通过mixin),通过这种方法,可以解决违背LSP原则的问题,并且简化了Kernel中类和模块的实现。因此我们说ruby中违背了LSP原则,但是ruby也提供了灵活的方法让你遵循LSP原则,到底是遵循还是违背,要看实际应用场景。

原文连接Liskov Substitution Principle and the Ruby Core Libraries -- by Dean Wampler

转载请注明:运维派 » 浅谈ruby core library 与 Liskov Substitution Principle原则

0
2.6k
0