之前的文章中,我们讨论过OCP和LSP及他们在动态语言中的表现特征。分别参考支持Open Class特性的编程语言中的开闭原则(Open-Closed Principle),浅谈ruby core library 与 Liskov Substitution Principle原则。
今天我们要说的是LSP原则在Duck Typing语言中的表现。
Duck Typing(中文翻译为“鸭子类型”)是一个新名词,它是面向对象语言中动态类型(多态)的另外一种表达形式。我们知道传统的(强类型)的面向对象语言中,要确定某个对象有哪些方法和属性通常看它继承哪个类或实现哪个接口。而Duck Typing是通过这个对象的行为和属性来判定它大概是什么。Duck Typing最初的定义来自 duck test
当我看见一只鸟,它走路时候像鸭子,游泳时候像鸭子,嘎嘎叫的时候也像鸭子,我就称这只鸟为鸭子。
然后我们重新温故一下LSP原则的定义。
LSP原则:
如果S是T的子类,那么代码中所有用到T的地方,都可以通过S替代。这个原则是传统的强类型面向对象语言(如Java)必须遵守的一条原则。关于LSP原则的更多介绍,参考LSP wiki。
可以说LSP原则是为继承量身定做的,继承能够完美遵循此原则。与使用LSP原则来描述继承相比较,那个著名的is-a关系可以说不是足够严格。不过,另外一个behaves-as-a(行为看起来像)的关系定义更是不严格。我们注意到is-a是定义结构之间的关系,behaves-as-a(行为看起来像)是定义行为之间的关系。
大家注意到我有一个小小的假设:LSP定义了继承,为什么继承被具体定义但是替代没有呢?因为对大多数静态类型(强类型)语言来说,继承是替代的表达方式。
例如,Java中通常的抽象方式是定义一个接口:
public interface Tracing {
void trace(String message);
}
client会用Tracing定义的trace里实现纪录日志,client可以使用的类必须是实现Tracing接口:
public class TracerClient {
private Tracer tracer;
public TracerClient(Tracer tracer) {
this.tracer = tracer;
}
public void doWork() {
tracer.trace("in doWork():");
// ...
}
}
但是Duck Typing语言是另外一个实现替代(其实就是多态)的形式,这用语言通常是Ruby和Python。
当我看见一只鸟,它走路时候像鸭子,游泳时候像鸭子,嘎嘎叫的时候也像鸭子,我就称这只鸟为鸭子。
通俗地说,Duck Typing表达的是client可以使用任何对象只要该对象实现了client想要调用的方法。换句话说,该对象必须响应client发给它的消息。
只要client认为某对象是一只鸭子,它就是。
在我们的例子中,client只关心是否有trace方法,因此,使用Ruby可以这样实现:
class TracerClient
def initialize tracer
@tracer = tracer
end
def do_work
@tracer.trace "in do_work:"
# ...
end
end
class MyTracer
def trace message
p message
end
end
client = TracerClient.new(MyTracer.new)
这个例子中并没有定义接口(interface),只是传递一个对象给TracerClient的initialize方法,使得TracerClient能够响应client的trace消息。这里我给MyTracer加上了trace方法,当然你也可以给任何对象加上trace方法。
因此LSP原则还是这里的核心原则,其意义就体现在替换上,只是Duck Typing不是用继承表达替换而已。
那么Duck Typing到底是好还是坏呢?现在有很多关于动态类型(弱类型)语言和静态类型(强类型)语言的讨论,我在这儿不断言谁好谁坏,只是发表我的观点。
Duck Typing不好的一面是,由于没有了Tracer的抽象,我们只能根据对象方法的名字来揣测此方法能做什么。同时,也不能找出全部提供trace方法的类。
从另外一面来说,clien实际并不关心Tracer的类型,而是只关心trace方法。这样实际上将client和server做了一点解耦。
我们使用Scala来作比较,看Scala中是如何实现替换的。Scala是一种静态类型语言,不支持Duck Typing,但是它提供了一种很类似的机制被称作结构类型。结构类型的核心思想是使得程序员可以申明一个方法的参数,此参数接收指定的函数。与Java中的匿名接口实现类有点像。
在我们的Java例子代码中,在Scala中可以做到仅实现trace方法,而不用实现接口中的所有方法。
public class TracerClient {
public TracerClient(Tracer tracer) { ... }
// ...
}
}
上面的接口在Scala中,可以这么实现:
class ScalaTracerClient(val tracer: { def trace(message:String) }) {
def doWork() = { tracer.trace("doWork") }
}
class ScalaTracer() {
def trace(message: String) = { println("Scala: "+message) }
}
object TestScalaTracerClient {
def main() {
val client = new ScalaTracerClient(new ScalaTracer())
client.doWork();
}
}
TestScalaTracerClient.main()
从代码中可以看出,ScalaTracerClient的构造函数,接收一个类型是{ def trace(message:String) }的trace参数。client对server的要求就是响应trace消息。
因此我们就做成了Duck Typing的行为,且是静态类型的。如果在构造ScalaTracerClient传递的是一个不支持trace函数的对象,则在编译时候会报错,而不是运行时。
将上面所说的总结一下,LSP原则在Duck Typing语言中可以稍加修正地表述为:
如果S与T有相同的行为,那么在代码中所有依赖T的行为的地方,都可以使用T的行为替代。
我们将子类替换为了行为。
最后要说的是,client和server之间的合约还是十分重要,无论是在Duck Typing语言还是Structer Typing语言。当然,Duck Typing语言提供给我们一种新的扩展系统现有功能的方法。
原文来自:The Liskov Substitution Principle for “Duck-Typed” Languages
本文链接:http://www.yunweipai.com/336.html
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/53125.html