引言
動態類型語言在企業開發和互聯網領域應用廣泛,如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),程序員可以有選擇的忽略。
測試
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 buyingProducts
$buyingProducts.add( $customer)
編譯錯誤如下:
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 buyingProducts
##$ String flag, sss, abc
對於其他動態類型語言,同樣使用“基於注釋的擴展聲明指令”來實現兼容性的半靜態語言。
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結合,實現代碼提示,代碼重構等敏捷開發功能,有效提升動態語言的開發效率。在企業級應用和互聯網應用開發中有著良好的應用價值。