【摘要】动态类型语言在企业开发和互联网开发中应用广泛,而其弱类型的内在特点使其在这些业务复杂的应用开发中存在很多缺点:无法静态检查,程序不健壮,测试成本高;缺乏一些敏捷开发功能如 IDE 内实时验证、代码提示、代码重构等。为此,本文提出半静态语言,它的基本原理是两阶段模型,开发时运用变量类型声明进行类型检查,运行时采用解释执行的方式。并引入“基于注释的扩展声明指令”,与现有解释器保持完全兼容。 半静态语言它结合了动态语言和静态语言的优点,同时满足企业开发中的灵活性、健壮性与敏捷开发的需求。
【关键词】Semi-static Language, Dynamic Typing, Static Typing, Velocity
引言
动态类型语言在企业开发和互联网领域应用广泛,如 Ruby ,Velocity, Python 等。 动态类型语言在运行时进行类型推断,以解释方式执行,修改即生效,开发灵活性高;而静态类型语言 (如:Java,C/C+/C++) 在执行前做类型检查,需要编译运行,对于互联网前端开发不够灵活。
因此,许多大型互联网站选择 Freemarker, Velocity 这样的动态模板语言作为页面开发语言,在一定程度上满足了前端敏捷开发的需求。
然而,对于大型电子商务网站,不仅具有一般互联网需求频繁变更的特点,更显著特点则是业务繁多,业务模型和业务关系复杂。 因此,在此类应用开发中,Velocity 的开发也遇到了一些的问题。
前端模板开发问题
-
降低软件质量
Velocity 是弱类型动态语言,运行时才能检查出类型错误。由于动态类型等特点,有的错误在遇到特定参数时,才能激发执行路径,软件质量不能很好的保证。 -
测试成本高
由于无法像静态语言一样,在运行前进行类型检查,因此软件的测试周期长,测试成本高。 -
开发不敏捷
缺乏一些敏捷开发功能如 IDE 内实时验证、代码提示、代码重构等。虽然能修改即生效,但对于企业级开发,效率较低。 -
维护性差
对于一个大型系统,在重构业务模型(Java Model)或代码时, 无法知道哪些 Velocity 模板会受到影响;常常需要花费大量时间搜索相关模板,然后修改、测试。例如:笔者所在公司的一个基础产品升级,由于受影响模板众多,重构复杂,项目评估达上千人日。
这些动态语言天生的缺点在企业级和大型网站应用中非常突出,严重的影响了开发质量和开发效率。因此,在技术上亟待一种新的高质量、高效率的开发技术。
静态语言的优势
综合考虑后,我们发现动态类型语言(Dynamic Language)“解释执行方式和修改即生效”的最大优点仍是不能舍弃的。必须从问题出发,找到一条平滑的线路来解决问题。
遇到上述问题时,我们不由自主的会赞美 Java 的优点:
-
静态语法和静态类型实时检查。
如果赋值类型不匹配,方法不存在,参数类型错误等信息能马上在 IDE 中显示; -
代码提示:
调用属性,方法时能代码提示,开发非常高效; -
代码热链接:
通过变量和类名热链接到对应的 Java 类; -
代码重构:
修改一个 Java 类时,受影响的 Java 代码会被实时重新验证,马上会显示红色的错误; 更强大的是重构,对 Java 类,方法敏性重命名,会自动修改所有相关代码中对它的引用。
Java 等静态类型语言的这些优势就是解决问题的方向。那为什么动态语言不能做到这些呢? 原因在于动态语言的根本特点是变量无类型(即弱类型特点),类型在运行时推断,这使得它无法在开发阶段进行类型检查。
那如何将动态语言和静态语言的优点结合呢?答案就是半静态语言。
半静态语言(Semi-Static Language)
4.1 定义
半静态语言,严格说应该是静态化类型的动态语言(Statically Typed Dynamic Language)。它是这样一种语言:以静态方式开发,以解释方式执行;通过变量显式声明或隐式声明,运行前可对变量类型进行推断和验证。
静态语言,动态语言和半静态语言的特点对比分析如下:
语言类型
优点
缺点
举例
适用场景
Static Language
强类型,运行前类型检查,程序健壮
对 Java 等支持反射的语言,可实现代码提示,重构等敏捷开发特性
需编译运行,发布慢
无法快速响应需求变化
Java
C/C++
企业级后端开发
大型互联网后端开发
Dynamic Language
灵活性高,修改即生效
快速响应需求变化
弱类型,运行时类型检查,程序不健壮,测试成本高
PHP
Ruby
Velocity
业务简单的小型互联网前端开发
Semi-Static Laguange
开发时 (Devtime) 强类型,程序健壮
运行时 (Runtime) 弱类型,修改即生效,快速响应需求变化
业务复杂的企业级开发和大型互联网前端开发
半静态语言集合了静态语言和动态语言的优点,更适合企业级和大型互联网开发,例如:电子商务,ERP,金融,保险等。
4.2 技术原理
4.2.1 范例
为了实现目标,需要在动态类型语言基础上,引入变量声明技术。因此本质上,半静态语言也是一种声明式语言(Declarative Language), 这一点与静态类型语言一样。
以 Velocity 模板语言为例:
当前 Velocity Template 编程代码范例如下:
[Code 1] showBuyProducts.vm
<HTML>> Hello $customer.Name <table>> #foreach( $product in $buyingProducts ) Buy: $product.Name, Price: $product.Price, #end </table>>
该模板执行后,HTML 页面上将用 $customer.Name 显示“客户名称”,循环显示该客户购买的每个产品的名称和价格。在 Velocity 中,运行时通过 Velocity Context 传递变量 $customer 和 $buyingProducts,而开发时这两个变量是未定型的(Untyped,或者说都是 Object 类型)。
为了实现静态化开发,引入变量声明,在模板顶部对变量 $customer,$buyingProducts 进行显式类型声明。变量声明指令为“##$”。
格式为:
##$ <Type> <var1[,var2[,[…]]]>
带有变量声明的半静态模板代码为:
[Code 2] showBuyProducts_static.vm
##$ com.abc.crm.Customer customer ##$ com.abc.saling.Product product ##$ List<Product> buyingProducts <HTML> Hello $customer.Name <table> #foreach( $product in $buyingProducts ) Buy: $product.Name, Price: $product.Price, #end </table>
上述代码中,指定了变量 customer 的类型为 com.abc.crm.Customer,变量 buyingProducts 的类型为 Product 泛型集合。由于 "##"是 Velocity 的注释指令,因此 “##$” 在 Velocity Engine 解析(Parse)和渲染(Render)时不会与现有语法冲突,Velocity 引擎能正常执行,从而保证了兼容性。
4.2.2 动态语言一阶段模型
在动态类型语言中,只有一个运行时(Run Time)阶段,运行阶段由解释器(Intepreter)来对源代码进行解析(Parsing)、执行(Evaluation)产生执行结果。过程如下:
由于动态语言无类型的特点,在解析步骤中产生的抽象语法树(Abstract Syntax Tree,AST)所有变量被存储为统一的类型,例如 JavaScript,Velocity 中变量都作为 Object 类型。在执行步骤,一般由类型推断系统(Type Inference System)负责根据变量的实际值动态判断变量的类型,并判断函数、方法或属性调用是否正确,由解释器进行执行或计算,从而产生结果。
4.2.3 半静态语言两阶段模型
而半静态语言,分开发时(Develop Time)和运行时(Run Time)两个阶段,两个阶段互不干扰。
- 开发时阶段。
开发时进行类型检查。一个“编译器”,更严格说是类型化解析器(Typing Parser)负责对源代码进行解析和类型检查,然后输出检查结果。“变量声明”是类型检查的必要条件。检查结果包含类型检查失败的错误信息和警告信息,类似于 Java 编译时的错误信息。
与静态类型语言不同,此编译器不输出机器代码或字节码,只输出类型检查错误信息。
- 运行时阶段。
此阶段中,源代码仍由解释器以解释方式执行,同动态语言的解释执行过程。
半静态语言的两阶段模型如下图所示:
需要指出的是,运行时阶段仍采用无类型解析器(Untyping Parser), 是一个类型推断系统。而开发时采用的是一个新的类型化解析器 (Typing Parser), 是一个类型检查系统 (Type Checking System)。
4.2.4 开发流程
半静态语言的开发流程涉及 5 个步骤:
- 编码
- 编译 (类型检查).
半静态语言的编译与静态类型语言很不相同,它的编译只进行类型检查,不产生机器码或字节码。因此,半静态语言的编译可以称为“检查”(Checking).
在这个步骤中,如果代码存在类型错误(Error),编译失败,那么你必须退回到步骤 1)修改代码 bug,直到代码编译正确。
编译过程还可以产生警告(Warning),程序员可以有选择的忽略。 6. 测试
QA 执行功能测试,集成测试和系统测试。
如果测试失败,必须退回到步骤 1)。
-
发布
将代码发布到生产环境 -
执行
最终用户访问用半静态语言开发的应用功能。
从上面的开发流程可见, 开发时阶段覆盖了步骤 1)、2), 运行时阶段覆盖了步骤 3)、4)、5).
为了保证只有编译合法的半静态语言程序在生产环境运行,需要有以下两条约束规则来保证:
- 代码编译合法后,才能提交到测试阶段;
- 测试正确的代码才能发布上线。
由于半静态语言仍用解析器运行,理论上代码仍具有修改即生效的特点。但从软件质量保证角度,这个缺点应该规避。因此上线后的代码不允许未经编译、测试的随意修改。
4.2.5 类型检查系统和原理
半静态语言的类型检查系统中的核心组件编译器 Compiler(或称为 Checker),它本质上是一个类型化解析器。编译时,该系统采用类型检查算法(Type Checking Algorithm);而在运行时阶段,仍由解释器执行代码,采用类型推断算法(Type Inference Algorithm)。
半静态语言的类型检查基本原理是,根据变量声明对源码进行解析、类型检查和语义检查,输出检查结果。这个系统中类型检查系统的基本原理如下图所示:
我们使用一个命令行工具 vmcheck 来编译半静态语言代码。格式为:
Format: vmcheck templateFile
以前面的声明式 Velocity 源码为例,类型检查系统包含以下几个基本规则和检查点:
- 变量是否声明;
如果变量 $customer 未声明,编译错误如下:
Error: line:2,column:7,variable $customer not declared !
- JavaBean 的属性和方法是否存在
如果 com.alibaba.saling.Customer 类没有属性 ‘Name’ , 编译错误如下:
Error: line:2, column:7, property 'Name' not found for $customer.
如果 com.alibaba.utils.CurrencyUtil 类没有方法 ‘convert’ , 编译错误如下:
Error: line:6, column:22, method 'convert' not found for $currencyUtil.
- 方法调用的参数匹配 ;
3.1) 如果这样调用 ‘convert’ 方法:
$currencyUtil.convert()
则产生如下编译错误信息:
Error: line:6, column:22, insufficient parameters for method call 'convert' .
3.2) 如果这样调用 ‘convert’ 方法
$currencyUtil.convert( $customer , "##.##" )
则产生编译错误信息:
Error: line:6, column:22, parameter type mismatched of $customer for method call 'convert' , Double is required.
- 特定语句的类型匹配,如条件,循环语句:
如果有下面的复制语句调用
#set( $customer.Name = $product.Price)
则产生编译错误信息:
Error: line:11, column:5, type mismatched of assignment statement.
‘if’, ‘foreach’ 等语句使用的类型匹配规则类似。这与 Java 等强类型语言一样。
- 集合泛型的类型匹配
对于 Java 语言,JDK5+ 支持泛型特性。因此,类型检查也需支持泛型。对于以下代码
##$ List<product> buyingProducts<br></br>$buyingProducts.add( $customer) <br></br></product>
编译错误如下:
Error: line:12, column:5, parameter type mismatched of $buyingProducts for method call 'add' , 'com.alibaba.saling.Product' is required.
As for the previous Velocity code snippet [Code 1], after executing 'vcheck’ command on console,
4.3 变量声明
变量声明就是对变量的类型进行声明。变量声明根据放置的地点分为两种,显示声明(Explicit Declaration)和隐式声明(Implicit Declaration)。
- 显式声明
显式声明采用特殊指令(Directive)或语句(Statement),在源码中对变量进行类型声明。
显式声明通常的格式为:
<Declaration Directive> <Type> <varList>
为了保持与运行时解释器的兼容性,我们引入一种“基于注释的扩展声明指令”技术。以 Velocity 模板语言(VTL)为例, 在 Velocity 注释指令“##”上扩展“##$”指令用于变量声明。如下例所示:
[Code 3] showBuyProducts_static.vm
##$ com.abc.crm.Customer customer ##$ List<product> buyingProducts<br></br>##$ String flag, sss, abc<br></br></product>
对于其他动态类型语言,同样使用“基于注释的扩展声明指令”来实现兼容性的半静态语言。
Language
Comment Instruction
S2L Declaration Instruction
Velocity
##
##$
Javascript
//
//$
Ruby
#
#$
Python
#
#$
- 隐式声明
隐式声明不用在源码中编写声明语句,而从配置文件或其他地方分析变量声明。例如,使用 Velocity 进行 Web App 开发时,如果需要直接频繁操作 request,response,session 等 Servlet 容器对象,编译器可以将它们作为内置变量,使用隐式声明。如下表所示:
Built-in variable
Type
request
HttpServletRequest
response
HttpServletResponse
session
HttpSession
application
ServletContext
以下代码使用隐式声明变量 request, session,
<html> <body> Hello, $request.getParameter("username") ! <p/> Your logged in at $session.getAttribute("loginTime") last time. </body> </html>
这段代码看起来,对现有 Velocity 语法没有任何扩展. 但实际上,在编译时,编译器使用内置变量对源码进行类型检查。
如果编写了一段错误的调用,例如:
$session.getParameter("loginTime")
则编译器输出一条“方法不存在的”错误信息:
Error: line:12, column:5, method 'getParameter' not found for $session!.
4.4 语法约束
半静态语言基于某种动态类型语言进行实现,但它在语法语义上更接近与静态类型语言。在这两个端点,存在一些矛盾的地方,比如:变量动态定型,ducking type 等。因此,半静态语言需要有语法约束:
- 变量先声明,后使用
- 变量在作用域 scope 内置能声明为一个类型;
- 禁止 Ducking type 也就是说, 动态语言的无继承多态特性不允许使用,因为这与静态类型系统是冲突的。
如果违反这几个规则,编译器会产生相应的编译错误。以 ducking type 为例(Ruby 支持,而 Velocity 等 Java 系列脚本不支持), 如果尝试访问一个不存在的方法,则会产生下面的错误。
Error: line:12, column:5, method 'quack' not found for $dog.
而在 Ruby 中,只要 dog 存在 quack 方法,代码运行是正确的。
4.5 半静态语言组成模型和实现方式
半静态语言本质上是动态语言思想和静态语言思想的结合的产物。一种基本的半静态语言实现,核心功能是在运行前进行类型检查和语义检查。其组件集合 SS 包括:
- 一种静态类型语言 S,S 以编译方式运行;
- 一种以 S 语言为基础的动态类型语言 D。D 以解释方式由 P 执行,解释器 P 由 S 编写;
- 在语言 D 的语法集合上扩展变量声明语法,新语法集合名为 SD ;
- 用语言 S 对解释器 P 进行扩展,实现 SD 的类型编译器 C;
- 开发时,遵循 SD 语法集合的代码由 C 进行类型检查;
- 运行时,遵循 SD 语法集合的代码由 P 进行解释执行。
因此,新的半静态语言 SS 是基本组成是:新语法集合 SD 和类型编译器 C.
SS = SD + C
举例:
Java 是一种静态类型语言,运行前进行编译和类型检查;
Velocity 是一种基于 Java 的动态模板语言,通过 Velocity Engine 以解释方式运行;
基于 Velocity 实现半静态语言的方式为:为 Velocity 基本语法增加变量声明指令(语句),基于 Velocity 解释器 实现类型编译器 ,负责在开发时对模板进行类型检查。
实践中,Java 体系的动态类型语言一般与 Java 语言天生的结合使用,应用广泛。以它们为基础,很容易通过扩展方式实现类型编译器,进而实现半静态语言。 例如 Freemarker,Groovy,JRuby,Bean Shell 等。其他动态类型语言也可以基于此原理设计半静态语言,如:Python,Ruby。
IDE 敏捷开发(Agile Development in IDE)
对于 Velocity,Freemarker 这类动态类型语言,它们基于 Java 等强类型语言,在模板内能直接操作传入的 Java 对象。由于 Java 等语言有反射(Reflection)机制。因而,除了静态类型检查的基本功能,可以在类型检查和反射技术基础上,实现一系列 IDE 敏捷开发功能。包括:
- 代码提示:编辑器内的 Java 对象的属性,方法代码提示;
- 参数提示:编辑器内的 Java 对象的方法参数提示;
- 全量构建和增量构建:Java 类修改对相关 Velocity 模板的增量检查;
- 代码重构:修改 Java 属性或方法名称,自动批量修改相关模板中所有对应类型的 JavaBean 属性或方法名称。
其中 3),4) 功能对于大型系统的维护和重构价值尤为明显。以上这些敏捷开发功能可独立实现或结合集成开发环境(IDE)如 Eclipse 插件来实现。
结论
通过以上分析可见,半静态化语言结合了静态语言和动态语言的优点,能很好的解决动态语言编程的开发质量和开发效率问题。半静态化语言保留了动态语言的灵活性优点,同时达到了静态语言在开发时强类型检查优势,能有效提升程序健壮性,减低测试复杂性和测试成本。通过与 IDE 结合,实现代码提示,代码重构等敏捷开发功能,有效提升动态语言的开发效率。在企业级应用和互联网应用开发中有着良好的应用价值。
参考资料
[1] Gordon Plotkin. A Semantics for Static Type Inference. (1992). http://homepages.inf.ed.ac.uk/gdp/publications/Stat_Type_Inf.pdf
[2] Velocity Template Language Reference. Apache Software Foundation. (2010)
[3] Michael Furr. Static Type Inference for Ruby. (2009 ) http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.144.5525&rep=rep1&type=pdf
[4] http://en.wikipedia.org/wiki/Duck_typing
关于作者
何坤,Raymond He,阿里巴巴技术部中文站架构师。多年 JavaEE 领域开发经验, 喜欢钻研崇尚创新。专注敏捷 Web 框架设计,敏捷 Java 开发技术和分布式 Java 系统。2009-2010 作为 Webx2.5 项目架构师,推进中文站 Web 框架敏捷化升级、开发模式敏捷化等工作,通过一系列技术创新,将中文站开发效率每人每天平均节省 30-90 分钟。目前是阿里巴巴中文站开发效率领域负责人,正在研究半静态语言及其在大型网站的应用。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论