读书人

逾越边界: 延迟绑定

发布时间: 2012-08-26 16:48:05 作者: rapoo

跨越边界: 延迟绑定

简介:?静态类型语言(如 Java? 语言和 C)可以在编译时把方法调用绑定到其实现中。这项策略让这类语言可以执行更丰富的语法和类型检查。比起不具有此项编译时检查功能的动态类型语言来说,静态类型语言更加稳定且具有更佳的性能。然而静态类型语言存在一个严重的局限性:前期绑定。一些动态类型语言(如 Ruby、Smalltalk 和 Self)允许延迟绑定,它们可以实现另一个层次的编程功能。

?

?

几年前,我有幸教我的大女儿学滑雪。滑雪学校提供的工具里有一条绳子,用这条绳把雪橇的尖端绑在一起。利用这根绳,初学滑雪的人能够轻易地实现较为理想的滑雪动作,如转弯、减速和停止。最初,这些滑雪者十分依赖于这条绳子。我女儿还发誓说她离开这条绳就不滑雪。当然,她这样说是因为她刚刚开始学所以对整个过程不了解。这没关系。因为我知道将来她最终会在滑雪时迫使自己冲破这一束缚。

关于本系列

当今,非 Java 框架正在影响着 Java 框架的构建方式。您从其他语言学到的概念可以影响 Java 编程,您所编写的 Python(或 Ruby、Smalltalk 等语言)代码也可以改变编写 Java 代码的方式。因此,在?跨越边界?系列文章中,作者 Bruce Tate 提出这样一种观点:即通过学习其他框架方法和语言,Java 开发人员可以更好地武装自己!

本专栏介绍与 Java 开发完全不同的编程概念和技术,但这些概念和技术可以直接应用于 Java 开发。在某些情况下,需要集成这些技术来利用它们;但在其他情况下,则可以直接应用这些概念。在这里,单独的工具并不重要,重要的是那些其他语言和框架中能够影响 Java 社区开发人员、框架,甚至是基本方式的概念和思想。

作为一名 Java 开发人员,我也有过类似的经历。我喜爱静态类型提供的安全性。我曾和 Dave Thomas 就静态类型检查的优点这个问题进行过辩论,但我却不能被说服。如果没有最初的这条“安全之绳”,滑雪会是一种截然不同的体验。对于许多人来说,静态类型也是一种依赖,这和雪橇上的束带没有什么区别。Dave 认为我只是还没有足够的经历来理解动态语言的好处罢了。当我认识到 Ruby 和 Smalltalk 的妙处之后,我开始明白对动态类型我的确了解得不够,但这也加深了我对它的理解(参阅之前的?跨越边界?系列,获取对静态类型策略和动态类型策略的总体比较)。对我来说它最大的好处是延迟绑定。本文使用 Ruby、Smalltalk 和一个叫做 Self 的 Smalltalk 的派生语言的编程例子探讨了延迟绑定的好处。

后期绑定和前期绑定

编程语言能够将对函数(或在面向对象语言中的方法)的声明从其调用中分离出来。可以声明一个方法并使用单独的语法调用这个方法,但最终系统需要将这两者绑到一起。将调用和实现绑到一起的过程叫做绑定。前期先绑定到类型再绑定到实现,还是后期先绑定到类型再绑定到实现,这对一门给定语言的编程体验来说有着显著的影响。大多数面向对象的语言都在后期绑定到实现,从而允许多态性?,该功能让您能够将许多不同的子类型表示为一种类型。Java 代码和 C 主要在前期的一个编译的步骤里绑定到一种类型。使用此策略,编译器就有足够的信息可以捕获许多不同类型的 bug,比如说方法参数或返回值之间类型的不兼容。例如,清单 1 中的代码就会产生几个编译错误:


清单 1. 编译时绑定

连续区间

静态或动态只是连续区间中的点。一些语言高度静态。Java 语言比 C 或 C++ 更为动态。连续区间中的每个点都有一套自已的折衷方式。Java 语言有许多有助于延迟绑定的功能,这些功能都以相对较高的复杂度为代价。反射、依赖性注入以及 XML 配置都可用于延迟绑定和减少耦合。一直以来,Java 语言都是通过添加功能(如面向方面编程)来使其更为动态的。您也许会认为 Java 开发人员拥有了所需的一切。但还有一类语言 —— 如 Smalltalk、Self 和 Ruby —— 要比 Java 还要动态且允许用更佳的方式来延迟绑定。

这些语言可以提供 Java 语言所没有的技术,如覆盖在方法丢失时发生的行为。请记住,Java 语言需要存在用于编译时绑定的方法。其他语言允许打开的类,这些类能够基于开发人员需求进行改变。如果您曾长久关注框架的发展,就会发现对延迟绑定的需求在日益增长,这种需求导致了 Java 语言中出现了许多很不自然的捆绑,它们使这门语言变得复杂且模糊。而其他语言则持观望态度,等着我们去构建这类框架来实现更高的抽象级别以及更高的效率。对于您来说,好处很明显:可以获得一门更易表达且更高效的语言。

为了理解连续区间中的点,可以看一下反射的情况。使用 Java 语言,可以在运行时装载类,通过反射找到一个方法,为该方法验证正确的参数设置,然后执行该方法。要实现这些功能很可能需要编写很多代码行。但为延迟绑定所做出的这些努力常常会得不偿失,所以大多数 Java 应用程序开发人员不会使用此项技术。Ruby、Smalltalk 和 Self 都使用一种原操作(如 Ruby 中的object.send(method_name))来完成此功能。该技术改变着这些语言中编程的本质,这样的例子随处可见。

对类型策略和绑定策略越是深入研究就越会发现:等到运行时再绑定到调用或类型会根本性地改变编程的过程,从而开启一个全新的可能世界。没错,您会发现这样不那么安全。但您也会发现:重复少了、功能强大了并且在减少代码行的同时有了更大的灵活性。为了理解这一切是如何运行的,下面将快速介绍一下 Smalltalk、Self 和 Ruby。首先介绍延迟绑定调用方法,然后介绍一些可以在运行时改变类定义的可用技术。

?

延迟调用

在静态语言中,编译器在编译时直接将调用绑定到实现。动态语言则有些不同。Ruby、Smalltalk 和 Self 依赖于消息传送来延迟绑定。客户机使用消息传送来指定目标对象、消息和参数集。这完全是一个运行机制。所以动态语言有效地添加了一级间接寻址。它们将消息名绑定到一个对象上,而不是从调用绑定到类型再到实现。然后,将该对象绑定到一个名称或标记,并使用该名称或标记在运行时查找相关的实现。它是这样工作的:

  1. 客户机向目标对象发送一条消息。
  2. 该消息有一个名称和零个或多个参数。
  3. 该目标(可以是类或对象)查找是否有与此消息同名的方法。
  4. 如果有,目标对象调用该方法。
  5. 如果没有,目标对象向父对象发送一条消息。父对象可能是一个超类(Smalltalk)、一个父对象(Self)或一个模块(Ruby)。
  6. 如果在任何父对象中都没找到该方法,会调用一个错误捕捉方法。

上述所有步骤都发生在运行时?。这意味着在执行该消息的语句前,既不需要目标方法,也不需要实现。在 Smalltalk 中,一切皆是对象,且大多数行为都利用消息传送。甚至于控制结构都依赖它。Smalltalk 有三种消息:一元消息(无参数)、二元消息(带固定的参数集)和关键字消息(带已命名的参数)。例如:

  • 7 sin?将一元消息?sin?发送给目标对象?7
  • 3 + 4?是一条二元消息。它将带参数?4?的?+?消息发送给对象?3
  • array at: 1 put "value".?是一条关键字消息。该代码将消息?at: put:?发送到?array?对象,将?value?放置在数组中?1?的位置。
  • condition ifTrue: [doSomething] ifFalse: [doSomethingElse].?是一条关键字消息。括号中的代码叫做闭包代码块?。这个代码样例将?:ifTrue :ifFalse?消息发送到?condition?对象。如果条件为真,Smalltalk 执行?[doSomething]?代码块;否则执行?[doSomethingElse]?代码块。

    Ruby 支持消息传送和直接方法调用。Ruby 中的消息传送看起来有些许不同,但前提是一致的。在清单 2 中,定义了带?speak?方法和sleep?方法的?Dog?类。直接调用?speak?方法并通过?send?方法按?name?调用?sleep?方法。


    清单 2. 在 Ruby 中用两种方式调用方法?

    ?

    在运行时添加行为

    所有这三种语言(Self、Smalltalk 和 Ruby)都使在运行时添加行为变得十分简单。使用 Self 和 Smalltalk,对现有类所做的任何更改都是通过定义一个运行时修改来实现的。当添加一个方法时,也有效地修改了一个活动的类。在 Self 中添加或删除 slot 很简单:只需要发送?_add_slot?消息。类似地,在 Smalltalk 中,可以通过调用相应的消息(在一些地方称作?compile)来添加方法或属性。在这两种情况下,都可以直接在 image 中修改类的单个副本。接下来我要对 Ruby 中对类添加行为进行稍微深入一点的探讨。

    Ruby 框架常使用在运行时通过几种不同的机制修改类的技术。最简单的是打开类。可以打开任何的 Ruby 类,并通过重命名、添加或删除方法或属性来更改它。假设您想要在 Ruby 中扩展数字来简化柱状图的实现过程。您可以打开?Fixnum?类,并添加一个方法来打印出相应长度的柱,如清单 3 所示。


    清单 3. 扩展 Fixnum


    现状和超越

    在清单 4 的例子中,从内部扩展了?Dog。采用相同的技术,可以用相同的方式扩展任意的 Ruby 类。现在,延迟绑定全部的能量就凸现了出来。可以为一个普通的类扩展任意的功能并调用类的新行为,尽管当编写原始类时它们根本不存在。

    反射的功能也不能不提。Self、Ruby 和 Smalltalk 通常都进行反射。其消息传送功能允许不迫使用户访问物理的方法就调用方法,正如在 Java 语言中那样。class.methods?提供 Ruby 中一组方法名的数组,而?class methods?可以返回 Smalltalk 中的方法集。使用这些功能及类似的特性几乎可以找到在类或对象上进行快速自检所需的一切。

    到目前为止,我主要探讨了关于绑定到方法的内容,但延迟绑定的内容要多得多。看一下清单 5 中所示的 Ruby 的方法定义。


    清单 5. 添加两个数字


    用 Java 语言延迟绑定

    Java 社区对静态类型检查的迷恋程度令人惊讶,Java 程序员们正在不遗余力地寻找延迟绑定的方式。有些方法是成功的。诸如 Spring 等框架的存在主要是为了延迟绑定,它有助于减缓客户机和服务之间的耦合。面向方面的编程通过提供能够扩展类的功能(甚至可以超出其当前的功能)的服务来实现延迟绑定。像 Hibernate 这样的框架也可以延迟绑定,通过反射、代理和其他工具在运行时将持久性功能添加到纯粹、普通的 Java 对象(POJO)中。现在有很多关于如何用 POJO 编程的流行书籍可供开发人员参考,这些书籍大多会使用愈加复杂的技术(比反射还要先进),而这些技术主要是为了打开类并延迟绑定,从而有效地回避了静态类型。

    在其他地方,延迟绑定的方法就不那么成功。依赖于 XML 来延迟绑定的部署描述符有很多问题。对 XML 的过分依赖和我们对语言中的动态行为的强烈渴望有很大关系,因为这些语言常常有点太过静态,绑定得有点太早,并且有点太受限制。

    现在已经有一些语言和技术可以为 Java 程序员们极想解决的这几类问题提供解决方案,例如透明的持久性、为可测试性减少耦合、更加丰富的插件模型等。只要看看推动 Java 持久性框架发展的元程序设计,并同 Active Record、Gemstone 或 Og(动态语言中的持久性框架)中类似的解决方案对比一下,一切就一目了然了(参见?参考资料)。现在延迟绑定变得越来越重要,并且推动该过程的那些思想和做法在其它语言中也甚为高效。当您需要进行元程序设计时,请打开您的工具箱,加入几种允许延迟绑定的语言。不要害怕跨越边界!

    ?

    ?

读书人网 >编程

热点推荐