你在同一个项目中会用到多少种语言?如果算一算的话,会发现数量真的不少。我指的是 XML、Java、XSLT、HTML 和 CSS 等等。但是,你为什么会用到它们,原因无非在于它们就是主流,更何况,它们可能是某个必需框架下的唯一可选的语言。实际上,你几乎是被迫使用这些语言的。所有选择都已经替你完成了。样式?CSS。配置?通常是 XML。Web 界面描述?Html。然而,如果你想真正采用多语言编程,就不可避免地要从众多语言中做出选择。
我想如果一个人想要做出正确的选择,就必须时刻牢记,采用多语言编程的主要目的在于能够选择正确的语言解决手头的业务问题。于是,现在的问题就在于如何为给定的领域及子领域选择正确的语言?
首先,理解现有语言的特性是至关重要的(包括与生产环境相关的因素以及在企业级项目中各种限制语言选择的因素)。理解语言的特性并不仅仅意味着理解它的优点和缺点,更重要的是,要理解语言是如何对世界进行描述(建模)的。
理解现有的程序设计语言
我们可以这样看待程序设计语言,它是一个有限的词汇与规则的集合,那些词汇与规则可以组合起来描述一个特定的问题。语言提供的结构和概念,在很大程度上决定了我们描述特定领域问题的能力。这意味说,程序设计语言中非常重要的两个方面包括:1)现有的词语和规则集合,2)已存在的组合规则。
编程语言的范型
一个连贯完整的词汇集以及组合规则,可以产生一种范型,从而定义出很大一部分的语言特质(或者是在某种多范型程序设计语言中的一个子集语言的特质)。
在思考现有的语言时,范型是最重要的特性。真正关键的是使用恰当的范型生成简洁的、可读性强的代码。使用正确的范型,有助于保持问题域和软件模型之间的一致性,由此能够创建出更清晰的模型,产生更高的可读性。使用了错误的范型则恰恰相反,它通常不可避免地会产生大量的 ad-hoc 代码,代码数量会爆炸式地增长(大量代码完成一个相对直接的任务),并出现 hack 行为。
编程语言范型示例
让我们以 Xml 为例。Xml 可以这样来描述:它是一些元素的集合,其中每个元素还可以包含属性(或者标签)。元素中还可以嵌套其他元素,这样一个元素可以包含其他元素,属性可以设置到每个元素上。这可能表现了 XML 这种语言最重要的特质——组合式的本质。简而言之,这些语言范型的信息已经足以应付我们稍后在领域分析阶段中的所需(cf. infra)。到此为止,我们已经了解到 Xml 拥有一个内嵌式的结构;它的构造包含了最高地位(first class)的元素,元素又包含了地位其次的属性。
再来看看 Lua。它的构建是基于表(table)的。Lua 可以描述为最高地位的键 / 值表包含其他的表(table)。因此,很明显,它的样子看上去类似于表格,我们暂且称其为表格式(tabular)范型。它同样揭露了足够多关于范型的信息,至少足够帮我们应付(对领域问题)最初的快速分析。
我们可以使用相同的方式来考察其他语言。Lisp 的主要数据结构是列表(List),使用 Lisp 编程可以想象为列表操作,这也顺理成章地成为了该语言底层的范型。Lisp 同时还是一种函数式编程语言,其最高地位的函数可以组合起来,生成程序其他的部分,这反映出它的函数式编程范型。Haskell 是另一种函数式程序设计语言,但它同时还带着浓浓的数学特质。这种强类型的语言拥有广为人知的强大(数据)类型,这是一个重要的抽象工具,它使得 Haskell 成为了一种兼顾安全与强大抽象能力的语言。Prolog 主要是由布尔谓词和断言组成的。正像它的名字暗示的那样,Prolog 可以看作是一种基于逻辑的程序设计语言。
其他需要考虑的语言特质
在你为某个特定子领域选择语言时,除了前面提到的范型,还需要考虑其他一些特质。类型种类(动态 / 静态),句法和其他一些重要性低于范型的因素。这篇文章并不打算更进一步介绍这些特性的细节。我的建议是不要高估它们的重要性,把更多的注意力放在范型上,它才会在子领域的编码表现力上扮演真正重要的角色。
发现与定义不同子领域的差异性
当考虑多语言编程时,我们大多可能已经做出了决定,对于眼前项目的领域而言,有几种语言可以使用。通常,这样的决定来自于两件事:明确知晓某些语言更加适合某些种类的问题,另外,注意到给定的领域是由多个可能具有不同特质的部分组成的。
因此,一个问题域可以被分为多个部分(子领域)。这种划分立刻就能明显体现出的各部分的性质差异,令子领域之间的区隔清晰地显露出来。但是,这些子领域之间仍然存在交互,在实现交互的时候,不应该忽略它们特质上的不同。
一旦将问题领域正确地分为了多个子领域,接下来寻找与领域一致的程序设计语言的任务就很容易了。领域问题通常都非常繁杂,很难从一个角度窥视其全貌。通过分拆问题,我们有机会使用不同的工具来解决不同的问题,因为正确的工具支持更好的抽象和设计。
然而要想识别出子领域和它们的特质并不容易。它需要我们具备分析能力以及对目标领域的全盘掌握。在这个过程中,为了避免得出关于子领域特质的错误结论来,应该和领域专家一起紧密协作。这项工作的主要内容是识别出各模块的属性和它们之间的交互行为,并将交互的模式和已有的一些概念(可以将它们描述为范型或者子领域的特征)进行抽象。有很多技术和实践能够帮助我们学习和发现领域知识与特质,比如 Eric Evan 的著作“领域驱动设计”中提及的 Domain Crunching,以及 James Coplien 的多范型(Multi-Paradigm)设计等等。
时刻在头脑中牢记各种设计语言范型,这的确有助于我们将领域概念规范化,以和建模的构建相匹配;但是,不能矫枉过正,以至于在分析问题时掺杂过多的外力,刻意改变领域概念以让它适合首选的范型。在 OOP 中,如果领域对象都默认被当成一个对象,这种现象就非常普遍了。
为子领域选择正确的语言 / 范型
识别出每个子领域以后,就可以为它们挑选正确的程序设计语言了。什么是“正确的语言”?简而言之,就是可以用它以最流畅的方式编写子领域描述的问题。编写程序的时候,我们通常会受到语言及其概念的影响。某种语言在解决特定的子领域问题时可能非常笨拙,且代码会令人费解;但如果选择了合适范型的语言,就能利用子领域与程序设计语言概念之间的一致性,产生更简洁与更具备扩展性的代码。选择范型匹配的语言的重要优势之一就是“可扩展性”。领域自身不会突然改变它的特质,即使它会不断地发展。因此,完全可能沿着同一种范型的路线一直走下去。如果语言的特质与领域的范型匹配,那么可以断定,在领域发展的过程中,这将会减少变化所产生的开销,并控制的变化的范围。
案例:子领域 - 程序设计语言范型的一致性
图形用户界面
接下来看一看图形用户界面。当我们观察一个界面时,所见的是层层嵌套的面板。每个面板内部又包含很多图形组件(按钮、图形、构建块等),后者又可以包含其他的控件,控件还可以包含其他控件。通过这样简单的分析,我们认识到图形用户界面具有嵌套的特质。一件事物可以嵌套在另一个事物中。同时任何组件又都具有属性。有了这些事实基础后可以看出,XML 似乎是开发 GUI 的完美语言。
然而,我想我们的结论来得有些急躁。我觉得有很大一部分 GUI 都可以用 XML 来表现,但未必是所有事情。WPF 和 XAML 是很好的研究案例。XAML 采用了基于 XML 的语法,正如我们在前面所说的,这是为了适应 GUI 所具有的嵌套包含的特质。但是,也有一些工作使用的不是基于 XML 的特殊语法,比如元素绑定。还有很多图形界面描述的子领域是可以得益于使用比 XML 更适合的语言。我们以样式为例。使用 XML 编写样式会带来大量的干扰,因为 xml 的开闭标签并不能为这个子领域表达任何有意义的信息。寻找表现样式的更佳方法应该认真地考虑它的特质。但是,图形样式描述究竟有什么特质呢?GUI 的样式无非就是一个属性集,要求能通过一个特定的名称找到对应的值。更进一步,样式并不关心它的属性值被用在哪里;也许是一个组件,或者一个子组件,或者粒度更小的东西。样式仅仅是对属性的描述。如果组件拥有某个属性,就会考虑使用该属性值。否则可以把它们忽略,也完全没有任何影响。通过这些简单的分析,我们就能明白,为什么 CSS 为图形样式这一子领域提供了极佳的句法选择。
与流和数据有关的流程处理
想象我们将要处理一个由动作(action)、信息或者数据构成的流。基本上这样的流都是无止境的,它们经过修改、处理,产生另一个序列——基于这个流的另一个动作、信息或者数据流。它的实例包括 RSS feed、http 请求 / 响应,网络信号……还有很多与领域相关的流。这些例子当然都具有面向流的范型,毕竟是为了举例而特别找的;在实际工作中,经过充足的领域分析后,勾画出这种特性并不难。选择具有同样特质的技术,有助于降低代码的复杂度,并提高代码的可读性及质量。否则,你会看到自己在代码中写了大量的嵌套循环和“while”块,这通常是模块化被削弱的起点。
通常,函数式语言(比如 Haskell)有种有趣的特质,可以处理无穷的列表和流。对于面向流的范型而言,函数可以串连在一起,组成一个显式的工作流,这样可以产生一个更具模块化的方案。
更多关于范型与子领域相匹配的实例
这样的例子还有很多,领域与技术方案的范型相匹配,这在选择工具的时候非常关键。如果某个领域中包含了大量的规则,那么校验谓词可以从面向谓词的或者逻辑式语言中受益匪浅。比如 Prolog 和 Haskell 就是这类语言。像 Erlang 这样的并发程序设计语言最适合基于并发的子领域。而涉及大量字符串处理的子领域可以使用具有强大的字符串处理范型的语言。
多范型程序设计语言
有些程序设计语言并不只提供单一的范型,而是混合了多个可供选择的范型。尽管它看上去可能改善范型匹配的程度,但是并不能从根本上取代多语言编程。这两种 “多范型设计”的方法分别有其优点和缺点。举例来说,对于单一范型的语言,其语法已经被最大限度地优化,以表现它关注的范型。另一方面,对于多范型语言来说,在设计语言自身的同时就考虑到了将若干种范型进行整合。这既可能是优势——我们可以更容易地享受多种范型,也可能是劣势——缺少直观的表现力。多范型程序设计语言会变得越来越复杂,不过其中也有一些由于成为主流而从中收益颇丰,比如 C#和 C++。
在从这两种技术中做出选择时,需要考虑这些以及其他各种权衡条件。由于生产环境的限制,目前并非能够真正做到使用多种语言(不过这个事实正在一点点地被瓦解,我们已经看到在有些主流的平台上实现了很多有趣的语言),那么多范型语言可能是一个很好的选择。团队成员的技能也可能成为一个受到限制的因素(不过有很多人,包括我,并不同意这样的观点)。
结论
虽然上述的一些原则可以激发使用多语言编程的动机,比如根据软件架构分层来选择语言,但是,这些方法过于简单,而且并不能非常直接地、有效地解决问题,即哪些项目需要使用多于一个的程序设计语言?多语言设计的核心在于透彻的领域分析和多范型设计。而保持程序设计语言范型和子领域之间特质的匹配,其结果是编写出更具有可读性的、简洁的、容易更新的代码,这样的代码不会有过多的干扰。最大化多语言编程效力的第一步,是要理解并识别出程序设计语言的范型。而在这个项目开发的过程中,“领域驱动设计”和“多范型设计”两项技术会一直伴你左右。
参考资料
James O. Coplien, Multi Paradigm Design
Eric Evans, Domain Driven Design
关于作者
Sadek Drobi 是一名软件工程师,他关注企业及应用程序的设计与实现。他对各种能够弥合业务和开发人员之间隔阂的解决方案(例如敏捷、DSL、领域驱动设计)怀有浓厚的兴趣。他目前的研究关注于面向语言编程和多范型设计。Sadek 是来自 Valtech Consulting 的咨询师。他还是一位相当专业的摄影家。他的技术博客是 www.sadekdrobi.com ,并在 http://photos.sadekdrobi.com 维护了一个相册。
阅读英文原文: Paradigm based Polyglot Programming 。
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论