Javascript超程式設計之Annotation

chemingjun發表於2018-01-30

語言的自由度

自由度這個概念在不同領域有不同的定義,我們借鑑數學中構成一個空間的維數來表達其自由度的做法,在此指的是:解決同一個問題彼此不相關的設計方法學數量。

例如,解決一個比如商品打折的問題,如何設計順序、提取函式,具體的思路可能有很多,但是這可能都是從程式導向(OP)的角度,同樣解決這個問題,如果另一門語言還支援物件導向(OO)的設計方法,那麼我們認為後者的自由度要多一些,因為OO提供了幾乎完全從另一個角度解決問題的能力。

既然自由度可以借鑑“維數”的定義,我們來嘗試分析一下計算機語言的“維數”,在此之前,我們有必要簡單分析一下語言是怎樣一步一步變得複雜的。

本文關注的重點是命令式風格的計算機語言。

第一步,出現了結構體(資料結構)、常量、變數、算符、順序、分支、迴圈等這些體現“命令”的基本方面;

第二步,例程的出現,包括函式、過程等;

第三步,巨集的出現,包括巨集、模板、泛型;

第四步,對客觀世界在結構化上抽象能力出現,包括OO等;

第五步,超程式設計能力的出現,如註釋、反射等等; …

從計算機語言歷史來看,以上步驟不一定按照時間順序展開,我們更關注的是語言能力提升帶來的意義。其中,第二步的完成,標誌著結構化程式設計方法的出現,對大型軟體工程提供了較好的支援,第三步是對第二步的進一步抽象,第四步所代表的意義更加重大,其中非常重要的一點,意味著終於可以支援實現“層次化”,可以實現將“核心”與“外圍”做分離,將相對穩定與潛在變化的部分分開,也就是說,編碼所表達的內容不再只能扁平化,終於進化出了“階級”。

從本質上來講,以上演進反映了語言自身抽象能力的不斷提升。

這裡非常有意思的一個現象是,抽象化的不斷提升,會使得語言的維度提升至某個分數維——抽象的本質是在空間上提供了某種自相似的遞迴對映,從而體現出“分形”的結構形式,分形結構表現出在原有空間中增加了分數維,但是得到一個新的整數維是困難的,即比如1維可以提升至1.5維,但是無法到達2維。

所以,目前絕大部分計算機高階語言的維度是1.X。

但是第五步,意味著語言開始真正走向一個更高的維度。

事實上,實現超程式設計有多種方式,從語言本身來講,可以分為兩類:增強型API與新的語法實現,前者的代表是反射,後者的代表為Annotation。

我們來看一個例子:

public class TestCase{
    @Before
    public void setUp() throws Exception{}
    @After
    public void tearDown() throws Exception{}
    @Test
    public void add() {}
}
複製程式碼

上面是Java語言中使用Annotation型別定義了一個單元測試的三個階段,在這裡: @Before、@After、@Test用“變數”定義了“變數”,同時定義了執行的順序,這裡是“對編碼再進行編碼”的過程,是超程式設計的一種典型的實現。

我們當然也可以通過增強型API(反射或者用設計約束(比如摸版方法))來解決,但是無論哪一種,都不如Annotation的方式要簡單直接明瞭。

根本的原因,在於增強型API的實現方式與原有程式碼這兩個表達邏輯的維度存在過多的“相關性”,即1.X維的,但Annotation的方式在相關性上大大減少,兩個維度更加解耦,所以後者的自由度更高。

如下JS基於Mocha的單元測試程式碼:

describe('測試過程1', function() {
	it('1+1', function() {
		expect(fn_add(1, 1)).to.be.equal(2);
	});
});
複製程式碼

我們期望如下程式設計風格:

'@test(step=測試過程1,name=1+1,expect=2';
var step0 = function(){
	return fn_add(1, 1);
}
複製程式碼

###JS實現基於註釋的超程式設計

我們嘗試將Annotation的機制引入JS,如下:

'@Log(level=info,dateFormat=YYYY-MM-DD HH:mm)’;
var logInfo = function(_msg){
	console.log(_msg);
} 
複製程式碼

複雜的場景,考慮多個註釋的相關性:

'@Start';
var serverStart = function(){}

'@Rule(fileType=.(html|htm))';
var proHtml = function(_req,_res){}

'@Rule(fileType=.(jpg|gif|webp))';
var proPic = function(_req,_res){}

'@Finish';
var serverFinish = function(){}
複製程式碼

At-js框架

基於以上想法,我們實現了At-js框架並開源,At-js的實現思路非常簡單,在Node.js端,通過覆蓋執行時JS檔案載入機制實現對Annotation型別的識別判斷並對原生檔案進行Enhance處理,為效能考慮,At-js採用了正則掃描而非AST的方式。

At-js使用方法包括:定義註釋與使用註釋。

定義註釋:

require('at-js').define('helloworld',{//annotation's name
	scope: 'var', build: function () {//the scope of it's effected
    	return "return function(_msg)	{console.log('[helloworld]'+_msg);};"//the real script
	}
})
複製程式碼

使用註釋:

'@helloworld';
var sayHello = function(){}

sayHello('here')
複製程式碼

執行效果:

[Helloworld]here

以下程式碼描述了一個單元測試過程(https://github.com/CheMingjun/at-test):

'@test.start';
var start = function () {
	ds = {};
}

'@test.step(timeout=2000)';
var test0 = function* () {
	ds.test0 = 'finish';
	var rtn = yield (function(){
		return function(_next){
    		setTimeout(function(){
        		_next(null,3);
    	},2000)
		}
	})();
    assert.equal(rtn,3);
}

'@test.step';
var test1 = function () {
	ds.test1 = 'finish';
	return ds;
}

'@test.finish';
var fh = function () {
	ds = null;
}
複製程式碼

At-js支援Var級及File不同級別的註釋定義,上例屬於File級別複雜的註釋定義,兩者的API如下:

Var型註釋定義:

    {
        scope:'var',
        build:function(_ctx, _argAry){
            //_ctx
            {
                filePath,//應該該註釋的檔案位置
                name,//註釋名稱
                desc,//註釋中的變數表(key-value)
                refName,//被註釋的變數名稱
                refType//被註釋的變數型別(undefined|function|generator|object)
            }
            //_aryAry 被註釋變數簽名中的引數列表
        
            return //返回該變數被替換之後的程式碼
        }
    }
複製程式碼

File型註釋定義:

    {
        return {
            which: {//針對改組annotation中的每一項做處理
                'test.start': function (_ctx, _argAry) {
                    //_ctx 與 _argAry 同上定義
                    //處理邏輯
                }
            }, script: function () {
                return //返回該檔案追加的程式碼
            }
        }
    }
複製程式碼

在實際生產過程中,如下一套註釋實現了ORM:

	'@dao.column';
	var id;

	'@dao.column(name=name)';
	var name;

	'@dao.column(name=status)';
	var status;

	'@dao.column(name=creator_id)';
	var creatorId;

	'@dao.column(name=creator_name)';
	var creatorName;

	'@dao.column(name=gmt_create)';
	var createTime = function (_time) {
    		var mm = require('moment');
    		return mm(_time).format("YYYY-MM-DD HH:mm:ss");
	}

	'@dao.column(name=gmt_update)';
	var updateTime = function (_time) {
    		var mm = require('moment');
    		return mm(_time).format("YYYY-MM-DD HH:mm:ss");
	}

	'@dao.column(name=type)';
	var type;
複製程式碼

總結

本文給出了語言自由度的簡單定義,並在此基礎上論述了在語言發展過程中呈現的不同複雜性,並探討了超程式設計是如何從根本上增加語言的自由度的。在第二部分,我們嘗試在JS語言基礎上增加原生的超程式設計能力並介紹了該思路的實現:At-js框架。

相關文章