|
2008年2月18日
謹以此文慶祝清華建校108周年,及5字班本科畢業20周年 作者: Canonical 眾所周知,計算機科學得以存在的基石是兩個基本理論:圖靈于1936年提出的圖靈機理論和丘奇同年早期發表的Lambda演算理論。這兩個理論奠定了所謂通用計算(Universal Computation)的概念基礎,描繪了具有相同計算能力(圖靈完備),但形式上卻南轅北轍、大相徑庭的兩條技術路線。如果把這兩種理論看作是上帝所展示的世界本源面貌的兩個極端,那么是否存在一條更加中庸靈活的到達通用計算彼岸的中間路徑? 自1936年以來,軟件作為計算機科學的核心應用,一直處在不間斷的概念變革過程中,各類程序語言/系統架構/設計模式/方法論層出不窮,但是究其軟件構造的基本原理,仍然沒有脫出兩個基本理論最初所設定的范圍。如果定義一種新的軟件構造理論,它所引入的新概念本質上能有什么特異之處?能夠解決什么棘手的問題? 本文中筆者提出在圖靈機和lambda演算的基礎上可以很自然的引入一個新的核心概念--可逆性,從而形成一個新的軟件構造理論--可逆計算(Reversible Computation)。可逆計算提供了區別于目前業內主流方法的更高層次的抽象手段,可以大幅降低軟件內在的復雜性,為粗粒度軟件復用掃除了理論障礙。 可逆計算的思想來源不是計算機科學本身,而是理論物理學,它將軟件看作是處于不斷演化過程中的抽象實體, 在不同的復雜性層次上由不同的運算規則所描述,它所關注的是演化過程中產生的微小差量如何在系統內有序的傳播并發生相互作用。 本文第一節將介紹可逆計算理論的基本原理與核心公式,第二節分析可逆計算理論與組件和模型驅動等傳統軟件構造理論的區別和聯系,并介紹可逆計算理論在軟件復用領域的應用,第三節從可逆計算角度解構Docker、React等創新技術實踐。 一. 可逆計算的基本原理可逆計算可以看作是在真實的信息有限的世界中,應用圖靈計算和lambda演算對世界建模的一種必然結果,我們可以通過以下簡單的物理圖像來理解這一點。 首先,圖靈機是一種結構固化的機器,它具有可枚舉的有限的狀態集合,只能執行有限的幾條操作指令,但是可以從無限長的紙帶上讀取和保存數據。例如我們日常使用的電腦,它在出廠的時候硬件功能就已經確定了,但是通過安裝不同的軟件,傳入不同的數據文件,最終它可以自動產生任意復雜的目標輸出。圖靈機的計算過程在形式上可以寫成 
與圖靈機相反的是,lambda演算的核心概念是函數,一個函數就是一臺小型的計算機器,函數的復合仍然是函數,也就是說可以通過機器和機器的遞歸組合來產生更加復雜的機器。lambda演算的計算能力與圖靈機等價,這意味著如果允許我們不斷創建更加復雜的機器,即使輸入一個常數0,我們也可以得到任意復雜的目標輸出。lambda演算的計算過程在形式上可以寫成
可以看出,以上兩種計算過程都可以被表達為Y=F(X) 這樣一種抽象的形式。如果我們把Y=F(X)理解為一種建模過程,即我們試圖理解輸入的結構以及輸入和輸出之間的映射關系,采用最經濟的方式重建輸出,則我們會發現圖靈機和lambda演算都假定了現實世界中無法滿足的條件。在真實的物理世界中,人類的認知總是有限的,所有的量都需要區分已知的部分和未知的部分,因此我們需要進行如下分解:
重新整理一下符號,我們就得到了一個適應范圍更加廣泛的計算模式
除了函數運算F(X)之外,這里出現了一個新的結構運算符⊕,它表示兩個元素之間的合成運算,并不是普通數值意義上的加法,同時引出了一個新的概念:差量△。△的特異之處在于,它必然包含某種負元素,F(X)與△合并在一起之后的結果并不一定是“增加”了輸出,而完全可能是“減少”。 在物理學中,差量△存在的必然性以及△包含逆元這一事實完全是不言而喻的,因為物理學的建模必須要考慮到兩個基本事實: - 世界是“測不準”的,噪聲永遠存在
- 模型的復雜度要和問題內在的復雜度相匹配,它捕獲的是問題內核中穩定不變的趨勢及規律。
例如,對以下的數據  我們所建立的模型只能是類似圖(a)中的簡單曲線,圖(b)中的模型試圖精確擬合每一個數據點在數學上稱之為過擬合,它難以描述新的數據,而圖(c)中限制差量只能為正值則會極大的限制模型的描述精度。 以上是對Y=F(X)⊕△這一抽象計算模式的一個啟發式說明,下面我們將介紹在軟件構造領域落實這一計算模式的一種具體技術實現方案,筆者將其命名為可逆計算。 所謂可逆計算,是指系統化的應用如下公式指導軟件構造的一種技術路線
App : 所需要構建的目標應用程序 DSL: 領域特定語言(Domain Specific Language),針對特定業務領域定制的業務邏輯描述語言,也是所謂領域模型的文本表示形式 Generator : 根據領域模型提供的信息,反復應用生成規則可以推導產生大量的衍生代碼。實現方式包括獨立的代碼生成工具,以及基于元編程(Metaprogramming)的編譯期模板展開 Biz : 根據已知模型推導生成的邏輯與目標應用程序邏輯之間的差異被識別出來,并收集在一起,構成獨立的差量描述 aop_extends: 差量描述與模型生成部分通過類似面向切面編程(Aspect Oriented Programming)的技術結合在一起,這其中涉及到對模型生成部分的增加、修改、替換、刪除等一系列操作 DSL是對關鍵性領域信息的一種高密度的表達,它直接指導Generator生成代碼,這一點類似于圖靈計算通過輸入數據驅動機器執行內置指令。而如果把Generator看作是文本符號的替換生成,則它的執行和復合規則完全就是lambda演算的翻版。差量合并在某種意義上是一種很新奇的操作,因為它要求我們具有一種細致入微、無所不達的變化收集能力,能夠把散布系統各處的同階小量分離出來并合并在一起,這樣差量才具有獨立存在的意義和價值。同時,系統中必須明確建立逆元和逆運算的概念,在這樣的概念體系下差量作為“存在”與“不存在”的混合體才可能得到表達。 現有的軟件基礎架構如果不經過徹底的改造,是無法有效的實施可逆計算的。正如圖靈機模型孕育了C語言,Lambda演算促生了Lisp語言一樣,為了有效支持可逆計算,筆者提出了一種新的程序語言X語言,它內置了差量定義、生成、合并、拆分等關鍵特性,可以快速建立領域模型,并在領域模型的基礎上實現可逆計算。 為了實施可逆計算,我們必須要建立差量的概念。變化產生差量,差量有正有負,而且應該滿足下面三條要求: - 差量獨立存在
- 差量相互作用
- 差量具有結構
在第三節中筆者將會以Docker為實例說明這三條要求的重要性。 可逆計算的核心是“可逆”,這一概念與物理學中熵的概念息息相關,它的重要性其實遠遠超出了程序構造本身,在可逆計算的方法論來源一文中,筆者會對它有更詳細的闡述。 正如復數的出現擴充了代數方程的求解空間,可逆計算為現有的軟件構造技術體系補充了“可逆的差量合并”這一關鍵性技術手段,從而極大擴充了軟件復用的可行范圍,使得系統級的粗粒度軟件復用成為可能。同時在新的視角下,很多原先難以解決的模型抽象問題可以找到更加簡單的解決方案,從而大幅降低了軟件構造的內在復雜性。在第二節中筆者將會對此進行詳細闡述。 軟件開發雖然號稱是知識密集性的工作,但到目前為止,眾多一線程序員的日常中仍然包含著大量代碼拷貝/粘貼/修改的機械化手工操作內容,而在可逆計算理論中,代碼結構的修改被抽象為可自動執行的差量合并規則,因此通過可逆計算,我們可以為軟件自身的自動化生產創造基礎條件。筆者在可逆計算理論的基礎上,提出了一個新的軟件工業化生產模式NOP(Nop is nOt Programming),以非編程的方式批量生產軟件。NOP不是編程,但也不是不編程,它強調的是將業務人員可以直觀理解的邏輯與純技術實現層面的邏輯相分離,分別使用合適的語言和工具去設計,然后再把它們無縫的粘接在一起。筆者將在另一篇文章中對NOP進行詳細介紹。 可逆計算與可逆計算機有著同樣的物理學思想來源,雖然具體的技術內涵并不一致,但它們目標卻是統一的。正如云計算試圖實現計算的云化一樣,可逆計算和可逆計算機試圖實現的都是計算的可逆化。 二. 可逆計算對傳統理論的繼承和發展軟件的誕生源于數學家研究希爾伯特第十問題時的副產品,早期軟件的主要用途也是數學物理計算,那時軟件中的概念無疑是抽象的、數學化的。隨著軟件的普及,越來越多應用軟件的研發催生了面向對象和組件化的方法論,它試圖弱化抽象思維,轉而貼近人類的常識,從人們的日常經驗中汲取知識,把業務領域中人們可以直觀感知的概念映射為軟件中的對象,仿照物質世界的生產制造過程從無到有、從小到大,逐步拼接組裝實現最終軟件產品的構造。 像框架、組件、設計模式、架構視圖等軟件開發領域中耳熟能詳的概念,均直接來自于建筑業的生產經驗。組件理論繼承了面向對象思想的精華,借助可復用的預制構件這一概念,創造了龐大的第三方組件市場,獲得了空前的技術和商業成功,即使到今天仍然是最主流的軟件開發指導思想。但是,組件理論內部存在著一個本質性的缺陷,阻礙了它把自己的成功繼續推進到一個新的高度。 我們知道,所謂復用就是對已有的制成品的重復使用。為了實現組件復用,我們需要找到兩個軟件中的公共部分,把它分離出來并按照組件規范整理成標準形式。但是,A和B的公共部分的粒度是比A和B都要小的,大量軟件的公共部分是比它們中任何一個的粒度都要小得多的。這一限制直接導致越大粒度的軟件功能模塊越難以被直接復用,組件復用存在理論上的極限。可以通過組件組裝復用60%-70%的工作量,但是很少有人能超過80%,更不用說實現復用度90%以上的系統級整體復用了。 為了克服組件理論的局限,我們需要重新認識軟件的抽象本質。軟件是在抽象的邏輯世界中存在的一種信息產品,信息并不是物質。抽象世界的構造和生產規律與物質世界是有著本質不同的。物質產品的生產總是有成本的,而復制軟件的邊際成本卻可以是0。將桌子從房間中移走在物質世界中必須要經過門或窗,但在抽象的信息空間中卻只需要將桌子的坐標從x改為-x而已。抽象元素之間的運算關系并不受眾多物理約束的限制,因此信息空間中最有效的生產方式不是組裝,而是掌握和制定運算規則。 如果從數學的角度重新去解讀面向對象和組件技術,我們會發現可逆計算可以被看作是組件理論的一個自然擴展。 - 面向對象 : 不等式 A > B
- 組件 : 加法 A = B + C
- 可逆計算 : 差量 Y = X + △Y
面向對象中的一個核心概念是繼承:派生類從基類繼承,自動具有基類的一切功能。例如老虎是動物的一種派生類,在數學上,我們可以說老虎(A)這個概念所包含的內容比動物(B)這個概念更多,老虎>動物(即A>B)。據此我們可以知道,動物這個概念所滿足的命題,老虎自然滿足, 例如動物會奔跑,老虎必然也會奔跑( P(B) -> P(A) )。程序中所有用到動物這一概念的地方都可以被替換為老虎(Liscov代換原則)。這樣通過繼承就將自動推理關系引入到軟件領域中來,在數學上這對應于不等式,也就是一種偏序關系。 面向對象的理論困境在于不等式的表達能力有限。對于不等式A > B,我們知道A比B多,但是具體多什么,我們并沒有辦法明確的表達出來。而對于 A > B, D > E這樣的情況,即使多出來的部分一模一樣,我們也無法實現這部分內容的重用。組件技術明確指出"組合優于繼承",這相當于引入了加法
這樣就可以抽象出組件C進行重用。 沿著上述方向推演下去,我們很容易確定下一步的發展是引入“減法”,這樣才可以把 A = B + C看作是一個真正的方程,通過方程左右移項求解出
通過減法引入的“負組件”是一個全新的概念,它為軟件復用打開了一扇新的大門。 假設我們已經構建好了系統 X = D + E + F, 現在需要構建 Y = D + E + G。如果遵循組件的解決方案,則需要將X拆解為多個組件,然后更換組件F為G后重新組裝。而如果遵循可逆計算的技術路線,通過引入逆元 -F, 我們立刻得到
在不拆解X的情況下,通過直接追加一個差量△Y,即可將系統X轉化為系統Y。 組件的復用條件是“相同方可復用”,但在存在逆元的情況下,具有最大顆粒度的完整系統X在完全不改的情況下直接就可以被復用,軟件復用的范圍被拓展為“相關即可復用”,軟件復用的粒度不再有任何限制。組件之間的關系也發生了深刻的變化,不再是單調的構成關系,而成為更加豐富多變的轉化關系。 Y = X + △Y 這一物理圖像對于復雜軟件產品的研發具有非常現實的意義。X可以是我們所研發的軟件產品的基礎版本或者說主版本,在不同的客戶處部署實施時,大量的定制化需求被隔離到獨立的差量△Y中,這些定制的差量描述單獨存放,通過編譯技術與主版本代碼再合并到一起。主版本的架構設計和代碼實現只需要考慮業務領域內穩定的核心需求,不會受到特定客戶處偶然性需求的沖擊,從而有效的避免架構腐化。主版本研發和多個項目的實施可以并行進行,不同的實施版本對應不同的△Y,互不影響,同時主版本的代碼與所有定制代碼相互獨立,能夠隨時進行整體升級。 模型驅動架構(MDA)是由對象管理組織(Object Management Group,OMG)在2001年提出的軟件架構設計和開發方法,它被看作是軟件開發模式從以代碼為中心向以模型為中心轉變的里程碑,目前大部分所謂軟件開發平臺的理論基礎都與MDA有關。 MDA試圖提升軟件開發的抽象層次,直接使用建模語言(例如Executable UML)作為編程語言,然后通過使用類似編譯器的技術將高層模型翻譯為底層的可執行代碼。在MDA中,明確區分應用架構和系統架構,并分別用平臺無關模型PIM(Platform Independent Model)和平臺相關模型PSM(Platform Specific Model)來描述它們。PIM反映了應用系統的功能模型,它獨立于具體的實現技術和運行框架,而PSM則關注于使用特定技術(例如J2EE或者dotNet)實現PIM所描述的功能,為PIM提供運行環境。 使用MDA的理想場景是,開發人員使用可視化工具設計PIM,然后選擇目標運行平臺,由工具自動執行針對特定平臺和實現語言的映射規則,將PIM轉換為對應的PSM,并最終生成可執行的應用程序代碼。基于MDA的程序構造可以表述為如下公式
MDA的愿景是像C語言取代匯編那樣最終徹底消滅傳統編程語言。但經歷了這么多年發展之后,它仍未能夠在廣泛的應用領域中展現出相對于傳統編程壓倒性的競爭優勢。 事實上,目前基于MDA的開發工具在面對多變的業務領域時,總是難掩其內在的不適應性。根據本文第一節的分析,我們知道建模必須要考慮差量。而在MDA的構造公式中,左側的App代表了各種未知需求,而右側的Transformer和PIM的設計器實際上都主要由開發工具廠商提供,未知=已知這樣一個方程是無法持久保持平衡的。 目前,工具廠商的主要做法是提供大而全的模型集合,試圖事先預測用戶所有可能的業務場景。但是,我們知道“天下沒有免費的午餐”,模型的價值在于體現了業務領域中的本質性約束,沒有任何一個模型是所有場景下都最優的。預測需求會導致出現一種悖論: 模型內置假定過少,則無法根據用戶輸入的少量信息自動生成大量有用的工作,也無法防止用戶出現誤操作,模型的價值不明顯,而如果反之,模型假定很多,則它就會固化到某個特定業務場景,難以適應新的情況。 打開一個MDA工具的設計器,我們最經常的感受是大部分選項都不需要,也不知道是干什么用的,需要的選項卻到處找也找不到。 可逆計算對MDA的擴展體現為兩點: - 可逆計算中Generator和DSL都是鼓勵用戶擴充和調整的,這一點類似于面向語言編程(Language-oriented programming)。
- 存在一個額外的差量定制機會,可以對整體生成結果進行精確的局部修正。
在筆者提出的NOP生產模式中,必須要包含一個新的關鍵組件:設計器的設計器。普通的程序員可以利用設計器的設計器快速設計開發自己的領域特定語言(DSL)及其可視化設計器,同時可以通過設計器的設計器對系統中的任意設計器進行定制調整,自由的增加或者刪除元素。 面向切面(AOP)是與面向對象(OOP)互補的一種編程范式,它可以實現對那些橫跨多個對象的所謂橫切關注點(cross-cutting concern)的封裝。例如,需求規格中可能規定所有的業務操作都要記錄日志,所有的數據庫修改操作都要開啟事務。如果按照面向對象的傳統實現方式,需求中的一句話將會導致眾多對象類中陡然膨脹出現大量的冗余代碼,而通過AOP, 這些公共的“修飾性”的操作就可以被剝離到獨立的切面描述中。這就是所謂縱向分解和橫向分解的正交性。 
AOP本質上是兩個能力的組合: - 在程序結構空間中定位到目標切點(Pointcut)
- 對局部程序結構進行修改,將擴展邏輯(Advice)編織(Weave)到指定位置。
定位依賴于存在良好定義的整體結構坐標系(沒有坐標怎么定位?),而修改依賴于存在良好定義的局部程序語義結構。目前主流的AOP技術的局限性在于,它們都是在面向對象的語境下表達的,而領域結構與對象實現結構并不總是一致的,或者說用對象體系的坐標去表達領域語義是不充分的。例如,申請人和審批人在領域模型中是需要明確區分的不同的概念,但是在對象層面卻可能都對應于同樣的Person類,使用AOP的很多時候并不能直接將領域描述轉換為切點定義和Advice實現。這種限制反映到應用層面,結果就是除了日志、事務、延遲加載、緩存等少數與特定業務領域無關的“經典”應用之外,我們找不到AOP的用武之地。 可逆計算需要類似AOP的定位和結構修正能力,但是它是在領域模型空間中定義這些能力的,因而大大擴充了AOP的應用范圍。特別是,可逆計算中領域模型自我演化產生的結構差量△能夠以類似AOP切面的形式得到表達。 我們知道,組件可以標識出程序中反復出現的“相同性”,而可逆計算可以捕獲程序結構的“相似性”。相同很罕見,需要敏銳的甄別,但是在任何系統中,有一種相似性都是唾手可得的,即動力學演化過程中系統與自身歷史快照之間的相似性。這種相似性在此前的技術體系中并沒有專門的技術表達形式。 通過縱向和橫向分解,我們所建立的概念之網存在于一個設計平面當中,當設計平面沿著時間軸演化時,很自然的會產生一個“三維”映射關系:后一時刻的設計平面可以看作是從前一時刻的平面增加一個差量映射(定制)而得到,而差量是定義在平面的每一個點上的。這一圖像類似于范疇論(Category Theory)中的函子(Functor)概念,可逆計算中的差量合并扮演了函子映射的角色。因此,可逆計算相當于擴展了原有的設計空間,為演化這一概念找到了具體的一種技術表現形式。 軟件產品線理論源于一個洞察,即在一個業務領域中,很少有軟件系統是完全獨特的,大量的軟件產品之間存在著形式和功能的相似性,可以歸結為一個產品家族,把一個產品家族中的所有產品(已存在的和尚未存在的)作為一個整體來研究、開發、演進,通過科學的方法提取它們的共性,結合有效的可變性管理,就有可能實現規模化、系統化的軟件復用,進而實現軟件產品的工業化生產。 軟件產品線工程采用兩階段生命周期模型,區分領域工程和應用工程。所謂領域工程,是指分析業務領域內軟件產品的共性,建立領域模型及公共的軟件產品線架構,形成可復用的核心資產的過程,即面向復用的開發(development for reuse)。而應用工程,其實質是使用復用來開發( development with reuse),也就是利用已經存在的體系架構、需求、測試、文檔等核心資產來制造具體應用產品的生產活動。 卡耐基梅隆大學軟件工程研究所(CMU-SEI)的研究人員在2008年的報告中宣稱軟件產品線可以帶來如下好處: - 提升10倍以上生產率
- 提升10倍以上產品質量
- 縮減60%以上成本
- 縮減87%以上人力需求
- 縮減98%以上產品上市時間
- 進入新市場的時間以月計,而不是年
軟件產品線描繪的理想非常美好:復用度90%以上的產品級復用、隨需而變的敏捷定制、無視技術變遷影響的領域架構、優異可觀的經濟效益等等。它所存在的唯一問題就是如何才能做到?盡管軟件產品線工程試圖通過綜合利用所有管理的和技術的手段,在組織級別策略性的復用一切技術資產(包括文檔、代碼、規范、工具等等),但在目前主流的技術體制下,發展成功的軟件產品線仍然面臨著重重困難。 可逆計算的理念與軟件產品線理論高度契合,它的技術方案為軟件產品線的核心技術困難---可變性管理帶來了新的解決思路。在軟件產品線工程中,傳統的可變性管理主要是適配、替換和擴展這三種方式: 
這三種方式都可以看作是向核心架構補充功能。但是可復用性的障礙不僅僅是來自于無法追加新的功能,很多時候也在于無法屏蔽原先已經存在的功能。傳統的適配技術等要求接口一致匹配,是一種剛性的對接要求,一旦失配必將導致不斷向上傳導應力,最終只能通過整體更換組件來解決問題。可逆計算通過差量合并為可變性管理補充了“消除”這一關鍵性機制,可以按需在領域模型空間中構建出柔性適配接口,從而有效的控制變化點影響范圍。 可逆計算中的差量雖然也可以被解釋為對基礎模型的一種擴展,但是它與插件擴展技術之間還是存在著明顯的區別。在平臺-插件這樣的結構中,平臺是最核心的主體,插件依附于平臺而存在,更像是一種補丁機制,在概念層面上是相對次要的部分。而在可逆計算中,通過一些形式上的變換,我們可以得到一個對稱性更高的公式:
如果把G看作是一種相對不變的背景知識,則形式上我們可以把它隱藏起來,定義一個更加高級的“括號”運算符,它類似于數學中的“內積”。在這種形式下,B和D是對偶的,B是對D的補充,而D也是對B的補充。同時,我們注意到G(D)是模型驅動架構的體現,模型驅動之所以有價值就在于模型D中發生的微小變化,可以被G放大為系統各處大量衍生的變化,因此G(D)是一種非線性變換,而B是系統中去除D所對應的非線性因素之后剩余的部分。當所有復雜的非線性影響因素都被剝離出去之后,最后剩下的部分B就有可能是簡單的,甚至能夠形成一種新的可獨立理解的領域模型結構(可以類比聲波與空氣的關系,聲波是空氣的擾動,但是不用研究空氣本體,我們就可以直接用正弦波模型來描述聲波)。 A = (B,D)的形式可以直接推廣到存在更多領域模型的情況
因為B、D、E等概念都是某種DSL所描述的領域模型,因此它們可以被解釋為A投影到特定領域模型子空間所產生的分量,也就是說,應用A可以被表示為一個“特征向量”(Feature Vector), 例如
與軟件產品線中常用的面向特征編程(Feature Oriented Programming)相比,可逆計算的特征分解方案強調領域特定描述,特征邊界更加明確,特征合成時產生的概念沖突更容易處理。 特征向量本身構成更高維度的領域模型,它可以被進一步分解下去,從而形成一個模型級列,例如定義
, 并且假設D'可以繼續分解
,則可以得到
最終我們可以通過領域特征向量U'來描述D’,然后再通過領域特征向量D‘來描述原有的模型A。 可逆計算的這一構造策略類似于深度神經網絡,它不再局限于具有極多可調參數的單一模型,而是建立抽象層級不同、復雜性層級不同的一系列模型,通過逐步求精的方式構造出最終的應用。 在可逆計算的視角下,應用工程的工作內容變成了使用特征向量來描述軟件需求,而領域工程則負責根據特征向量描述來生成最終的軟件。 三. 初露端倪的差量革命(一)DockerDocker是2013年由創業公司dotCloud開源的應用容器引擎,它可以將任何應用及其依賴的環境打包成一個輕量級、可移植、自包含的容器(Container),并據此以容器為標準化單元創造了一種新型的軟件開發、部署和交付形式。 Docker一出世就秒殺了Google的親兒子lmctfy (Let Me Contain That For You)容器技術,同時也把Google的另一個親兒子Go語言迅速捧成了網紅,之后Docker的發展 更是一發而不可收拾。2014年開始一場Docker風暴席卷全球,以前所未有的力度推動了操作系統內核的變革,在眾多巨頭的跟風造勢下瞬間引爆容器云市場,真正從根本上改變了企業應用從開發、構建到部署、運行整個生命周期的技術形態。  Docker技術的成功源于它對軟件運行時復雜性的本質性降低,而它的技術方案可以看作是可逆計算理論的一種特例。Docker的核心技術模式可以用如下公式進行概括
Dockerfile是構建容器鏡像的一種領域特定語言,例如 FROM ubuntu:16.04 RUN useradd --user-group --create-home --shell /bin/bash work RUN apt-get update -y && apt-get install -y python3-dev COPY . /app RUN make /app ENV PYTHONPATH /FrameworkBenchmarks CMD python /app/app.py EXPOSE 8088
通過Dockerfile可以快速準確的描述容器所依賴的基礎鏡像,具體的構建步驟,運行時環境變量和系統配置等信息。 Docker應用程序扮演了可逆計算中Generator的角色,負責解釋Dockerfile,執行對應的指令來生成容器鏡像。 創造性的使用聯合文件系統(Union FS),是Docker的一個特別的創新之處。這種文件系統采用分層的構造方式,每一層構建完畢后就不會再發生改變,在后一層上進行的任何修改都只會記錄在自己這一層。例如,修改前一層的文件時會通過Copy-On-Write的方式復制一份到當前層,而刪除前一層的文件并不會真的執行刪除操作,而是僅在當前層標記該文件已刪除。Docker利用聯合文件系統來實現將多個容器鏡像合成為一個完整的應用,這一技術的本質正是可逆計算中的aop_extends操作。 Docker的英文是碼頭搬運工人的意思,它所搬運的容器也經常被人拿來和集裝箱做對比:標準的容器和集裝箱類似,使得我們可以自由的對它們進行傳輸/組合,而不用考慮容器中的具體內容。但是這種比較是膚淺的,甚至是誤導性的。集裝箱是靜態的、簡單的、沒有對外接口的,而容器則是動態的、復雜的、和外部存在著大量信息交互的。這種動態的復雜結構想和普通的靜態物件一樣封裝成所謂標準容器,其難度不可同日而語。如果沒有引入支持差量的文件系統,是無法構建出一種柔性邊界,實現邏輯分離的。 Docker所做的標準封裝其實虛擬機也能做到,甚至差量存儲機制在虛擬機中也早早的被用于實現增量備份,Docker與虛擬機的本質性不同到底在什么地方?回顧第一節中可逆計算對差量三個基本要求,我們可以清晰的發現Docker的獨特之處。 - 差量獨立存在:Docker最重要的價值就在于通過容器封裝,拋棄了作為背景存在(必不可少,但一般情況下不需要了解),占據了99%的體積和復雜度的操作系統層。應用容器成為了可以獨立存儲、獨立操作的第一性的實體。輕裝上陣的容器在性能、資源占用、可管理性等方面完全超越了虛胖的虛擬機。
- 差量相互作用:Docker容器之間通過精確受控的方式發生相互作用,可通過操作系統的namespace機制選擇性的實現資源隔離或者共享。而虛擬機的差量切片之間是沒有任何隔離機制的。
- 差量具有結構:虛擬機雖然支持增量備份,但是人們卻沒有合適的手段去主動構造一個指定的差量切片出來。歸根結底,是因為虛擬機的差量定義在二進制字節空間中,而這個空間非常貧瘠,幾乎沒有什么用戶可以控制的構造模式。而Docker的差量是定義在差量文件系統空間中,這個空間繼承了Linux社區最豐富的歷史資源。每一條shell指令的執行結果最終反映到文件系統中都是增加/刪除/修改了某些文件,所以每一條shell指令都可以被看作是某個差量的定義。差量構成了一個異常豐富的結構空間,差量既是這個空間中的變換算符(shell指令),又是變換算符的運算結果。差量與差量相遇產生新的差量,這種生生不息才是Docker的生命力所在。
(二)React2013年,也就是Docker發布的同一年,Facebook公司開源了一個革命性的Web前端框架React。React的技術思想非常獨特,它以函數式編程思想為基礎,結合一個看似異想天開的虛擬DOM(Virtual DOM)概念,引入了一整套新的設計模式,開啟了前端開發的新大航海時代。 class HelloMessage extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.action = this.action.bind(this); } action(){ this.setState(state => ({ count: state.count + 1 })); }, render() { return ( <button onClick={this.action}> Hello {this.props.name}:{this.state.count} </button> ); } }
ReactDOM.render( <HelloMessage name="Taylor" />, mountNode );
React組件的核心是render函數,它的設計參考了后端常見的模板渲染技術,主要區別在于后端模板輸出的是HTML文本,而React組件的Render函數使用類似XML模板的JSX語法,通過編譯轉換在運行時輸出的是虛擬DOM節點對象。例如上面HelloMessage組件的render函數被翻譯后的結果類似于 render(){ return new VNode("button", {onClick: this.action, content: "Hello "+ this.props.name + ":" + this.state.count }); } 可以用以下公式來描述React組件: VDom = render(state) 當狀態發生變化以后,只要重新執行render函數就會生成新的虛擬DOM節點,虛擬DOM節點可以被翻譯成真實的HTML DOM對象,從而實現界面更新。這種根據狀態重新生成完整視圖的渲染策略極大簡化了前端界面開發。例如對于一個列表界面,傳統編程需要編寫新增行/更新行/刪除行等多個不同的DOM操作函數,而在React中只要更改state后重新執行唯一的render函數即可。 每次重新生成DOM視圖的唯一問題是性能很低,特別是當前端交互操作眾多、狀態變化頻繁的時候。React的神來之筆是提出了基于虛擬DOM的diff算法,可以自動的計算兩個虛擬DOM樹之間的差量,狀態變化時只要執行虛擬Dom差量對應的DOM修改操作即可(更新真實DOM時會觸發樣式計算和布局計算,導致性能很低,而在JavaScript中操作虛擬DOM 的速度是非常快的)。整體策略可以表示為如下公式
顯然,這一策略也是可逆計算的一種特例。 只要稍微留意一下就會發現,最近幾年merge/diff/residual/delta等表達差量運算的概念越來越多的出現在軟件設計領域中。比如大數據領域的流計算引擎中,流與表之間的關系可以表示為
對表的增刪改查操作可以被編碼為事件流,而將表示數據變化的事件累積到一起就形成了數據表。 現代科學發端于微積分的發明,而微分的本質就是自動計算無窮小差量,而積分則是微分的逆運算,自動對無窮小量進行匯總合并。19世紀70年代,經濟學經歷了一場邊際革命,將微積分的思想引入經濟分析,在邊際這一概念之上重建了整個經濟學大廈。軟件構造理論發展到今天,已經進入一個瓶頸,也到了應該重新認識差量的時候。 四. 結語筆者的專業背景是理論物理學,可逆計算源于筆者將物理學和數學的思想引入軟件領域的一種嘗試,它最早由筆者在2007年左右提出。一直以來,軟件領域對于自然規律的應用一般情況下都只限于"模擬"范疇,例如流體動力學模擬軟件,雖然它內置了人類所認知的最深刻的一些世界規律,但這些規律并沒有被用于指導和定義軟件世界自身的構造和演化,它們的指向范圍是軟件世界之外,而不是軟件世界自身。在筆者看來,在軟件世界中,我們完全可以站在“上帝的視角”,規劃和定義一系列的結構構造規律,輔助我們完成軟件世界的構建。而為了完成這一點,我們首先需要建立程序世界中的“微積分”。 類似于微積分,可逆計算理論的核心是將“差量”提升為第一性的概念,將全量看作是差量的一種特例(全量=單位元+全量)。傳統的程序世界中我們所表達的都只是“有”,而且是“所有”,差量只能通過全量之間的運算間接得到,它的表述和操縱都需要特殊處理,而基于可逆計算理論,我們首先應該定義所有差量概念的表達形式,然后再圍繞這些概念去建立整個領域概念體系。為了保證差量所在數學空間的完備性(差量之間的運算結果仍然需要是合法的差量),差量所表達的不能僅僅是“有”,而必須是“有”和“沒有”的一種混合體。也就是說差量必須是“可逆的”。可逆性具有非常深刻的物理學內涵,在基本的概念體系中內置這一概念可以解決很多非常棘手的軟件構造問題。 為了處理分布式問題,現代軟件開發體系已經接受了不可變數據的概念,而為了解決大粒度軟件復用問題,我們還需要接受不可變邏輯的概念(復用可以看作是保持原有邏輯不變,然后增加差量描述)。目前,業內已經逐步出現了一些富有創造性的主動應用差量概念的實踐,它們都可以在可逆計算的理論框架下得到統一的詮釋。筆者提出了一種新的程序語言X語言,它可以極大簡化可逆計算的技術實現。目前筆者已經基于X語言設計并實現了一系列軟件框架和生產工具,并基于它們提出了一種新的軟件生產范式(NOP)。
瀏覽器前端編程的面貌自2005年以來已經發生了深刻的變化,這并不簡單的意味著出現了大量功能豐富的基礎庫,使得我們可以更加方便的編寫業務代碼,更重要的是我們看待前端技術的觀念發生了重大轉變,明確意識到了如何以前端特有的方式釋放程序員的生產力。本文將結合jQuery源碼的實現原理,對javascript中涌現出的編程范式和常用技巧作一簡單介紹。 1. AJAX: 狀態駐留,異步更新 首先來看一點歷史。 A. 1995年Netscape公司的Brendan Eich開發了javacript語言,這是一種動態(dynamic)、弱類型(weakly typed)、基于原型(prototype-based)的腳本語言。 B. 1999年微軟IE5發布,其中包含了XMLHTTP ActiveX控件。 C. 2001年微軟IE6發布,部分支持DOM level 1和CSS 2標準。 D. 2002年Douglas Crockford發明JSON格式。 至此,可以說Web2.0所依賴的技術元素已經基本成形,但是并沒有立刻在整個業界產生重大的影響。盡管一些“頁面異步局部刷新”的技巧在程序員中間秘密的流傳,甚至催生了bindows這樣龐大臃腫的類庫,但總的來說,前端被看作是貧瘠而又骯臟的沼澤地,只有后臺技術才是王道。到底還缺少些什么呢? 當我們站在今天的角度去回顧2005年之前的js代碼,包括那些當時的牛人所寫的代碼,可以明顯的感受到它們在程序控制力上的孱弱。并不是說2005年之前的js技術本身存在問題,只是它們在概念層面上是一盤散沙,缺乏統一的觀念,或者說缺少自己獨特的風格, 自己的靈魂。當時大多數的人,大多數的技術都試圖在模擬傳統的面向對象語言,利用傳統的面向對象技術,去實現傳統的GUI模型的仿制品。 2005年是變革的一年,也是創造概念的一年。伴隨著Google一系列讓人耳目一新的交互式應用的發布,Jesse James Garrett的一篇文章《Ajax: A New Approach to Web Applications》被廣為傳播。Ajax這一前端特有的概念迅速將眾多分散的實踐統一在同一口號之下,引發了Web編程范式的轉換。所謂名不正則言不順,這下無名群眾可找到組織了。在未有Ajax之前,人們早已認識到了B/S架構的本質特征在于瀏覽器和服務器的狀態空間是分離的,但是一般的解決方案都是隱藏這一區分,將前臺狀態同步到后臺,由后臺統一進行邏輯處理,例如ASP.NET。因為缺乏成熟的設計模式支持前臺狀態駐留,在換頁的時候,已經裝載的js對象將被迫被丟棄,這樣誰還能指望它去完成什么復雜的工作嗎? Ajax明確提出界面是局部刷新的,前臺駐留了狀態,這就促成了一種需要:需要js對象在前臺存在更長的時間。這也就意味著需要將這些對象和功能有效的管理起來,意味著更復雜的代碼組織技術,意味著對模塊化,對公共代碼基的渴求。 jQuery現有的代碼中真正與Ajax相關(使用XMLHTTP控件異步訪問后臺返回數據)的部分其實很少,但是如果沒有Ajax, jQuery作為公共代碼基也就缺乏存在的理由。
2. 模塊化:管理名字空間 當大量的代碼產生出來以后,我們所需要的最基礎的概念就是模塊化,也就是對工作進行分解和復用。工作得以分解的關鍵在于各人獨立工作的成果可以集成在一起。這意味著各個模塊必須基于一致的底層概念,可以實現交互,也就是說應該基于一套公共代碼基,屏蔽底層瀏覽器的不一致性,并實現統一的抽象層,例如統一的事件管理機制等。比統一代碼基更重要的是,各個模塊之間必須沒有名字沖突。否則,即使兩個模塊之間沒有任何交互,也無法共同工作。 jQuery目前鼓吹的主要賣點之一就是對名字空間的良好控制。這甚至比提供更多更完善的功能點都重要的多。良好的模塊化允許我們復用任何來源的代碼,所有人的工作得以積累疊加。而功能實現僅僅是一時的工作量的問題。jQuery使用module pattern的一個變種來減少對全局名字空間的影響,僅僅在window對象上增加了一個jQuery對象(也就是$函數)。 所謂的module pattern代碼如下,它的關鍵是利用匿名函數限制臨時變量的作用域。 var feature =(function() {
// 私有變量和函數 var privateThing = 'secret', publicThing = 'not secret',
changePrivateThing = function() { privateThing = 'super secret'; },
sayPrivateThing = function() { console.log(privateThing); changePrivateThing(); };
// 返回對外公開的API return { publicThing : publicThing, sayPrivateThing : sayPrivateThing } })(); js本身缺乏包結構,不過經過多年的嘗試之后業內已經逐漸統一了對包加載的認識,形成了RequireJs庫這樣得到一定共識的解決方案。jQuery可以與RequireJS庫良好的集成在一起, 實現更完善的模塊依賴管理。http://requirejs.org/docs/jquery.html require(["jquery", "jquery.my"], function() { //當jquery.js和jquery.my.js都成功裝載之后執行 $(function(){ $('#my').myFunc(); }); }); 通過以下函數調用來定義模塊my/shirt, 它依賴于my/cart和my/inventory模塊, require.def("my/shirt", ["my/cart", "my/inventory"], function(cart, inventory) { // 這里使用module pattern來返回my/shirt模塊對外暴露的API return { color: "blue", size: "large" addToCart: function() { // decrement是my/inventory對外暴露的API inventory.decrement(this); cart.add(this); } } } );
3. 神奇的$:對象提升 當你第一眼看到$函數的時候,你想到了什么?傳統的編程理論總是告訴我們函數命名應該準確,應該清晰無誤的表達作者的意圖,甚至聲稱長名字要優于短名字,因為減少了出現歧義的可能性。但是,$是什么?亂碼?它所傳遞的信息實在是太隱晦,太曖昧了。$是由prototype.js庫發明的,它真的是一個神奇的函數,因為它可以將一個原始的DOM節點提升(enhance)為一個具有復雜行為的對象。在prototype.js最初的實現中,$函數的定義為 var $ = function (id) { return "string" == typeof id ? document.getElementById(id) : id; }; 這基本對應于如下公式 e = $(id) 這絕不僅僅是提供了一個聰明的函數名稱縮寫,更重要的是在概念層面上建立了文本id與DOM element之間的一一對應。在未有$之前,id與對應的element之間的距離十分遙遠,一般要將element緩存到變量中,例如 var ea = docuement.getElementById('a'); var eb = docuement.getElementById('b'); ea.style.... 但是使用$之后,卻隨處可見如下的寫法 $('header_'+id).style... $('body_'+id).... id與element之間的距離似乎被消除了,可以非常緊密的交織在一起。 prototype.js后來擴展了$的含義, function $() { var elements = new Array(); for (var i = 0; i < arguments.length; i++) { var element = arguments[i]; if (typeof element == 'string') element = document.getElementById(element); if (arguments.length == 1) return element; elements.push(element); } return elements; } 這對應于公式 [e,e] = $(id,id) 很遺憾,這一步prototype.js走偏了,這一做法很少有實用的價值。 真正將$發揚光大的是jQuery, 它的$對應于公式 [o] = $(selector) 這里有三個增強 A. selector不再是單一的節點定位符,而是復雜的集合選擇符 B. 返回的元素不是原始的DOM節點,而是經過jQuery進一步增強的具有豐富行為的對象,可以啟動復雜的函數調用鏈。 C. $返回的包裝對象被造型為數組形式,將集合操作自然的整合到調用鏈中。
當然,以上僅僅是對神奇的$的一個過分簡化的描述,它的實際功能要復雜得多. 特別是有一個非常常用的直接構造功能. $("<table><tbody><tr><td>...</td></tr></tbody></table>").... jQuery將根據傳入的html文本直接構造出一系列的DOM節點,并將其包裝為jQuery對象. 這在某種程度上可以看作是對selector的擴展: html內容描述本身就是一種唯一指定. $(function{})這一功能就實在是讓人有些無語了, 它表示當document.ready的時候調用此回調函數。真的,$是一個神奇的函數, 有任何問題,請$一下。 總結起來, $是從普通的DOM和文本描述世界到具有豐富對象行為的jQuery世界的躍遷通道。跨過了這道門,就來到了理想國。 4. 無定形的參數:專注表達而不是約束 弱類型語言既然頭上頂著個"弱"字, 總難免讓人有些先天不足的感覺. 在程序中缺乏類型約束, 是否真的是一種重大的缺憾? 在傳統的強類型語言中, 函數參數的類型,個數等都是由編譯器負責檢查的約束條件, 但這些約束仍然是遠遠不夠的. 一般應用程序中為了加強約束, 總會增加大量防御性代碼, 例如在C++中我們常用ASSERT, 而在java中也經常需要判斷參數值的范圍 if (index < 0 || index >= size) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); 很顯然, 這些代碼將導致程序中存在大量無功能的執行路徑, 即我們做了大量判斷, 代碼執行到某個點, 系統拋出異常, 大喊此路不通. 如果我們換一個思路, 既然已經做了某種判斷,能否利用這些判斷的結果來做些什么呢? javascript是一種弱類型的語言,它是無法自動約束參數類型的, 那如果順勢而行,進一步弱化參數的形態, 將"弱"推進到一種極致, 在弱無可弱的時候, weak會不會成為標志性的特點? 看一下jQuery中的事件綁定函數bind, A. 一次綁定一個事件 $("#my").bind("mouseover", function(){}); B. 一次綁定多個事件 $("#my").bind("mouseover mouseout",function(){}) C. 換一個形式, 同樣綁定多個事件 $("#my").bind({mouseover:function(){}, mouseout:function(){}); D. 想給事件監聽器傳點參數 $('#my').bind('click', {foo: "xxxx"}, function(event) { event.data.foo..}) E. 想給事件監聽器分個組 $("#my").bind("click.myGroup″, function(){}); F. 這個函數為什么還沒有瘋掉??? 就算是類型不確定, 在固定位置上的參數的意義總要是確定的吧? 退一萬步來說, 就算是參數位置不重要了,函數本身的意義應該是確定的吧? 但這是什么? 取值 value = o.val(), 設置值 o.val(3) 一個函數怎么可以這樣過分, 怎么能根據傳入參數的類型和個數不同而行為不同呢? 看不順眼是不是? 可這就是俺們的價值觀. 既然不能防止, 那就故意允許. 雖然形式多變, 卻無一句廢話. 缺少約束, 不妨礙表達(我不是出來嚇人的). 5. 鏈式操作: 線性化的逐步細化 jQuery早期最主要的賣點就是所謂的鏈式操作(chain). $('#content') // 找到content元素 .find('h3') // 選擇所有后代h3節點 .eq(2) // 過濾集合, 保留第三個元素 .html('改變第三個h3的文本') .end() // 返回上一級的h3集合 .eq(0) .html('改變第一個h3的文本');
在一般的命令式語言中, 我們總需要在重重嵌套循環中過濾數據, 實際操作數據的代碼與定位數據的代碼糾纏在一起. 而jQuery采用先構造集合然后再應用函數于集合的方式實現兩種邏輯的解耦, 實現嵌套結構的線性化. 實際上, 我們并不需要借助過程化的思想就可以很直觀的理解一個集合, 例如 $('div.my input:checked')可以看作是一種直接的描述,而不是對過程行為的跟蹤. 循環意味著我們的思維處于一種反復回繞的狀態, 而線性化之后則沿著一個方向直線前進, 極大減輕了思維負擔, 提高了代碼的可組合性. 為了減少調用鏈的中斷, jQuery發明了一個絕妙的主意: jQuery包裝對象本身類似數組(集合). 集合可以映射到新的集合, 集合可以限制到自己的子集合,調用的發起者是集合,返回結果也是集合,集合可以發生結構上的某種變化但它還是集合, 集合是某種概念上的不動點,這是從函數式語言中吸取的設計思想。集合操作是太常見的操作, 在java中我們很容易發現大量所謂的封裝函數其實就是在封裝一些集合遍歷操作, 而在jQuery中集合操作因為太直白而不需要封裝. 鏈式調用意味著我們始終擁有一個“當前”對象,所有的操作都是針對這一當前對象進行。這對應于如下公式 x += dx 調用鏈的每一步都是對當前對象的增量描述,是針對最終目標的逐步細化過程。Witrix平臺中對這一思想也有著廣泛的應用。特別是為了實現平臺機制與業務代碼的融合,平臺會提供對象(容器)的缺省內容,而業務代碼可以在此基礎上進行逐步細化的修正,包括取消缺省的設置等。 話說回來, 雖然表面上jQuery的鏈式調用很簡單, 內部實現的時候卻必須自己多寫一層循環, 因為編譯器并不知道"自動應用于集合中每個元素"這回事. $.fn['someFunc'] = function(){ return this.each(function(){ jQuery.someFunc(this,...); } } 6. data: 統一數據管理 作為一個js庫,它必須解決的一個大問題就是js對象與DOM節點之間的狀態關聯與協同管理問題。有些js庫選擇以js對象為主,在js對象的成員變量中保存DOM節點指針,訪問時總是以js對象為入口點,通過js函數間接操作DOM對象。在這種封裝下,DOM節點其實只是作為界面展現的一種底層“匯編”而已。jQuery的選擇與Witrix平臺類似,都是以HTML自身結構為基礎,通過js增強(enhance)DOM節點的功能,將它提升為一個具有復雜行為的擴展對象。這里的思想是非侵入式設計(non-intrusive)和優雅退化機制(graceful degradation)。語義結構在基礎的HTML層面是完整的,js的作用是增強了交互行為,控制了展現形式。 如果每次我們都通過$('#my')的方式來訪問相應的包裝對象,那么一些需要長期保持的狀態變量保存在什么地方呢?jQuery提供了一個統一的全局數據管理機制。 獲取數據 $('#my').data('myAttr') 設置數據 $('#my').data('myAttr',3); 這一機制自然融合了對HTML5的data屬性的處理 <input id="my" data-my-attr="4" ... /> 通過 $('#my').data('myAttr')將可以讀取到HTML中設置的數據。 第一次訪問data時,jQuery將為DOM節點分配一個唯一的uuid, 然后設置在DOM節點的一個特定的expando屬性上, jQuery保證這個uuid在本頁面中不重復。 elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; 以上代碼可以同時處理DOM節點和純js對象的情況。如果是js對象,則data直接放置在js對象自身中,而如果是DOM節點,則通過cache統一管理。 因為所有的數據都是通過data機制統一管理的,特別是包括所有事件監聽函數(data.events),因此jQuery可以安全的實現資源管理。在clone節點的時候,可以自動clone其相關的事件監聽函數。而當DOM節點的內容被替換或者DOM節點被銷毀的時候,jQuery也可以自動解除事件監聽函數, 并安全的釋放相關的js數據。 7. event:統一事件模型 "事件沿著對象樹傳播"這一圖景是面向對象界面編程模型的精髓所在。對象的復合構成對界面結構的一個穩定的描述,事件不斷在對象樹的某個節點發生,并通過冒泡機制向上傳播。對象樹很自然的成為一個控制結構,我們可以在父節點上監聽所有子節點上的事件,而不用明確與每一個子節點建立關聯。 jQuery除了為不同瀏覽器的事件模型建立了統一抽象之外,主要做了如下增強: A. 增加了自定制事件(custom)機制. 事件的傳播機制與事件內容本身原則上是無關的, 因此自定制事件完全可以和瀏覽器內置事件通過同一條處理路徑, 采用同樣的監聽方式. 使用自定制事件可以增強代碼的內聚性, 減少代碼耦合. 例如如果沒有自定制事件, 關聯代碼往往需要直接操作相關的對象 $('.switch, .clapper').click(function() { var $light = $(this).parent().find('.lightbulb'); if ($light.hasClass('on')) { $light.removeClass('on').addClass('off'); } else { $light.removeClass('off').addClass('on'); } }); 而如果使用自定制事件,則表達的語義更加內斂明確, $('.switch, .clapper').click(function() { $(this).parent().find('.lightbulb').trigger('changeState'); }); B. 增加了對動態創建節點的事件監聽. bind函數只能將監聽函數注冊到已經存在的DOM節點上. 例如 $('li.trigger').bind('click',function(){}} 如果調用bind之后,新建了另一個li節點,則該節點的click事件不會被監聽. jQuery的delegate機制可以將監聽函數注冊到父節點上, 子節點上觸發的事件會根據selector被自動派發到相應的handlerFn上. 這樣一來現在注冊就可以監聽未來創建的節點. $('#myList').delegate('li.trigger', 'click', handlerFn); 最近jQuery1.7中統一了bind, live和delegate機制, 天下一統, 只有on/off. $('li.trigger’).on('click', handlerFn); // 相當于bind $('#myList’).on('click', 'li.trigger', handlerFn); // 相當于delegate 8. 動畫隊列:全局時鐘協調 拋開jQuery的實現不談, 先考慮一下如果我們要實現界面上的動畫效果, 到底需要做些什么? 比如我們希望將一個div的寬度在1秒鐘之內從100px增加到200px. 很容易想見, 在一段時間內我們需要不時的去調整一下div的寬度, [同時]我們還需要執行其他代碼. 與一般的函數調用不同的是, 發出動畫指令之后, 我們不能期待立刻得到想要的結果, 而且我們不能原地等待結果的到來. 動畫的復雜性就在于:一次性表達之后要在一段時間內執行,而且有多條邏輯上的執行路徑要同時展開, 如何協調? 偉大的艾薩克.牛頓爵士在《自然哲學的數學原理》中寫道:"絕對的、真正的和數學的時間自身在流逝著". 所有的事件可以在時間軸上對齊, 這就是它們內在的協調性. 因此為了從步驟A1執行到A5, 同時將步驟B1執行到B5, 我們只需要在t1時刻執行[A1, B1], 在t2時刻執行[A2,B2], 依此類推. t1 | t2 | t3 | t4 | t5 ... A1 | A2 | A3 | A4 | A5 ... B1 | B2 | B3 | B4 | B5 ... 具體的一種實現形式可以是 A. 對每個動畫, 將其分裝為一個Animation對象, 內部分成多個步驟. animation = new Animation(div,"width",100,200,1000, 負責步驟切分的插值函數,動畫執行完畢時的回調函數); B. 在全局管理器中注冊動畫對象 timerFuncs.add(animation); C. 在全局時鐘的每一個觸發時刻, 將每個注冊的執行序列推進一步, 如果已經結束, 則從全局管理器中刪除. for each animation in timerFuncs if(!animation.doOneStep()) timerFuncs.remove(animation)
解決了原理問題,再來看看表達問題, 怎樣設計接口函數才能夠以最緊湊形式表達我們的意圖? 我們經常需要面臨的實際問題: A. 有多個元素要執行類似的動畫 B. 每個元素有多個屬性要同時變化 C. 執行完一個動畫之后開始另一個動畫 jQuery對這些問題的解答可以說是榨盡了js語法表達力的最后一點剩余價值. $('input') .animate({left:'+=200px',top:'300'},2000) .animate({left:'-=200px',top:20},1000) .queue(function(){ // 這里dequeue將首先執行隊列中的后一個函數,因此alert("y") $(this).dequeue(); alert('x'); }) .queue(function(){ alert("y"); // 如果不主動dequeue, 隊列執行就中斷了,不會自動繼續下去. $(this).dequeue(); });
A. 利用jQuery內置的selector機制自然表達對一個集合的處理. B. 使用Map表達多個屬性變化 C. 利用微格式表達領域特定的差量概念. '+=200px'表示在現有值的基礎上增加200px D. 利用函數調用的順序自動定義animation執行的順序: 在后面追加到執行隊列中的動畫自然要等前面的動畫完全執行完畢之后再啟動. jQuery動畫隊列的實現細節大概如下所示, A. animate函數實際是調用queue(function(){執行結束時需要調用dequeue,否則不會驅動下一個方法}) queue函數執行時, 如果是fx隊列, 并且當前沒有正在運行動畫(如果連續調用兩次animate,第二次的執行函數將在隊列中等待),則會自動觸發dequeue操作, 驅動隊列運行. 如果是fx隊列, dequeue的時候會自動在隊列頂端加入"inprogress"字符串,表示將要執行的是動畫. B. 針對每一個屬性,創建一個jQuery.fx對象。然后調用fx.custom函數(相當于start)來啟動動畫。 C. custom函數中將fx.step函數注冊到全局的timerFuncs中,然后試圖啟動一個全局的timer. timerId = setInterval( fx.tick, fx.interval ); D. 靜態的tick函數中將依次調用各個fx的step函數。step函數中通過easing計算屬性的當前值,然后調用fx的update來更新屬性。 E. fx的step函數中判斷如果所有屬性變化都已完成,則調用dequeue來驅動下一個方法。
很有意思的是, jQuery的實現代碼中明顯有很多是接力觸發代碼: 如果需要執行下一個動畫就取出執行, 如果需要啟動timer就啟動timer等. 這是因為js程序是單線程的,真正的執行路徑只有一條,為了保證執行線索不中斷, 函數們不得不互相幫助一下. 可以想見, 如果程序內部具有多個執行引擎, 甚至無限多的執行引擎, 那么程序的面貌就會發生本質性的改變. 而在這種情形下, 遞歸相對于循環而言會成為更自然的描述. 9. promise模式:因果關系的識別 現實中,總有那么多時間線在獨立的演化著, 人與物在時空中交錯,卻沒有發生因果. 軟件中, 函數們在源代碼中排著隊, 難免會產生一些疑問, 憑什么排在前面的要先執行? 難道沒有它就沒有我? 讓全宇宙喊著1,2,3齊步前進, 從上帝的角度看,大概是管理難度過大了, 于是便有了相對論. 如果相互之間沒有交換信息, 沒有產生相互依賴, 那么在某個坐標系中順序發生的事件, 在另外一個坐標系中看來, 就可能是顛倒順序的. 程序員依葫蘆畫瓢, 便發明了promise模式. promise與future模式基本上是一回事,我們先來看一下java中熟悉的future模式. futureResult = doSomething(); ... realResult = futureResult.get(); 發出函數調用僅僅意味著一件事情發生過, 并不必然意味著調用者需要了解事情最終的結果. 函數立刻返回的只是一個將在未來兌現的承諾(Future類型), 實際上也就是某種句柄. 句柄被傳來傳去, 中間轉手的代碼對實際結果是什么,是否已經返回漠不關心. 直到一段代碼需要依賴調用返回的結果, 因此它打開future, 查看了一下. 如果實際結果已經返回, 則future.get()立刻返回實際結果, 否則將會阻塞當前的執行路徑, 直到結果返回為止. 此后再調用future.get()總是立刻返回, 因為因果關系已經被建立, [結果返回]這一事件必然在此之前發生, 不會再發生變化. future模式一般是外部對象主動查看future的返回值, 而promise模式則是由外部對象在promise上注冊回調函數. function getData(){ return $.get('/foo/').done(function(){ console.log('Fires after the AJAX request succeeds'); }).fail(function(){ console.log('Fires after the AJAX request fails'); }); } function showDiv(){ var dfd = $.Deferred(); $('#foo').fadeIn( 1000, dfd.resolve ); return dfd.promise(); } $.when( getData(), showDiv() ) .then(function( ajaxResult, ignoreResultFromShowDiv ){ console.log('Fires after BOTH showDiv() AND the AJAX request succeed!'); // 'ajaxResult' is the server’s response }); jQuery引入Deferred結構, 根據promise模式對ajax, queue, document.ready等進行了重構, 統一了異步執行機制. then(onDone, onFail)將向promise中追加回調函數, 如果調用成功完成(resolve), 則回調函數onDone將被執行, 而如果調用失敗(reject), 則onFail將被執行. when可以等待在多個promise對象上. promise巧妙的地方是異步執行已經開始之后甚至已經結束之后,仍然可以注冊回調函數 someObj.done(callback).sendRequest() vs. someObj.sendRequest().done(callback) callback函數在發出異步調用之前注冊或者在發出異步調用之后注冊是完全等價的, 這揭示出程序表達永遠不是完全精確的, 總存在著內在的變化維度. 如果能有效利用這一內在的可變性, 則可以極大提升并發程序的性能. promise模式的具體實現很簡單. jQuery._Deferred定義了一個函數隊列,它的作用有以下幾點: A. 保存回調函數。 B. 在resolve或者reject的時刻把保存著的函數全部執行掉。 C. 已經執行之后, 再增加的函數會被立刻執行。 一些專門面向分布式計算或者并行計算的語言會在語言級別內置promise模式, 比如E語言. def carPromise := carMaker <- produce("Mercedes"); def temperaturePromise := carPromise <- getEngineTemperature() ... when (temperaturePromise) -> done(temperature) { println(`The temperature of the car engine is: $temperature`) } catch e { println(`Could not get engine temperature, error: $e`) } 在E語言中, <-是eventually運算符, 表示最終會執行, 但不一定是現在. 而普通的car.moveTo(2,3)表示立刻執行得到結果. 編譯器負責識別所有的promise依賴, 并自動實現調度. 10. extend: 繼承不是必須的 js是基于原型的語言, 并沒有內置的繼承機制, 這一直讓很多深受傳統面向對象教育的同學們耿耿于懷. 但繼承一定是必須的嗎? 它到底能夠給我們帶來什么? 最純樸的回答是: 代碼重用. 那么, 我們首先來分析一下繼承作為代碼重用手段的潛力. 曾經有個概念叫做"多重繼承", 它是繼承概念的超級賽亞人版, 很遺憾后來被診斷為存在著先天缺陷, 以致于出現了一種對于繼承概念的解讀: 繼承就是"is a"關系, 一個派生對象"is a"很多基類, 必然會出現精神分裂, 所以多重繼承是不好的. class A{ public: void f(){ f in A } } class B{ public: void f(){ f in B } } class D: public A, B{} 如果D類從A,B兩個基類繼承, 而A和B類中都實現了同一個函數f, 那么D類中的f到底是A中的f還是B中的f, 抑或是A中的f+B中的f呢? 這一困境的出現實際上源于D的基類A和B是并列關系, 它們滿足交換律和結合律, 畢竟,在概念層面上我們可能難以認可兩個任意概念之間會出現從屬關系. 但如果我們放松一些概念層面的要求, 更多的從操作層面考慮一下代碼重用問題, 可以簡單的認為B在A的基礎上進行操作, 那么就可以得到一個線性化的結果. 也就是說, 放棄A和B之間的交換律只保留結合律, extends A, B 與 extends B,A 會是兩個不同的結果, 不再存在詮釋上的二義性. scala語言中的所謂trait(特性)機制實際上采用的就是這一策略. 面向對象技術發明很久之后, 出現了所謂的面向方面編程(AOP), 它與OOP不同, 是代碼結構空間中的定位與修改技術. AOP的眼中只有類與方法, 不知道什么叫做意義. AOP也提供了一種類似多重繼承的代碼重用手段, 那就是mixin. 對象被看作是可以被打開,然后任意修改的Map, 一組成員變量與方法就被直接注射到對象體內, 直接改變了它的行為. prototype.js庫引入了extend函數, Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; } 就是Map之間的一個覆蓋運算, 但很管用, 在jQuery庫中也得到了延用. 這個操作類似于mixin, 在jQuery中是代碼重用的主要技術手段---沒有繼承也沒什么大不了的.
11. 名稱映射: 一切都是數據 代碼好不好, 循環判斷必須少. 循環和判斷語句是程序的基本組成部分, 但是優良的代碼庫中卻往往找不到它們的蹤影, 因為這些語句的交織會模糊系統的邏輯主線, 使我們的思想迷失在疲于奔命的代碼追蹤中. jQuery本身通過each, extend等函數已經極大減少了對循環語句的需求, 對于判斷語句, 則主要是通過映射表來處理. 例如, jQuery的val()函數需要針對不同標簽進行不同的處理, 因此定義一個以tagName為key的函數映射表 valHooks: { option: {get:function(){}}} 這樣在程序中就不需要到處寫 if(elm.tagName == 'OPTION'){ return ...; }else if(elm.tagName == 'TEXTAREA'){ return ...; } 可以統一處理 (valHooks[elm.tagName.toLowerCase()] || defaultHandler).get(elm); 映射表將函數作為普通數據來管理, 在動態語言中有著廣泛的應用. 特別是, 對象本身就是函數和變量的容器, 可以被看作是映射表. jQuery中大量使用的一個技巧就是利用名稱映射來動態生成代碼, 形成一種類似模板的機制. 例如為了實現myWidth和myHeight兩個非常類似的函數, 我們不需要 jQuery.fn.myWidth = function(){ return parseInt(this.style.width,10) + 10; } jQuery.fn.myHeight = function(){ return parseInt(this.style.height,10) + 10; } 而可以選擇動態生成 jQuery.each(['Width','Height'],function(name){ jQuery.fn['my'+name] = function(){ return parseInt(this.style[name.toLowerCase()],10) + 10; } }); 12. 插件機制:其實我很簡單 jQuery所謂的插件其實就是$.fn上增加的函數, 那這個fn是什么東西? (function(window,undefined){ // 內部又有一個包裝 var jQuery = (function() { var jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context, rootjQuery ); } .... // fn實際就是prototype的簡寫 jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function( selector, context, rootjQuery ) {... } } // 調用jQuery()就是相當于new init(), 而init的prototype就是jQuery的prototype jQuery.fn.init.prototype = jQuery.fn; // 這里返回的jQuery對象只具備最基本的功能, 下面就是一系列的extend return jQuery; })(); ... // 將jQuery暴露為全局對象 window.jQuery = window.$ = jQuery; })(window); 顯然, $.fn其實就是jQuery.prototype的簡寫. 無狀態的插件僅僅就是一個函數, 非常簡單. // 定義插件 (function($){ $.fn.hoverClass = function(c) { return this.hover( function() { $(this).toggleClass(c); } ); }; })(jQuery); // 使用插件 $('li').hoverClass('hover'); 對于比較復雜的插件開發, jQuery UI提供了一個widget工廠機制, $.widget("ui.dialog", { options: { autoOpen: true,... }, _create: function(){ ... }, _init: function() { if ( this.options.autoOpen ) { this.open(); } }, _setOption: function(key, value){ ... } destroy: function(){ ... } }); 調用 $('#dlg').dialog(options)時, 實際執行的代碼基本如下所示: this.each(function() { var instance = $.data( this, "dialog" ); if ( instance ) { instance.option( options || {} )._init(); } else { $.data( this, "dialog", new $.ui.dialog( options, this ) ); } } 可以看出, 第一次調用$('#dlg').dialog()函數時會創建窗口對象實例,并保存在data中, 此時會調用_create()和_init()函數, 而如果不是第一次調用, 則是在已經存在的對象實例上調用_init()方法. 多次調用$('#dlg').dialog()并不會創建多個實例.
13. browser sniffer vs. feature detection 瀏覽器嗅探(browser sniffer)曾經是很流行的技術, 比如早期的jQuery中 jQuery.browser = { version:(userAgent.match(/.+(?:rv|it|ra|ie)[/: ]([d.]+)/) || [0,'0'])[1], safari:/webkit/.test(userAgent), opera:/opera/.test(userAgent), msie:/msie/.test(userAgent) && !/opera/.test(userAgent), mozilla:/mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent) }; 在具體代碼中可以針對不同的瀏覽器作出不同的處理 if($.browser.msie) { // do something } else if($.browser.opera) { // ... }
但是隨著瀏覽器市場的競爭升級, 競爭對手之間的互相模仿和偽裝導致userAgent一片混亂, 加上Chrome的誕生, Safari的崛起, IE也開始加速向標準靠攏, sniffer已經起不到積極的作用. 特性檢測(feature detection)作為更細粒度, 更具體的檢測手段, 逐漸成為處理瀏覽器兼容性的主流方式. jQuery.support = { // IE strips leading whitespace when .innerHTML is used leadingWhitespace: ( div.firstChild.nodeType === 3 ), ... } 只基于實際看見的,而不是曾經知道的, 這樣更容易做到兼容未來.
14. Prototype vs. jQuery prototype.js是一個立意高遠的庫, 它的目標是提供一種新的使用體驗,參照Ruby從語言級別對javascript進行改造,并最終真的極大改變了js的面貌。$, extends, each, bind...這些耳熟能詳的概念都是prototype.js引入到js領域的. 它肆無忌憚的在window全局名字空間中增加各種概念, 大有誰先占坑誰有理, 舍我其誰的氣勢. 而jQuery則扣扣索索, 抱著比較實用化的理念, 目標僅僅是write less, do more而已. 不過等待激進的理想主義者的命運往往都是壯志未酬身先死. 當prototype.js標志性的bind函數等被吸收到ECMAScript標準中時, 便注定了它的沒落. 到處修改原生對象的prototype, 這是prototype.js的獨門秘技, 也是它的死穴. 特別是當它試圖模仿jQuery, 通過Element.extend(element)返回增強對象的時候, 算是徹底被jQuery給帶到溝里去了. prototype.js與jQuery不同, 它總是直接修改原生對象的prototype, 而瀏覽器卻是充滿bug, 謊言, 歷史包袱并夾雜著商業陰謀的領域, 在原生對象層面解決問題注定是一場悲劇. 性能問題, 名字沖突, 兼容性問題等等都是一個幫助庫的能力所無法解決的. Prototype.js的2.0版本據說要做大的變革, 不知是要與歷史決裂, 放棄兼容性, 還是繼續掙扎, 在夾縫中求生.
1. C語言抽象出了軟件所在的領域(domain): 由變量v1,v2,...和函數f1,f2,...組成的空間
2. 面向對象(OOP)指出,在這一領域上可以建立分組(group)結構:一組相關的變量和函數構成一個集合,我們稱之為對象(Object)。同時在分組結構上可以定義一個運算(推理)關系: D > B, 派生類D從基類B繼承(inheritance),相應的派生對象符合基類對象所滿足的所有約束。推理是有價值的,因為根據 D > B, B > A 可以自動推導出 D > A,所有針對A的斷言在理論上對D都成立(這也就是我們常說的“派生對象 is a 基類對象”)。編譯器也能有點智能了。
一個有趣的地方是,D > B意味著在D和B之間存在著某種差異,但是我們卻無法把它顯式的表達出來!也就是說在代碼層面上我們無法明確表達 D - B是什么。為了把更多的信息不斷的導入到原有系統中,面向對象內置提供的方法是建立不斷擴展的類型樹,類型樹每增長一層,就可以多容納一些新的信息。這是一種金字塔式的結構,只不過是一種倒立的金字塔,最終基點會被不斷增長的結構壓力所壓垮。
3. 組件技術(Component)本質上是在提倡面向接口(interface),然后通過接口之間的組合(Composition)而不是對象之間的繼承(inheritance)來構造系統。基于組合的觀念相當于是定義了運算關系:D = B + C。終于,我們勉強可以在概念層面上做加法了。
組件允許我們隨意的組合,按照由簡單到復雜的方向構造系統,但是組件構成的成品之間仍然無法自由的建立關系。這意味著組件組裝得到的成品只是某種孤立的,偶然的產物。
F = A + B + C ? G = A + D + C。
4. 在數學上,配備了加法運算的集合構成半群,如果要成為群(Group),則必須定義相應的逆運算:減法。 群結構使得大粒度的結構變換成為可能。
F = A + B + C = A + D - D + B + C = (A + D + C) - D + B = G - D + B
在不破壞原有代碼的情況下,對原有系統功能進行增刪,這就是面向切面(AOP)技術的全部價值。
業務架構平臺的設計與實現要比普通業務系統困難很多。一個核心難點在于如何建立普遍有效的應用程序模型,如何控制各種偶然性的業務需求對系統整體架構的沖擊。大多數現有的業務架構平臺都是提供了一個龐大的萬能性產品,它預料到了所有可能在業務系統開發中出現的可能性,并提供了相應的處理手段。業務系統開發人員的能力被限定在業務架構平臺所允許的范圍之內。如果業務架構平臺的復雜度為A+,則我們最多只能用它來開發復雜度為A的業務系統。一個典型的特征就是使用業務架構平臺的功能配置非常簡單,但是要開發相應的功能特性則非常困難,而且必須采用與業務系統開發完全不同的技術手段和開發方式。
采用業務架構平臺來開發業務系統,即使看似開發工作量小,最終產生的各類配置代碼量也可能會大大超過普通手工編程產生的代碼量,這意味著平臺封裝了業務內在的復雜性,還是意味著平臺引入了不必要的復雜性?很多業務架構平臺的賣點都是零代碼的應用開發,低水平的開發人員也可以主導的開發,但是為什么高水平的程序員不能借助于這些開發平臺極大的提高生產率?
一般的業務架構平臺無法回答以下問題:
1) 業務系統可以通過使用設計工具來重用業務架構平臺已經實現的功能,但是業務系統內部大量相似的模型配置如何才能夠被重用?
2) 特定的業務領域中存在著大量特殊的業務規則,例如“審批串行進行,每一步都允許回退到上一步,而且允許選擇跳轉到任意后一步”。這些規則如何才能夠被引入設計工具,簡化配置過程?
3) 已經開發好的業務系統作為產品來銷售的時候,如何應對具體客戶的定制化?如果按照客戶要求修改配置,則以后業務系統自身是否還能夠實現版本升級?
Witrix平臺提供的基本開發模型為
App = Biz aop-extends Generator<DSL>
在這一圖景下,我們就可以回答以上三個問題:
1) 業務模型通過領域特定語言(DSL)來表達,因此可以使用語言中通用的繼承或者組件抽象機制來實現模型重用。
2) 推理機對于所有推理規則一視同仁,特殊的業務規則與通用的業務規則一樣都可以參與推理過程,并且一般情況下特殊的業務規則更能夠大幅簡化系統實現結構。
3) 相對于原始模型的修改被獨立出來,然后應用面向切面(AOP)技術將這些特定代碼織入到原始模型中。原始模型與差異修改相互分離,因此原始模型可以隨時升級。
Witrix平臺所強調的不是強大的功能,而是一切表象之后的數學規律。Witrix平臺通過少數基本原理的反復應用來構造軟件系統,它本身就是采用平臺技術構造的產物。我們用復雜度為A的工具制造復雜度為A+的產品,然后進一步以這個復雜度為A+的產品為工具來構造復雜度為A++的產品。
一種技術思想如果確實能夠簡化編程,有效降低系統構造的復雜性,那么它必然具有某種內在的數學解釋。反之,無論一種技術機制顯得如何華麗高深,如果它沒有
清晰的數學圖象,那么就很難證明自身存在的價值。對于模型驅動架構(MDA),我長期以來一直都持有一種批判態度。(Physical Model
Driven http://canonical.javaeye.com/blog/29412
)。原因就在于“由工具自動實現從平臺無關模型(PIM)向平臺相關模型(PSM)的轉換”這一圖景似乎只是想把系統從實現的泥沼中拯救出來,遮蔽特定語
言,特定平臺中的偶然的限制條件,并沒有觸及到系統復雜性這一核心問題。而所謂的可視化建模充其量不過是說明人類超強的視覺模式識別能力使得我們可以迅速
識別系統全景圖中隱含的整體結構,更快的實現對系統結構的理解,并沒有證明系統復雜性有任何本質性的降低。不過如果我們換一個視角,
不把模型局限為某種可視化的結構圖,而將它定義為某種高度濃縮的領域描述,
則模型驅動基本上等價于根據領域描述自動推導得到最終的應用程序。沿著這一思路,Witrix平臺中的很多設計實際上可以被解釋為模型定義,模型推導以及
模型嵌入等方面的探索。這些具體技術的背后需要的是比一般MDA思想更加精致的設計原理作為支撐。我們可以進行如下抽象分析。(Witrix架構分析 http://canonical.javaeye.com/blog/126467
)
1. 問題復雜?線性切分是削減問題規模(從而降低問題復雜性)的通用手段,例如模塊(Module)。(軟件中的分析學 http://canonical.javaeye.com/blog/33885
)
App = M1 + M2 + M3 +
2. 分塊過多?同態映射是系統約化的一般化策略,例如多態(polymorphism)。(同構與同態:認識同一性 http://canonical.javaeye.com/admin/blogs/340704
)
(abc,abb,ade, ) -> [a], (bbb, bcd,bab, ) -> [b]
3. 遞歸使用以上兩種方法,將分分合合的游戲進行到底,推向極致。
4. 以少控多的終極形態?如果存在,則構成輸入與輸出之間的非線性變換(輸入中局部微小變化將導致輸出中大范圍明顯的變化)。
App = F(M)
5.
變換函數F可以被詮釋為解釋器(Interpreter)或者翻譯機,例如工作流引擎將工作流描述信息翻譯為一步步的具體操作,工作流描述可以看作是由底
層引擎支撐的,在更高的抽象層面上運行的領域模型。但是在這種觀點下,變換函數F似乎是針對某種特定模型構造的,引擎內部信息傳導的途徑是確定的,關注的
重點始終在模型上,那么解釋器自身應該如何被構造出來呢?
App ~ M
6.
另外一種更加開放的觀點是將變換函數F看作是生成器(Generator)或者推理機。F將根據輸入的信息,結合其他知識,推理生成一系列新的命題和斷
言。模型的力量源于推導。變換函數F本身在系統構造過程中處于核心地位,M僅僅是觸發其推理過程的信息源而已。F將榨干M的最后一點剩余價值,所有根據M
能夠確定的事實將被自動實現,而大量單靠M自身的信息無法判定的命題也可以結合F內在的知識作出判斷。生成器自身的構造過程非常簡單--只要不斷向推理系
統中增加新的推理規則即可。語言內置的模板機制(template)及元編程技術(meta
programming),或者跨越語言邊界的代碼生成工具都可以看作是生成器的具體實例。(關于代碼生成和DSL http://canonical.javaeye.com/blog/275015
)
App = G<M>
7. 生成器G之所以可以被獨立實現,是因為我們可以實現相對知識與絕對知識的分離, 這也正是面向對象技術的本質所在。(面向對象之形式系統 http://canonical.javaeye.com/blog/37064
)
G<M> ==> G = {m => m.x(a,b,c);m.y(); }
8.
現實世界的不完美,就在于現實決不按照我們為其指定的理想路線前進。具體場景中總是存在著大量我們無法預知的“噪聲”,它們使得任何在“過去”確立的方程
都無法在“未來”保持持久的平衡。傳統模型驅動架構的困境就在于此。我們可以選擇將模型M和生成器G不斷復雜化,容納越來越多的偶然性,直至失去對模型整
體結構的控制力。另外一種選擇是模型在不斷膨脹,不斷提高覆蓋能力的過程中,不斷的空洞化,產生大量可插入(plugin)的接入點,最終喪失模型的推理
能力,退化成為一種編碼規范。Witrix平臺中采用的是第三種選擇:模型嵌入--模型中的多余信息被不斷清洗掉,模型通過精煉化來突出自身存在的合理
性,成為更廣泛的運行環境中的支撐骨架。(結構的自足性 http://canonical.javaeye.com/blog/482620
)
App != G0<M0> , App != G0<M1>, App = G1<M1>
9.
現在的問題是:如何基于一個已經被完美解決的重大問題,來更有效率的搞定不斷出現但又不是重復出現的小問題。現在我們所需要的不是沿著某個維度進行均勻的
切分,而必須是某種有效的降維手段。如果我們可以定義一種投影算子P,
將待解決的問題投射到已經被解決的問題域中,則剩下的補集往往可以被簡化。(主從分解而不是正交分解 http://canonical.javaeye.com/blog/196826
)
dA = App - P[App] = App - G0<M0>
10. 要實現以上微擾分析策略,前提條件是可以定義逆元,并且需要定義一種精細的粘結操作,可以將分散的擾動量極為精確的應用到基礎系統的各處。Witrix平臺的具體實現類似于某種AOP(面向切面編程)技術。(逆元:不存在的真實存在 http://canonical.javaeye.com/blog/325051
)
App = A + D + B = (A + B + C) - C + D = App0 + (-C + D) = G0<M0> + dA
11. 模型驅動并不意味著一個應用只能由唯一的一個模型來驅動,但是如果引入多個不同形式的模型,則必須為如下推理提供具體的技術路徑:
A. 將多個模型變換到共同的描述域
B. 實現多個模型的加和
C. 處理模型之間的沖突并填補模型之間的空白
在Witrix平臺中模型嵌入/組合主要依賴于文本化及編譯期運行等機制。(文本化 http://canonical.javaeye.com/blog/309395
)
App = Ga<Ma> + Gb<Mb> + dA
12.
系統的開發時刻t0和實施時刻t1一般是明確分離的,因此如果我們要建立一個包含開發與實施時刻信息的模型,則這一模型必須是含時的,多階段的。關于時
間,我們所知道的最重要的事實之一是“未來是不可預知的”。在t0時刻建立的模型如果要涵蓋t1時刻的所有變化,則必須做出大量猜測,而且t1時刻距離
t0時刻越遠,猜測的量越大,“猜測有效”這一集合的測度越小,直至為0。延遲選擇是實現含時系統控制的不二法門。
在Witrix平臺中,所有功能特性的實現都包含某種元數據描述或者定制模板,因此結合配置機制以及動態編譯技術既可實現多階段模型。例如對于一個在未來
才能確定的常量數組,我們可以定義一個Excel文件來允許實施人員錄入具體的值,然后通過動態編譯技術在編譯期解析Excel文件,并完成一系列數值映
射運算,最終將其轉化為編譯期存在的一個常量。這一過程不會引入任何額外的運行成本,也不要求任何特定的緩存機制,最終的運行結構與在未來當所有信息都在
位之后再手寫代碼沒有任何區別。(D語言與tpl之編譯期動作 http://canonical.javaeye.com/blog/57244
)
App(t1) = G(t0,t1)<M(t0,t1)> + dA(t0,t1)
13. 級列理論提供了一個演化框架,它指出孤立的模型必須被放在模型級列中被定義,被解釋。(關于級列設計理論 http://canonical.javaeye.com/blog/33824
)
M[n] = G<M[n-1]> + dMn
14.
推理的鏈條會因為任何局部反例的出現而中斷。在任意時空點上,我們能夠斷言的事實有哪些?信息越少,我們能夠確定的事實越少,能夠做出的推論也就越少。現
在流行的很多設計實質上是在破壞系統的對稱性,破壞系統大范圍的結構。比如明明ORM容器已經實現所有數據對象的統一管理,非要將其拆分為每個業務表一個
的DAO接口。很多對靈活性的追求完全沒有搞清楚信息是對不確定性的消除,而不確定性的減少意味著限制的增加,約束的增加。(From Local To
Global http://canonical.javaeye.com/blog/42874
)
組件/構件技術的宣言是生產即組裝,但是組裝有成本,有后遺癥(例如需要額外的膠水或者螺釘)。軟件的本質并不是物質,而是信息,而信息的本質是抽象的規
律。在抽象世界中最有效的生產方式是抽象的運算,運算即生產。組件式開發意味著服從現有規律,熟練應用,而原理性生產則意味著不斷創造新的規律。功能模
塊越多,維護的成本越高,是負擔,而推理機制越多,生產的成本越低,是財富。只有恢復軟件的抽象性,明確把握軟件構造過程內在的數學原理,才能真正釋放軟
件研發的生產力。(從編寫代碼到制造代碼 http://canonical.javaeye.com/blog/333167
)
注解1:很多設計原則其實是在強調軟件由人構造由人理解,軟件開發本質上是人類工程學,需要關注人類的理解力與協作能力。例如共同的建模語言減少交互成本,基于模型減少設計與實現的分離,易讀的代碼比高性能的代碼更重要,做一件事只有唯一的一種方式等。
注解2:生成系統的演繹遠比我們想象的要深刻與復雜得多。例如生命的成長可以被看作是在外界反饋下不斷調整的生成過程。
注解3:領域描述是更緊致但卻未必是更本質的表達。人類的DNA如果表達為ATGC序列完全可以拷貝到U盤中帶走,只要對DNA做少量增刪,就可以實現老
鼠到人類的變換(人類和老鼠都有大約30000條基因,其中約有80%的基因是“完全一樣的”,大約共享有99%的類似基因),但是很難認為人類所有智慧
的本質都體現在DNA中,DNA看起來更像是某種序列化保存形式而已。
注解4:模型轉換這一提法似乎是在強調模型之間的同構對應,轉換似乎總是可以雙向進行的,僅僅是實現難度限制了反向轉換而已。但是大量有用的模型變換卻是單向的,變換過程要求不斷補充新的信息。
注解5:模型驅動在很多人看來就是數據庫模型或者對象模型驅動系統界面運行,但實際上模型可以意指任意抽象知識。雖然在目前業內廣泛流行的對象范式下,所
有知識都可以表達為對象之間的關聯,但是對象關系(名詞之間的關聯關系)對運算結構的揭示是遠遠不夠充分的。很多時候所謂的領域模型僅僅是表明概念之間具
有相關性,但是如果不補充一大段文字說明,我們對于系統如何運作仍然一知半解。數學分析其實是將領域內在的意義抽空,僅余下通用的形式化符號。
html主要通過內置的<script>,<link>, <img>等標簽引入外部的資源文件,一般的Web框架并沒有對這些資源文件進行抽象,因此在實現組件封裝時存在一些難以克服的困難。例如一個使用傳統JSP Tag機制實現的Web組件中可能用到js1.js, js2.js和css1.css等文件,當在界面上存在多個同樣的組件的時候,可能會生成多個重復的<script>和<link>標簽調用,這將對頁面性能造成嚴重的負面影響。資源管理應該是一個Web框架的內置組成部分之一。在Witrix平臺中,我們主要借助于tpl模板引擎來輸出html文本, 因此可以通過自定義標簽機制重新實現資源相關的html標簽, 由此來提供如下增強處理功能:
1. 識別contextPath
tpl模板中的所有資源相關標簽都會自動拼接Web應用的contextPath, 例如當contextPath=myApp時
<script src="/a.js"></script> 將最終輸出 <script src="/myApp/a.js" ...>
2. 識別重復裝載
<script src="a.js" tpl:once="true"></script>
tpl:once屬性將保證在頁面中script標簽實際只會出現一次.
3. 識別組件內相對路徑
開發Web組件時,我們希望所有資源文件都應該相對組件目錄進行定位,但是直接輸出的<script>等標簽都是相對于最終的調用鏈接進行相對路徑定位的. 例如在page1.jsp中調用了組件A, 在組件A的實現中, 輸出了<script src="my_control.js"></script>
我們的意圖一般是相對于組件A的實現文件進行定位, 而不是相對于page1.jsp進行定位. tpl模板引擎的相對路徑解析規則為永遠相對于當前文件進行定位. 例如
<c:include src="sub.tpl" />
在sub.tpl中的所有相對路徑都相對于sub.tpl文件進行定位.
4. 編譯期文件有效性檢查
在編譯期, tpl引擎會檢查所有引入的資源文件的有效性. 如果發現資源文件丟失, 將直接拋出異常. 這樣就不用等到上線后才發現文件命名已修改等問題.
5. 緩存控制
瀏覽器缺省會緩存css, js等文件, 因此系統上線后如果修改資源文件可能會造成與客戶端緩存不一致的情況. 一個簡單的處理方式是每次生成資源鏈接的時候都拼接文件的修改日期或者版本號, 這樣既可利用客戶端緩存, 又可以保證總是使用最新版本. 例如
<script src="a.js"></script>將會輸出 <script src="/myApp/myModule/a.js?344566" ...>
6. 字符集選擇
為了簡化國際化處理, 一般提倡的最佳實踐方式是堅持使用UTF-8編碼. 但是很多情況下可能使用系統內置的GBK編碼會更加方便一些, 另外集成一些既有代碼時也存在著不同字符集的問題. 在Witrix平臺中, 所有輸出的資源標簽都會標明對應的字符集, 如果沒有明確設置就取系統參數中的缺省字符集.
例如 <script src="a.js"></script> 將會輸出 <script ... charset="GBK"></script>
7. 缺省theme支持
為了支持多種頁面風格, 往往不是簡單的替換css文件即可實現的, 它可能意味著整個組件的實現代碼的更換. Witrix平臺中通過一系列缺省判斷來簡化這一過程. 例如如下代碼表明如果設置了ui_theme系統參數, 并且對應的特殊實現存在, 則使用特殊實現, 否則系統缺省實現.
<c:include src="${cp:ui_theme()}/ctl_my_ctl.tpl" >
<c:include src="default/ctl_my_ctl.tpl" />
</c:include>
AOP(Aspect Oriented Programming)早已不是什么新鮮的概念,但有趣的是,除了事務(transaction), 日志(Log)等寥寥幾個樣板應用之外,我們似乎找不到它的用武之地。http://canonical.javaeye.com/blog/34941
很多人的疑惑是我直接改代碼就行了,干嗎要用AOP呢?AOP的定義和實現那么復雜,能夠提供什么特異的價值呢?
Witrix平臺依賴于AOP概念來完成領域模型抽象與模型變換,但是在具體的實現方式上,卻與常見的AOP軟件包有著很大差異。http://canonical.javaeye.com/blog/542622
AOP的具體技術內容包括定位和組裝兩個部分。簡化切點定位方式和重新規劃組裝空間,是Witrix中有效使用AOP技術的前提。
在Witrix平臺中,對于AOP技術的一種具體應用是支持產品的二次開發。在產品的實施過程中,經常需要根據客戶的特定需求,修改某些函數的實現。我們
可以選擇在主版本代碼中不斷追加相互糾纏的if-else語句,試圖去包容所有已知和未知的應用場景。我們也可以選擇主版本代碼和定制代碼獨立開發的方
式,主版本代碼實現邏輯框架,定制代碼通過AOP機制與主版本代碼融合,根據具體場景要求對主版本功能進行修正。AOP的這種應用與所謂的橫切概念是有所
區別的。典型的,一個橫切的切點會涉及到很多類的很多方法,而函數定制則往往要求準確定位到某個業務對象的某個特定的業務方法上。傳統AOP技術的切點定
義方式并不適合這種精確的單點定位。在Witrix平臺中,我們通過直接的名稱映射來定義切點。例如,修正spring中注冊的MyObject對象的
myFunc方法,可以在app.aop.xml文件中增加如下標簽
<myObject.myFunc>
在原函數執行之前執行
<aop:Proceed/> <!-- 執行原函數內容 -->
在原函數執行之后執行
</myObject.myFunc>
[spring對象名.方法名]這種映射方法比基于正則字符串匹配的方式要簡單明確的多。spring容器本身已經實現了對象的全局管理功能,spring對象名稱必然是唯一的,公開發布的,相互之間不沖突的,沒有必要再通過匹配運算重新發現出它的唯一性。
對于一些確實存在的橫切需求,我們可以通過Annotation機制來實現切點坐標標定,將復雜的切點匹配問題重新劃歸為[對象名.方法名]。
@AopClass({"myObject","otherObject"})
class SomeClass{
@AopMethod({"myFunc","otherFunc"})
void someFunc(){}
}
針對以上對象,在app.aop.xml文件中可以定義
<I-myObject.I-myFunc>
.
</I-myObject.I-myFunc>
結構的穩定性,直觀的理解起來,就是結構在存在外部擾動的情況下長時間保持某種形式不變性的能力。穩定意味著小的擾動造成的后果也是“小”的。在數學中,Taylor級數為我們描繪了變化傳播的基本圖景。
F(x0 + dx) = F(x0) + F'(x0)*dx + 0.5*F''(x0)*dx^2 + 
擾動dx可能在系統F中引發非常復雜的作用過程,在系統各處產生一個個局部變化結果。表面上看起來,似乎這些變化結果存在著無窮多種可能的分組方式,例如 (F'(x0)-2)*dx + 2*dx^2, 但是基于微分分析,我們卻很容易了解到Taylor級數的每一級都對應著獨立的物理解釋,它們構成自然的分組標準。某一量級下的所有變化匯總歸并到一起,并對應一個明確的整體描述。在抽象的數理空間中,我們具有一種無所不達的變化搜集能力。變化項可以從基礎結構中分離出來,經過匯總后可以對其進行獨立的研究。變化本身并不會直接導致基礎結構的崩潰。
在軟件建模領域,模型的穩定性面臨的卻是另一番場景。一個軟件模型一旦被實現之后,種種局部需求變更就都會形成對原有基礎結構的沖擊。一些局部的需求變化可能造成大片原有實現失效,我們將被迫為類似的需求重新編寫類似的代碼。此時,軟件開發并不像是一種純粹的信息創造,而是宛若某種物質產品的生產(參見從編寫代碼到制造代碼 http://canonical.javaeye.com/blog/333167 )。顯然,我們需要一種能力,將局部變化從基礎結構中剝離出來,經過匯總歸并之后再進行綜合分析和處理。這正是AOP(Aspect Oriented Programming)技術的價值所在。
M1 = (G0+dG0)<M0+dM0> ==> M1 = G0<M0> + dM
AOP本質上是軟件結構空間的自由修正機制。只有結合AOP技術之后,軟件模型才能夠重新恢復抽象的本質,在時間之河中逃離隨機變化的侵蝕,保持實現層面的穩定性。在這一背景下,建模的目的將不是為了能夠跟蹤最終需求的變動,而是要在某個獨立的層面上能夠自圓其說,能夠具有某種獨立存在的完滿性,成為思維上可以把握的某個穩定的基點。模型的真實性將因為自身結構的完備性而得到證明,與外部世界的契合程度不再是價值判斷的唯一標準。 http://canonical.javaeye.com/blog/482620
說到軟件建模,一個常見的論調是模型應該符合實際需求,反映問題的本質。但是何謂本質,卻是沒有先驗定義的。在成功的建立一個模型之前,無論在內涵上還是在外延上我們都很難說清楚一個問題的本質是什么。如果將模型看作是對領域結構的一種顯式描述和表達,我們可以首先考察一下一個“合適”的結構應該具備哪些特征。
按照結構主義哲學的觀點,結構具有三個要素:整體性,具有轉換規律或法則(轉換性),自身調整性(自律性)。整體性意味著結構不能被簡單的切分,其構成要素通過內在的關系運算實現大范圍的關聯與轉換,整體之所以成為整體正是以轉換/運算的第一性為保證的。這種轉換可以是共時的(同時存在的各元素),也可以是歷時的(歷史的轉換構造過程),這意味著結構總要求一個內在的構造過程,在獨立于外部環境的情況下結構具有某種自給自足的特性,不依賴于外部條件即可獨立的存在并保持內在的活動。自律性意味著結構內在的轉換總是維持著某種封閉性和守恒性,確保新的成分在無限地構成而結構邊界卻保持穩定。注意到這里對結構的評判并不是來自外在規范和約束,而是基于結構內在的規律性,所強調的不是結構對外部條件的適應性,而是自身概念體系的完備性。實際上,一個無法直接對應于當前實際環境的結構仍然可能具有重要的價值,并在解決問題的過程中扮演不可或缺的角色。在合理性這個視角下,我們所關注的不僅僅是當前的現實世界,而是所有可能的世界。一個“合理”的結構的價值必能在它所適應的世界中凸現出來。
在信息系統中,我們可能經常會問這個模型是否是對業務的準確描述,是否可以適應需求的變更,是否允許未來的各種擴展等等。但是如果換一個思維方向,我們會發現這些問題都是針對最終確立的模型而發問的,而在模型構建的過程中,那些可被利用的已存在的或者可以存在的模型又是哪些呢。每一個信息模型都對應著某種自動推理機,可以接收信息并做一定的推導綜合工作。一個可行的問題是,如何才能更有效的利用已有的信息進行推導,如何消除冗余并減少各種轉換成本。我們經常可以觀察到,某一信息組織方式更充分的發掘了信息之間的內在關聯(一個表象是它對信息的使用不是簡單的局域化的,而是在多處呈現為互相糾纏的方式,難以被分解),這種內在關聯足夠豐富,以至于我們不依賴于外部因素就可以獨立的理解。這種糾纏在一起的信息塊自然會成為我們建模的對象。
如果模型的“覆蓋能力”不再是我們關注的重點,那么建模的思維圖式將會發生如下的轉化
最終的模型可以由一系列微模型交織構成。模型的遞進構造過程并不同于組件(Component)的實物組裝接口,也不是CAD圖紙堆疊式的架構概念所能容納的。在Witrix平臺中,模型分解和構造表達為如下形式 http://canonical.javaeye.com/blog/333167
Biz[n] = Biz[n+1] aop-extends CodeGenerator<DSLx, DSLy>。
在軟件發展的早期,所有的程序都是特殊構造的,其必然的假設是【在此情況下】,重用不在考慮范圍之內,開發可以說是一個盲目試錯的過程。隨著我們逐步積累了一些經驗,開始自覺的應用理論分析手段,【在所有情況下】都成立的一些普適的原理被揭示出來,它們成為我們在廣闊的未知世界中跋涉時的向導。當我們的足跡漸漸接近領域的邊界,對領域的全貌有一個總體的認知之后,一種對自身成熟性的自信很自然的將我們導向更加領域特定的分析。很多時候,我們會發現一個特化假設可以大大提高信息的利用率,推導出眾多未被顯式設定的知識。我們需要那些【在某些情況下】有效的規則來構成一個完備的模型庫。這就如同有大量備選的數學定理,面對不同的物理現象,我們會從一系列的數學工具中選擇一個進行使用一樣。
軟件開發技術的技術本質在于對代碼結構的有效控制. 我們需要能夠有效的分解/重組代碼片斷, 凸顯設計意圖. 面向對象是目前最常見的代碼組織技術. 典型的, 它可以處理如下模式
A1 --> B2, A2 --> B2, A3 --> B3 ...
我們觀察到A1和A2之間, B2和B2之間具有某種概念關聯性, 同時存在某種抽象結構 [A] --> [B].
對于這種情況, 我們可以定義對象 [A], [B], 它們分別是 A1和A2的聚合, B1和B2的聚合等. 舉例來說, 對于如下表格描述, <ui:Col>所提供的信息在映射為html實現的時候將在多處被應用.
<ui:Table data="${data}">
<ui:Col name="fieldA" label="labelA" width="20" />
<ui:Col name="fieldB" label="labelB" width="10" />
</ui:Table>
這里<ui:Col>提供的信息對應三個部分的內容: 1. 列標題 2. 列樣式(寬度等) 3. 列數據
面向對象的常見做法是抽象出 UiCol對象, 它作為UiTable對象的屬性存在, 在生成表頭, 表列樣式和表格數據內容時將被使用. 但是我們注意到面向對象要求多個方法通過this指針形成狀態耦合
,在某種意義上它意味著所有的成員方法在任一時刻都是同時存在著的。它們所代表著的存在的代價必須被接受(存儲空間等)。即使并不同時被使用,我們仍然需要同時持有所有成員函數指針及
共享的this指針。實際上, 我們并不一定需要A1和A2同時在場. 在這種情況下, 編譯期技術可以提供另一種不同的行為聚合方式.
<table>
<thead>
<sys:CompileTagBody cp:part="thead" />
</thead>
<cols>
<sys:CompileTagBody cp:part="cols" />
</cols>
<tbody>
<sys:CompileTagBody cp:part="tbody" />
</tbody>
</table>
只要<ui:Col>標簽的實現中針對編譯期的cp:part變量進行分別處理, 即可實現信息的部分析取.
html最早的設計目標只是作為某種多媒體文檔展現技術,其設計者顯然無法預料到今天Web應用的蓬勃發展,一些設計缺陷也就難以避免。特別是html規范中缺乏對于復雜交互式組件模型的支持,直接導致企業應用的前臺開發困難重重。AJAX技術可以看作是對這種困境的一種改良性響應,它試圖通過javascript語言在應用層創建并維護一系列復雜的交互機制。很多完善的ajax框架走得相當遙遠,最終基本將html作為一種底層“匯編”語言來使用。例如,一個很整齊美觀的類Excel表格可能是由一個個div拼接而成,與html原生的table元素已經沒有任何關系。
Witrix平臺中對于前臺html模型也作了一定的增強,但基本的設計思想是盡量利用原生控件,并盡量保持原生控件內在的數據關系,而不是重新構建一個完整的底層支撐環境。采用這種設計的原因大致有如下幾點:
1. 前臺技術目前競爭非常激烈,我們優先選擇的方式是集成第三方組件,盡量保持原生環境有利于降低集成成本。
2. 通過javascript構造的控件可能存在性能瓶頸和其他瀏覽器內在的限制。例如一般Ajax框架提供的Grid控件都無法支撐大量單元格的顯示。
3. Witrix平臺的tpl模板技術可以非常方便的生成html文本,并提供強大的控件抽象能力,因此在前臺動態創建并組織界面元素在Witrix平臺中是一種不經濟的做法。
4. Witrix平臺提供的分解機制非常細致,存儲于不同地方的不同來源的代碼會在不同的時刻織入到最終的頁面中,基于原生環境有利于降低平臺快速演進過程中的設計風險。
Witrix平臺中對于html模型的增強主要關注于以最少的代碼實現界面控件與業務邏輯的自然結合。基本結構包括:
1. 通過ControlManager對象在前臺建立一種container結構,統一管理控件的注冊和獲取。js.makeControl(elmOrId)返回特殊注冊的控件對象或者根據原生html元素生成一個包裝對象。
2. 通過js.getWxValue(elm)和js.setWxValue(elm,value)這兩個函數統一對控件的值的存取過程。
3. 通過js.regListener(elm,listenerFunc)統一管理控件之間的相關觸發,實現控件之間的相互監聽。當js.setWxValue(elm,value)被調用時,注冊在ControlManager中的listenerFunc將被調用。
4. stdPage.setFieldValue(fieldName,value)和stdPage.getFieldValue(fieldName,value)統一針對業務字段的值的存取過程,這里fieldName對應于實體上的業務字段名。
5. 通過ajax.addForm(frmId)等函數統一前臺提交參數的提取過程,通過stdPage.buildAjax()等函數統一后臺服務的調用方式。
6. 通過stdPage對象統一封裝業務場景中的"常識"。
基于以上一些基礎機制,Witrix平臺即可提供一些復雜的業務組件封裝。例如<input name="productCode" onkeypress="stdPage.keyPressToLoadRefByCode({objectName:'SomeProduct',queryField:'productCode'})" .../>通過簡單的調用一個js函數即可實現如下功能:
a. 在文本框中輸入回車的時候自動提交到后臺查找對應產品代碼的產品,并更新前臺多個相關字段的值
b. 如果沒有查找到相應產品,則彈出對話框根據界面上已有的部分字段信息提示客戶添加新的產品信息。
c. 如果找到多個對應產品,則彈出列表允許客戶選擇其一。
d. 具體的處理過程可以通過函數參數進行精細的控制。
在meta文件中,結合上下文環境中的元數據信息,我們在缺省情況下可以直接使用 <ds:LoadRefByCodeInputor objectName="SomeProduct" />標簽,不需要任何其他附加參數。
Witrix平臺中一般利用原生控件來保存數據值,而不是將數據保存在分離的js對象中。例如對于一個選擇控件,經常要求選擇得到的是實體的id,而顯示在界面上的是某個其他字段的值。Witrix平臺中一般的實現結構是
<input type="hidden" name="${fieldName}" value="${entity[dsMeta.idField]}" id="${id}" textId="text_${id}" />
<input type="text" value="${entity[dsMeta.nameField]}" id="text_${id}" />
通過textId等擴展屬性即可明確定義控件多個部分之間的關聯關系,同時保證控件的實現完全與html規范相兼容。
Witrix平臺中目前使用的"標準化"的擴展屬性有textId(對應文本顯示控件的id), showName(某些無文字顯示的選擇控件需要保留顯示字段值), op(字段作為查詢條件提交時的比較算符),validator(字段值對應的檢驗函數),setWxValue/getWxValue(重定義控件值的存取行為),serializer(特殊處理前臺控件的提交參數)等。擴展屬性不僅可以引入說明信息,還可以引入豐富的控件行為。
分層是最常見的軟件架構方式之一。分層之后可以區分出橫縱兩個維度,縱向往往表現出一種隔離性。出于有意無意的各種原因,層次之間傳遞信息很容易出現模糊甚至丟失的現象。B/S多層體系架構下的程序因為瀏覽器和服務器之間的狀態空間相互獨立,相對于共享全局狀態空間的C/S程序,更容易出現信息傳遞不暢的問題。實際上,我們經常可以觀察到B/S程序中存在著大量的"接力"代碼,即在交界處,總是存在著大量用于讀取變量,拼接變量,轉換變量等與主體業務無關但卻又不可或缺的代碼。在多層架構程序中,信道構建應該是一個需要給予足夠重視的問題。
在系統規劃中,多層結構應該內置與具體語義無關的通用信道,它跨越多個層次,允許信息透明的通過,并以未預期的方式在不同的層面激發各種相關的行為。在Witrix平臺中,平臺代碼與特定應用中的業務代碼處于高度交織的狀態,一個特定業務功能的實現往往需要多處業務代碼相互協同,平臺必須成為某種透明的背景。例如,假設我們編制了一個通用的列表選擇控件,它封裝的邏輯是從一個實體列表中進行選擇
<app:SelectOne objectName="MyEntity" />
如果現在要求選擇時只列出某個類型的實體,則調用形式為
<app:SelectOne objectName="MyEntity" extArgs="$bizId=select&$type=1" />
在調用入口處補充必要的信息之后會推動系統在遙遠的狀態空間中應用一個特定的過濾條件。這里$bizId負責指示平臺應用特定的元數據配置,而其他的參數則由元數據中的邏輯負責處理。平臺與特定業務代碼各取所需,相互配合,將盡可能多的邏輯剝離為通用機制。
現代數學是建立在等價類這一概念的基礎之上的。同構是對等價關系的一種刻劃。簡單的可以把它理解為兩個系統之間的一種“保持”運算規則的一一對應關系。在
數學中一個符號所代表的是所有能夠互相同構的對象。例如整數3可以看作是與某個元素個數為3的集合可以建立一一對應關系的所有的集合所構成的整體。所以在
數學中,如果我們解決某個特定的問題,它同時也就意味著我們解決了一系列相互等價的問題。
同構關系對于認知可以起到本質上的簡化作用。如果通過一個推理鏈條,確認了A == B == C == D,則可以直接從概念上推導出 A
== D,
這一關系有可能被直觀理解,而不需要理會中間的推理步驟。(注意到以上元素兩兩建立同構關系的時候可能要采用不同的對應手段,因此上面的等式并不是平凡
的。)另一方面,我們可以確定一個模型元素M, 將系統簡化為 A == M, B == M, C == M, D ==
M。只要理解了元素M就理解了等價的其他元素。
Witrix平臺中PDM定義作為基礎的結構模型,它同時映射成為數據庫表,以及hbm, java,
meta等多個代碼文件,此外還對應于約定的WebObject名稱和BizFlow文件名稱,相應的報表文件目錄等。我們只要理解了pdm模型,即可通
過推理自然的掌握各個層面上對應的結構。這意味著只要知道實體名稱,就知道如何通過Web訪問這個對象,知道數據在數據庫中對應的數據庫表,而不需要知道
后臺是如何讀取前臺提交的參數以及如何執行保存數據指令的。不僅僅是在模型驅動領域,在系統設計的各個方面,我們都應該盡量充分的利用當前的信息通過推理
得到系統其他部分的結構,而不是通過手工關聯或者判斷在程序中動態維持這種對應關系。例如在flow-cp機制中,biz的id,
action的id等都根據step配置的id推導得到,這樣在工作列表跳轉的時候就可以根據規則推導出跳轉頁面對應的鏈接,而不需要手工編寫頁面重定向
代碼。

同態(homomorphism)關系相對于同構關系,只強調單向映射的可行性,它是一個舍棄屬性的過程。同態作為最基礎的認知手段之一,它不僅僅是用一
個符號來置換一組元素,而是同時保留了某種全局的運算關系,因此同態映像可以構成某種獨立的完整的研究對象。通過同態映射,我們可以在不同的抽象層面上研
究原系統的一個簡化版本。例如meta中的layout是一種典型的領域特定語言(DSL)。
userName userTitle
emailAddress
每一個字段表示了一個可能任意復雜的inputor或者viewer,
字段之間的前后關系描述了最終顯示頁面上顯示內容的相對關系。當viewer根據需求發生改變的時候,并不影響到layout層面上的關系,因此
layout可以保持不變。如果我們在系統中把問題分解為多個抽象層面上,多個觀察視角上的同態模型,則可能實現更高的軟件復用程度。
在Witrix平臺的設計中,很多細粒度的標簽都定義為tpl文本段,這樣平臺只要理解某一層面上的交互關系,實際應用中可能出現的細節在標簽內部進行局
部處理,不會突破原始設計的形式邊界,不會影響到原先設定的主體系統結構。例如BizFlow中的tpls段,action的source段等。
上世紀50年代以前,生物學家做夢也想象不到具有無限復雜性的生物遺傳過程,竟然可以被抽象為ATGC四個抽象符號的串聯。生命竟然不理會各種已知的或是
未知的物理化學作用,被抽象的三聯碼所驅動。一種抽象的本質似乎成了生命世界的本原。在軟件的世界中,可以被識別的抽象元素絕不只是語言本身所提供的那些
機制。
有一個心理學實驗,要求被試者將青草,公雞,牛三個東西分
成兩組,結果多數中國兒童將青草和牛分成一組,而多數美國兒童將公雞和牛分成一組。中國人的思想中青草和牛之間存在現實的關系,牛吃草,而西方人的典型邏
輯是公雞和牛都屬于動物這一范疇。通過分類將物體類型化,這是西方人從小就接受的訓練。據說美國嬰兒學習名詞的速度要快于動詞,而中國的嬰兒則相反,這并不是偶然的。
中國人的傳統哲學認為世界是普遍聯系的,事物之間存在著禍福相依的辯證轉化關系。而古希臘人強調個體意識,以兩分法看待世界,他們將世界看成是孤立的物體組成(原子論)構成,然后選擇一個孤立物體(脫離背景),開始研究它的各項屬性,接著將屬性泛化,構成分類的基礎。西方語言中大量抽象概念都是從作為屬性的形容詞直接轉化而來,例如
white
-->
whiteness
。而中文中很少有精確的類型定義,而多半是富有表現力的,隱喻性的詞語,例如我們不談論抽象的白,而只說雪白,沒有抽象的
size
,而只說具體的大小。
亞里士多德認為鐵球在空氣中下落是因為它具有“重性”,而木塊在水中漂浮是因為木塊具有“輕性”。這種將一切原因歸結為事物內在屬性的傳統在一定程度上妨礙了西方人認識到背景的存在和作用,但使得他們可以把問題簡化。
古希臘人對于類型的熱衷源于他們對于永恒的迷戀。靜態的亙古不變的世界才是他們的思想棲息的場所。具體的物體是易逝的,多變的,只有抽象的類型才是永恒的存在,也只有抽象概念之間的關系才是永真的聯系。而具體實例之間的關聯在某種程度上被認為是不重要的,甚至是不可靠的。

將具有某一屬性的所有物體定義為一個集合,這一做法在上世紀初被發現會引起邏輯悖論,動搖了整個數學的基礎,它絕不像表面上看起來那么單純。但確定無疑的是,通過類型來把握不變的事實是一種非常重要且有效的認識策略。面向對象語言強調名詞概
念,從引入類定義以及類之間的繼承關系開始,這符合西方一貫的作風。而
Ruby
這種強調實例間關系的動態語言首先由日本人發明,可能也不是偶然的。雖然現在大家都在玩高科技了,可實際販賣給你的多半仍然是包治百病的祖傳秘方。文化可能造成認知上的一種偏執,在技術領域這一現象并沒有被清楚的意識到。
軟件開發作為一種工程技術,它所研究的一個重點就是如何才能有效降低軟件產品的研發成本。在這一方向上,組件技術取得了空前的成功。它所提供的基本圖景是
像搭積木一樣從無到有的組裝出最終的產品。在某種意義上,這是對現代建筑工業的模仿和致敬。新材料,預制件,框架結構,這些建筑學的進展在軟件領域被一一
復制,建筑工地上的民工自然也成為了程序員們學習的楷模。畢竟,在組件的世界中碼代碼,基本上也是一種“搬磚”的行為。
值得慶幸的是,軟件開發作為一種智力活動,它天生具有一種“去民工化”的傾向。信息產品有著與物質世界產品完全不同的抽象本質。在物理空間中,建造100
棟房屋,必須付出100倍的努力,老老實實的干上100遍。而在概念空間中建造100棟房屋,我們卻可以說其他房屋與第一棟一模一樣,加點平移旋轉變換即
可。一塊磚填了地基就不能用來蓋屋頂,而一段寫好的代碼卻可以在任何可用的場合無損耗的被使用。一棟建好的房屋發現水管漏水要大動干戈,而在完成的軟件中
補個局部bug卻是小菜一碟。在抽象的世界中有效的進行生產,所依賴的不應該是大干,苦干的堆砌,而應該是發現某種可用于推導的原理,基于這些原理,輸入
信息可以立刻轉換為最終的結果,而不需要一個逐步構造的過程。即我們有可能超越組裝性生產,實現某種類似于數學的原理性生產。http://canonical.javaeye.com/blog/325051
代碼復用是目前軟件業中鼓吹降低生產成本的主要口號之一。但是在組件技術的指向下,一般所復用的只是用于構建的磚塊,而并不是某種構造原理。即使在所有信
息已經確定的情況下,我們仍然不可能從需求立刻得到可執行的產品。很多代碼即使我們在想象中已經歷歷在目,卻仍然需要一行行把它們謄寫下來。當我們發現系
統中已經沒有任何組件值得抽象的時候,仍然留下來眾多的工作需要機械化的執行。代碼復用的理想國距離我們仍然非常的遙遠。
子例程(subroutine)是最早的代碼重用機制。這就像是將昨天已經完成的工作錄制下來,在需要的時候重新播放。函數(function)相對于子
例程更加強大。很多代碼看起來并不一樣,但是如果把其中的差異部分看作是變量,那么它們的結構就可以統一了。再加上一些判斷和循環,很多面目迥異的東西其
實是存在著大量共享信息的。面向對象技術是一次飛躍性的發展。眾多相關信息被打包到一個名稱(類型)中,復用的粒度和復雜度都大大提升。派生類從基類繼
承,可以通過重載實現對原有代碼的細致調整。不過,這種方式仍然無法滿足日益增長的復用需求。很多時候,一個名稱并不足以標定我們最終需要的代碼結構,在
實際使用的時候還需要補充更多的信息。類型參數化,即泛型技術,從事后的角度看其實是一種明顯的解決方案。根據參數動態的生成基類自然可以吸納更多的變
化。經歷了所謂Modern
C++的發展之后,我們現在已經非常明確,泛型并非僅僅能夠實現類型共變,而是可以從類型參數中引入更豐富的結構信息,它的本質是一種代碼生成的過程。http://canonical.javaeye.com/blog/57244
認清了這一點,它的擴展就非常明顯了
BaseClass<ArgClass> --> CodeGenerator<DSL>
DSL(或者某種模型對象)相對于普通的類型(Class),信息密度要大很多,它可以提供更豐富也更完整的輸入信息。而CodeGenerator也不必拘泥于基礎語言本身提供的各種編譯機制,而是可以靈活應用各種文本生成技術。http://canonical.javaeye.com/blog/309395
CodeGenerator在這里所提供的正是根據輸入模型推導出完整實現代碼的構造原理。
現在很多人熱衷于開發自己的簡易代碼生成工具,這些工具也許可以在簡單的情形下減輕一些體力工作,但是生成的代碼一般不能直接滿足需求,仍然需要手工執行
大量的刪改工作。當代碼生成之后,它成為一種固化的物質產品,不再能夠隨著代碼生成工具的改進而同步改進,在長期的系統演化過程中,這些工具并不一定能夠
減少累積的工作量。
修正過程 ==> CodeGenerator<DSL>
為了改進以上代碼生產過程,一些人試圖在CodeGenerator中引入越來越多的可配置性,將各種變化的可能內置在構造原理中。顯然這會造成CodeGenerator的不正常的腫脹。當更多的偶然性被包含在原理中的時候,必然會破壞原理的簡單性和普適性。
輸入信息 + 一段用于推導的原理 + 修正補充 = 真實模型
必須存在[修正補充]這一項才能維持以上方程的持久平衡。
為了擺脫人工修正過程,將模型調整納入到概念世界中,我們需要超越繼承機制的,更加強大的,新的技術手段。其實在當前的技術背景下,這一技術已經是呼之欲出了。這就是AOP, Aspect Oriented Programming。http://canonical.javaeye.com/blog/34941
Biz ==[AOP extends]==> CodeGenerator<DSL>
繼承僅僅能夠實現同名方法之間的簡單覆蓋,而AOP所代表的技術原理卻是在代碼結構空間中進行任意復雜的刪改操作,它潛在的能力等價于人工調整。
為了實現上述生產模式,需要對編程語言,組件模型,框架設計等方面進行一系列改造。目前通用的AOP實現和元編程技術其實并不足以支持以上模式。http://canonical.javaeye.com/blog/275015
這一生產模式將會如何演化,也是一個有趣的問題。按照級列理論,我們立刻可以得到如下發展方向:
Context0 + DSL1 + EXT0 = DSL0
Context1 + DSL2 + EXT1 = DSL1
http://canonical.javaeye.com/blog/33824
Witrix平臺中BizFlow可以看作是對DaoWebAction的修正模型,但是它本身具有完整的意義,可以直觀的被理解。在BizFlow的基礎上可以逐步建立SeqFlow,StateFlow等模型。http://canonical.javaeye.com/blog/126467
現在有些人試圖深挖函數式語言,利用模式匹配之類的概念,做符號空間的全局優化。但是我們需要認識到通用的機制是很少的,能夠在通用語言背景下被明確提出
的問題更是很少的。只有在特定領域中,在掌握更多局部信息的情況下,我們才能提出豐富的問題,并作出一定背景下的解答。DSL的世界中待做的和可做的工作
很多。http://canonical.javaeye.com/blog/147065
對于程序員而言,未來將變得越來越豐富而復雜,它將持續拷問我們的洞察力。我們不是一行行的編寫代碼,把需求一條條的翻譯到某種實現上,而是不斷發明局部的生產原理,依靠自己制定的規則在抽象的空間中不斷的創造新的表象。
負數沒有直接的幾何意義,因此它被認為是對應于不存在的事物。而按照古希臘的邏輯,不存在的事物是不可能存在的,因而也就是無法被理解的,更不可能參與到
推理過程中,因此是無意義的,無法被定義的,
因此它是不存在的。中國人注重的是運算的合理性,而不是數的真理性,大概在公元前400年左右就創造了負數和零的概念。而在西方直到公元7世紀(唐代)的
一本印度人的著作中才出現負數,它被用來表示負債。西方人沒有能夠創造負數,他們對負數的接受更遲至15世紀左右。這件事實在一定程度上說明了存在某種深
刻的困難阻礙我們理解負數概念。
在引入負數之前,3x^2 + 8 = 4x 和 3x^2 + 4x + 8 = 0
這兩個方程的結構是完全不同的,它們需要不同的求解技術,因此也就不可能利用符號抽象出 a x^2 + b x + c =
0。引入負數才使得我們能夠以統一的方式提出問題,并研究通用的求解技術。
群論(Group Theory)是對結構進行抽象研究的數學分支。群的定義包括四條規則
1. 元素之間的運算滿足結合律 (a * b) * c = a * (b * c)
2. 元素之間的運算封閉,即 a * b 仍然屬于該群
3. 存在單位元,即對所有a, a * e = e*a = a
4. 每個元素存在對應的逆元,a * a^-1= e
逆運算是非常重要的結構要求,逆元是對負數的一種抽象推廣。如果沒有逆元,則只能構成半群(semi-group),它的性質要少很多。
目前軟件設計中所有的原則都指向組裝過程,從無到有,層層累進。構件組裝的隱喻中所包含的圖像是操縱實際可見的積木,是缺少逆元概念的。
考察一個簡單的例子,假設需要的產品是三角形內部挖去一個五邊形的剩余部分。有三種生產策略:
1. 對最終需要的產品形態進行三角剖分,使用8個小三角形拼接出來。這種方式比較繁瑣,而且最后粘接工序的可靠性和精確性值得懷疑。
2. 拿到一個真實的三角形,然后用刀在內部挖出一個五邊形的洞。這種方式需要消耗一定的人工,而且也很難保證五邊形的精確性,即使我們曾經精確的生產過其他五角形和三角形。實際上一般情況下我們是逐步銼出一個五邊形的,并沒有充分利用到五邊形的對稱性。
3. 在概念空間中做一個抽象計算 (-五邊形) + (三角形) = 所需產品
如果我們能夠生產一種負的五邊形和一種正的三角形,則可以立刻得到最終的產品。

在軟件開發的實踐中,我們目前多數采用的是兩種方式:
1. 采用可視化設計工具通過拖拽操作開發出完整的界面和后臺
2. 拷貝一些已有的代碼,刪除掉不需要的部分,增加一些新的實現,也可能對已有實現做一些不兼容的修正。
在第二種方式中
新結構的構造 = 已有結構 + 軟件之外的由人執行的一個剪裁過程
這個剪裁過程表現為一個時間序列。如果我們對原有結構進行了調整,則需要重新關聯一個時間序列,而此時間序列并不會自動重播。為了壓縮以時間為度量單位的
生產成本,我們必須減少對時間序列的依賴。在時間序列中展開的一個構造過程可以被轉化為一個高維設計空間中的一種更加豐富的構造原理,我們最終的觀測可以
看作是設計空間向物理空間的一個投影(想象一束光打下來)。這種方式更容易保證程序的正確性。
時間序列 --[原理轉化]--> 空間關系
這樣我們就可以使用第三種生產策略:利用構造原理進行抽象計算。如果我們只盯著產品的最終形態看,只是想著怎么把它像搭積木一樣搭建出來,就不可能識別出
系統結構本身所蘊含的對稱性。如果我們發現了系統內蘊的結構特征,但是卻只能通過構造過程中的行動序列來追隨它,同樣無法實現有效的工作復用。同時因為這
個行動序列一般處于系統規則約束之外,完全由人的自覺來保障,因此很難保證它的正確性。現實世界的規范要求并不是模型本身所必須滿足的,只要我們能夠創造
新的結構原理,在概念空間中我們就可以擁有更多的自由。現在業內鼓吹的軟件構造原理多半是參照物理世界中生產具體物質產品的生產工序,卻沒有真正把握信息的抽象本質。掌握規則,制訂規則,才是信息空間中的游戲規則。
物理學中最重要的分析學思想之一是微擾論(Perturbation).
針對一個復雜的物理現象,首先建立一個全局的規范的模型,然后考慮各種微擾條件對原有模型的影響。在小擾動情況下,模型的變化部分往往可以被線性化,被局
域化,因而問題得到簡化。微擾分析得到的解依賴于全局模型的解而存在,因而這是一種主從關系的分解方式。但是如果主體模型是我們已經熟知的物理現象,則我
們關注的重點可以全部放在擾動解上,認為所有特定的物理規律都體現在擾動解中。如果微擾分析得到的物理元素足夠豐富,則微擾模型本身可以成為獨立的研究對
象,在其中我們同樣可以發現某種普適的結構規律。
考察如下的構造過程
X = a + b + c
Y = a + b + d = (a + b + c) - c + d = X - c + d
對于數學而言,上述的推導是等價的,但是對于物理學而言,Y = a + b + d 和 Y = X - c +
d是有著本質不同的。第一種方式要求打破原先X的構造,而重新的組裝其實是有成本的,特別是在X本身非常復雜的情況下。典型的,如果X是經過測試的功能,
重新組裝后原先由測試保障的概念邊界被打破。
我們可以從Y = X + dX抽象出擾動模型 dX = - c + d
主從分解模式自然的導出逆元概念。
如果沒有逆元,我們必然需要分解。但是如果發掘了背景這一概念,在逆元運算下,對背景不是分解讓其成為可見的部分,而是采用追加的,增刪的方法對背景結構
進行修正,則我們有可能在沒有完整背景知識的情況下,獨立的理解局部變化的結構。即背景是透明的,知識成為局部的。在Witrix平臺中,BizFlow
+ DaoWebAction + StdPage
才構成完整的程序模型,BizFlow其實是對標準模型的差異描述,但是它可以被單獨的理解。如果我們從接觸程序開始就接受BizFlow,
就可能完全不需要了解數據庫訪問和前臺界面渲染的知識。我們并不是通過在DaoWebAction中設定各種可預見的調用形式,而是在BizFlow中通
過類似AOP的操作方式直接對標準模型進行修正。這種修正中一個很重要的部分就是刪除標準模型中缺省提供的功能。
WebMVC之前世今生 http://canonical.javaeye.com/blog/163196
Witrix架構分析 http://canonical.javaeye.com/blog/126467
變化的部分構成獨立于原始模型的新的模型,它的結構關系是完備的,可以獨立的理解。在原始模型崩潰的情況下,它仍然可能保持有效性。
從物理學的角度看,我們所觀測到的一切物理現象,都是某種物理作用的結果,也就是物質結構相對于背景狀況的一種偏離。我們只可能觀測到變化的部分,因此我們對世界的認識其實只是世界的擾動模型而已,世界的本體不屬于科學研究的范疇。
軟件技術的發展是一個結構化不斷加深的過程,我們逐漸擁有了越來越豐富的結構識別, 表達和處理手段。在這一方向上,
組件技術最終取得了巨大的商業成功。但是區分同時也就意味著隔閡。面向對象技術最基礎的概念在于 O = C(O),
對象的復合(Composition)仍然是對象. 然而在越來越復雜的軟件生態環境中,這一圖景實現的可能性被大大的壓縮.
面對層層封裝造成的形態各異的表達方式, 不同來源, 不同目標, 不同運行環境下的信息的交互變得越來越困難。我們逐漸喪失了概念上的簡潔性,
也喪失了數學世界中的那種穿透一切的統一的力量. 所謂SOA(Serivce Oriented
Architecture)技術試圖通過補充更多的環境信息,放棄狀態關聯,暴露元知識等方式來突破現有的困境。 http://canonical.javaeye.com/blog/33803
這其中一項關鍵的技術抉擇是基于文本格式進行信息表達。
在過去10年中Web技術取得了空前的成功,它造就了互聯網這一世界上最大的分布式集成應用。SOA從某種程度上說是對這一事實在技術層面上的反思。基于
文本傳遞的web技術所表現出來的開放性,可理解性和可觀測性,與封閉的,難以直接解讀的,必須擁有大量相關知識才能正確操作的二進制結構相比,這本身就
是革命性的創新。不需要特殊的工具就可以非常輕易的察看到網頁程序的輸入輸出,所有交互過程都處在完全透明的檢查下,各種不曾事先規劃的文本處理手段都可
以參與到web創建的過程中。隨著速度,存儲不再是我們考慮的首要問題,文本化作為一種技術趨勢逐漸變得明確起來。但是任何一種技術思想的成功或失敗都不
可能是因為某種單一的原因所造成的,因此有必要對文本化的技術價值做更加細致的剖析。
1.
文本化是一定程度上的去結構化,但是通過這種方式我們獲得了對程序結構的更強的控制力。無論我們所關心的文本片斷層層嵌套在大段文本的哪個部分,我們都可
以通過text.indexOf(subStr)來實現定位,通過text.replace(oldStr,newStr)實現變換。而在組件系統中,我
們只有通過某種預定的遍歷方式逐步遞歸才有可能訪問到組件內部的組成對象。如果某個組件不按照規范實現或者規范中缺少明確的設定,則這一信息關聯鏈條將會
斷裂。在一些組件系統中,甚至沒有規定一種統一的遍歷方式,則我們根本無法定義出通用的大范圍的結構定位/變換手段。即使是在設計精良的組件框架中,受限
制于組件的封裝性要求,我們也不可能訪問到全部的信息。當我們以組件設計者意料之外的方式使用這些組件的時候,就會遇到重重障礙。即使所需的只是微小的局
部調整,因為無法觸及到需要修改的部分,我們也可能被迫放棄整個組件。
2.
文本是普適的信道。各種操作系統,各種程序語言所具有的最基本的能力就是文本處理能力,對于文本格式的約定是最容易達成共識的。雖然對于機器而言,理解基
于地址定位的二進制數字可能更加直接,但是所有的應用程序都是由人來負責編制,調試,部署,維護的,如果人可以不借助特殊的工具就可以明確了解到系統內發
生的過程,則系統的構建和維護成本就會大幅下降。
3.
文本表述形式上的冗余性增強了系統的概念穩定性和局部可理解性。在二進制格式中,經常出現的是根據相對地址定位,這要求我們完整理解整個二進制結構,才能
夠逐步定位到局部數據塊。同時,二進制格式中也經常使用一些外部的信息,例如某個位置處的數據為整型,占用四個字節等。這樣的信息可能只能在文檔說明里查
到,而在數據體中沒有任何的體現,這限制了獨立的理解可能性。與此相反,文本格式經常是自說明式的,例如width:3.5px,
這提高了系統的可理解性,特別是局部可理解性。即使我們對系統只具有很少的知識,一般也能根據數據周圍的相關信息進行局部操作。一般很容易就能夠定位到特
殊的局部數據區,安全的跳過眾多未知的或者不被關心的結構. 一個程序新手也可以在很短時間內靠著連蒙帶猜,
實現xml格式的word文件中的書簽替換功能,而要搞清楚word的二進制格式,并獨立編制出正確的替換功能,顯然就不是一兩周的工作量可以解決的了.
這其中, 可理解性的差異是存在著數量級上的鴻溝的.
4. xml這種半結構化的文本格式規范的興起, 以通用的方式為文本描述引入了基本的形式約束, 實現了結構表達的均一性.
C語言中的宏(Macro)本質上就是一種文本替換技術,它的威力在于沒有預定義的語義, 因此可以超越其他語法成分, 破除現有語法無法跨越的限制.
但是它的危險性在于缺乏與其能力相適應的形式約束, 難以控制. 而在xml格式規范下, 不同語義,
不同抽象層面的節點可以共存在同一個形式體系中, 可以用通用的方式進行定位,校驗, 轉換等. Witrix平臺在動態xml方面發展了一系列技術,
為文本處理引入了更多應用相關的規則, 增強了文本描述的抽象能力和表達能力.
5. 文本作為描述(description)而不是執行指令(execution). C語言的源代碼與機器碼基本上是一一對應的,
即源代碼本身所表達的就是指令的執行過程. 而在Web應用領域, HTML語言僅僅是聲明界面上需要展現什么,
但是如何去實現是由通用的解析引擎負責,它并不是我們關注的重點. 描述需要結合詮釋(解釋)才能夠產生實際的運行效果,
才能對現實的物理世界產生影響.這在某種程度上實際上是延遲了執行過程. 一種描述可以對應多種詮釋,
例如同樣的元數據在前臺可以用來生成界面,在后臺可以用于生成數據庫, 進行數據有效性校驗等. 在特定的應用領域中,執行引擎可以是通用的,
可以獨立于描述本身不斷演化, 因此一種領域特定的描述,它所承載的內容并不是固化的, 而是可以隨著執行引擎的升級不斷增強的. 例如,
在Witrix平臺中, FlowDSL本身所做出的流程描述是穩定的,
但是隨著流程引擎不斷改進,不斷引入新的功能,所有使用DSL的已實現的應用都同步得到升級. http://canonical.javaeye.com/blog/275015
6. Text = Process(Text) 這個不動點在Unix系統中得到了充分的應用: 多個小程序通過管道(Pipe)組合在一起,
可以完成相當復雜的功能. 一個更加豐富的處理模型是 XML = Process(XML). 文本描述很自然的支持多趟處理,
它使得我們可以充分利用全局知識(后續的處理過程可以使用前次處理過程收集的全局信息), 可以同時支持多個抽象層面(例如DSL的不斷動態展開).
Witrix平臺中的編譯期運行技術實際上就對應于如下簡單規則: 編譯期運行產生文本輸出, 對輸出文本再次進行編譯. 通過這一遞歸模式,
可以簡單的實現動態解析與靜態描述之間的平衡. 模板(Template)技術是具有關鍵性作用的文本生成技術.
out.write("<div>");out.write(value);out.write("</div>");這種
API拼接方式顯然不如<div>${value}</div>這種模板生成方式直觀且易于使用.
在Witrix平臺的tpl模板語言中, xml的規范性使得在多趟編譯過程中我們一直可以維持某種形式約束.
7. 不是所有的情況下都應該使用文本. 普元EOS中所鼓吹的XML總線之類的技術是我所極力反對的. http://canonical.javaeye.com/blog/33794
代碼生成(Code Generation)本身是一個非常宏大的概念。從某種意義上說,當我們明確了計算的意義之后,所做的一切都只是一系列代碼生成的過程,最終的目標是生成某種可執行的機器碼。對web程序員來說,代碼生成是最熟悉不過的了,每天我們所做的工作就是JSP=>Servlet=>HTML。不過,現在多數人腦海中的代碼生成,指的一般只是根據配置輸出一個或多個程序文本的過程,最常見的是根據數據庫模型生成增刪改查相關代碼。這種技術其實很少在小型以上的項目中起到積極的作用.因為一般的生成工具都沒有實現追加功能,無法適應模型的增量修改。此外一般生成的代碼相比于手工書寫的代碼要更加冗長,需要被直接理解的代碼總量不降反升.為圖一時之快,所要付出的是長期的維護成本。
在應用開發中,有些領域是非常適合于使用代碼生成技術的。例如根據領域模型生成ORM(對象-關系映射)描述,或者根據接口描述生成遠程調用代理/存根 (Proxy/Stub)等。因為它們實際上只是對同一信息的不同技術形式或者不同技術層面的同義反復而已。這種生成最理想的方式是動態進行,可以隨時保持模型的有效性。RoR(RubyOnRails)框架中ActiveRecord技術便是一個成功的范例,它甚至提供了動態生成的DAO函數,減少了一系列的包裝調用過程。
代碼生成更加深刻的應用是完成高層模型向低層模型的轉化,這一過程往往是非平凡(non-trivial)的。在Witrix平臺中通過代碼生成來支持領域抽象,可以用非常低的成本跨越結構障礙,將自定義的領域模型嵌入到現有的技術體系中。這其中我們的主要工作是解決了生成代碼與手工書寫代碼之間的有效隔離及動態融合問題,確保代碼生成可以反復的以增量的方式進行,同時支持最細粒度處對生成的代碼進行定制調整。
舉一個簡單的例子,假設現在需要開發一個三步審批的流程,每一步的操作人可以錄入意見,可以選擇通過或者回退,可以選擇下一步操作的具體操作人,系統自動記錄操作時間,每個操作人可以查看自己的操作歷史等。雖然在現有技術體系中實現這一功能需要不少代碼,但是在業務層面上描述這一功能并不需要很多文字,實際需要提供的信息量很小。顯然,建立領域模型是比較適合的做法,可以定義一種DSL(Domain Specific Language)來描述這一模型。
<flow_cp:SeqFlow>
<step id="draft" userField="draferId" dateField="draftTime" waitStatus="drafted" />
<step id="check" userField="checkerId" dateField="checkTime" opinionField="checkOpinion"
waitStatus="sent" />
<step id="approve" userField="approverId" dateField="approveTime"
opinionField="approveOpinion" waitStatus="checked" passStatus="approved" />
</flow_cp:SeqFlow>
以上功能涉及到多個操作場景,實現的時候需要補充大量具體信息,其中很大一部分信息來自于背景知識,例如顯示樣式,界面布局,前后臺通信方式等。以上模型可以進一步抽象為如下標簽
<flow_cp:StepFlow3/>
在不同應用中復用以上流程邏輯的時候可能需要局部修正,例如
<flow_cp:StepFlow3>
<step id="check" userField="checker" />
</flow_cp:StepFlow3>
更加復雜的情形是DSL本身提供的抽象無法滿足全部需求,而需要在局部補充更多模型之外的信息,例如物品接收單審批通過后自動導入庫存等。
在Witrix中,代碼生成不是直接產生最終的輸出,而是在編譯期生成基礎模型,它與補充描述通過extends算子進行融合運算之后產生最終輸出, 這種融合可以實現基礎功能的新增,更改或者刪除。典型的調用形式為
<biz-flow>
<extends>
<flow_cp:StepFlow3>
<step id="check" userField="checker" />
</flow_cp:StepFlow3>
</extends>

<action id="pass_approve">
.
</action>
</biz-flow>
這里的操作過程可以看作是BizFlow extends SeqFlow<FlowConfig extends StepFlow3Config>,與泛型技術非常類似,只是需要更強的局部結構控制能力。
按照級列理論 http://canonical.javaeye.com/blog/33824 ,我們可以定義一個DSL的級列,整個抽象過程為
Context0 + DSL1 + EXT0 = DSL0
Context1 + DSL2 + EXT1 = DSL1
在目前一些通用語言中,也有一些所謂內嵌DSL的方案,可以提供比較簡潔的業務描述。但是僅僅建立DSL描述是不充分的,從級列理論的觀點看,我們必須提供一種DSL的補充手段,能夠在細節處補充DSL模型之外的信息,實現兩者的自然融合。同時我們應該可以在不同的抽象層面上獨立的進行操作,例如在 DSL1和DSL2的層面上都可以通過類似繼承的操作實現局部調整,這同時也包括在不同的抽象層面上都能對模型進行合法性校驗。
軟件系統的構建之所以與建筑工程不同,無法達到建筑工程的精確性和可控性,其中一個很重要的原因在于建筑的產物是一個靜態的結構,建筑的過程主要是采用各種預制件填充某個規劃好的建筑空間,而軟件是一種動態運行的產品,它的各個組成部分之間的關系不是可以靜態描述的,而是存在著復雜的交互關系,而且軟件在運行的過程中還需要根據需求的變化進行動態的調整,這種動態性使得軟件開發中很難抽象出固定的預制件,很難像建筑工程那樣實現標準件的組裝。現在所謂構件技術的構件插拔圖景其實是具有誤導性的。
但是從另外一方面說,軟件內在的動態性使得它可以具備更強的適應能力,如果所編制的軟件把握住了業務機制的核心內容,則在運行過程中只需要進行少量調整就可以應對大量類似情況。100棟類似的建筑需要花費100倍的建造費用,而100個近似的軟件需求的滿足可能只需要花費2至3倍的開發費用。現代軟件企業在研發過程中都在不斷的追求自身產品的平臺化,其目的正在于以不斷提高的適應性來應對不斷變化的客戶需求。我們所面對的要求不僅僅是精確把握需求,而是要深刻把握需求背后所對應的業務機制。
AOP(Apsect Oriented Programming)概念的正式出現也有一些時日了,但是它在程序構造過程中似乎仍未找到合適的切入點,一般系統的設計實現很少將AOP作為必要的技術元素。AOP作為一種普適的技術思想,它所代表的是程序結構空間中的定位和組裝技術。 http://canonical.javaeye.com/blog/34941 AOP使我們可以通過非侵入性的方式動態修改“任意”已經構建好的程序,而不需要事前有大量的設計準備。原則上說,這種技術思想是可以在任何程序語言基礎上進行表達的,并不是只有java, C#這樣的面向對象語言才允許AOP操作. Witrix平臺中所應用的部分技術與AOP有些類似,只是大量的結構調整表現為xml生成和xml變換,在具體的使用方式上也有一些微妙的差異。 http://canonical.javaeye.com/blog/126467
相對于通用程序語言,xml語言其實是AOP技術的一個更加合適的形式載體。
1. xml格式特殊的規范性確保了在最細的邏輯粒度上,程序結構也是可識別的,可操縱的(在這一點上非常類似于函數式語言)。而所有的命令式語言(imperative language)中,函數內部的結構都是很難采用統一方式進行描述和定位的。
<ns1:loop>
<rpt:Row/>
</ns1:loop>
2. xml節點的坐標可以采用xpath或者css選擇符等通用方式進行描述,而一般程序結構無法達到xml格式這樣的均一性,其中的坐標定位方式要復雜得多。
3. xml節點上可以增加任意屬性,不同的屬性可以屬于不同的命名空間(namespace),這些屬性可以輔助AOP的定位機制。而一般程序語言中如果沒有Annotation機制, 則定位只能依賴于函數名和類名(函數參數只有類型沒有名稱),而類名和函數名隨時可能因為業務變化而調整(不是專為定位而存在), 由此構建的切點描述符是不穩定的。
<ui:PageTable pager="${pager}" cache:timeout="1000" />
4. xml節點的增刪改查顯然要比字節碼生成技術要簡單和直觀得多。
AOP技術難以找到應用的一個重要原因在于很多人機械式的將它定位為一種橫切技術,認為它的價值完全在于某個確定的切面可以插入到多個不同的切點,實現系統的橫向分解。而在實際應用中,業務層面上很少具有可抽象的固定的共同性,我們所迫切需要的一般是對已有程序結構進行動態擴展的一種能力。橫切是AOP的一種特殊的應用,但不是它的全部。相對于繼承(inheritance)等依賴于概念詮釋的結構擴展機制,AOP所代表正是對程序結構空間進行任意操縱的一種能力。AOP可以為基礎結構增加功能,改變原有功能實現,也可以取消原有功能實現,它不需要把所有的擴展邏輯按照樹形結構進行組織,不要求在基礎結構中為擴展編寫特殊的代碼。這種自由的結構擴展能力在Witrix平臺中被發展為“實現業務代碼與平臺基礎架構之間的動態融合”。
在Witrix平臺的實際應用中,AOP的切點匹配能力并不是十分重要。一般情況下我們主要通過整體結構規劃來確保控制點意義明確且相對集中,因此不需要額外通過切點匹配進行業務功能的再組織,不需要再次從雜亂的程序邏輯中重新發現特殊的控制點。例如在Witrix平臺的Jsplet框架中所有后臺事件響應都通過objectName和objectEvent參數觸發,在觸發后臺事件響應函數之前都會調用bizflow文件中的beforeAction段。
在bizflow文件中,aop操作是明確指定到具體函數的,使用模糊匹配在一般情況下只會使問題變得不必要的復雜化。例如擴展actQuery函數
<action id="aop-Query-default">
<source>
通過自定義標簽抽象出多個Action之間的共用代碼
<app:DoWorkA/>
</source>
</action>
在Witrix平臺中結構組裝主要是通過自定義標簽庫和extends算子來實現,它們都依賴于xml格式的規范性。
1. 通過在custom目錄下實現同名的自定義標簽,即可覆蓋Witrix平臺所提供的缺省標簽實現,這里所依賴的并不是復雜的匹配過程,而是自然直觀的映射過程。 http://canonical.javaeye.com/blog/196826
2. 所有的xml配置文件支持extends操作,它允許定制兩個具有業務含義的xml節點之間的結構融合規則。例如<biz-flow extends="docflow">。
實際使用中, AOP技術的一個應用難點在于狀態空間的管理問題。一般interceptor中所能訪問的變量局限為this指針所攜帶的成員變量,以及函數調用時傳入的調用參數。interceptor很難在狀態空間中創建新的變量,也很難讀取在其他地方所產生的狀態變量。例如對于如下擴展 A(arg1); B(arg2); C(arg3); =〉 Ax(arg1); B(arg2); Cx(arg3); 因為原有的調用序列中沒有傳遞額外的參數,因此A和C的擴展函數之間很難實現共享內部變量x。在TPL模板語言中,tpl本身是無狀態的,狀態變量通過外部的$thisContext對象統一管理。通過這種行為與狀態的分離,結合靈活的變量作用域控制機制,可以以比較簡單的方式實現擴展函數之間的信息共享。
說到分解,很多人心中的意象大概只有正交分解。正交分解無疑是最重要的一種分析方法,它也是所謂“分而治之”思想最常見的實現策略。但是正交分解一般潛在的假定是分解后的子部分是大致均衡的,它們是相對具有獨立價值的,可以彼此脫離獨立發展。這是分解后實現系統解耦的重要原因。 http://canonical.javaeye.com/blog/33885 但是物理學中另一種重要的分析學思想是微擾論(Perturbation). 針對一個復雜的物理現象,首先建立一個全局的規范的模型,然后考慮各種微擾條件對原有模型的影響。在小擾動情況下,模型的變化部分往往可以被線性化,被局域化,因而問題得到簡化。微擾分析得到的解依賴于全局模型的解而存在,因而這是一種主從關系的分解方式。但是如果主體模型是我們已經熟知的物理現象,則我們關注的重點可以全部放在擾動解上,認為所有特定的物理規律都體現在擾動解中。如果微擾分析得到的物理元素足夠豐富,則微擾模型本身可以成為獨立的研究對象,在其中我們同樣可以發現某種普適的結構規律。
Witrix平臺中系統化的應用主從分解模式,通過類似AOP的技術實現了業務模型與平臺技術的自然結合。 http://canonical.javaeye.com/blog/126467 最近我們的一個產品的新版本即將在全國范圍內部署,如何有效的控制眾多相近的二次開發版本,同時確保主版本的快速升級,是在架構層面必須解決的問題。 http://canonical.javaeye.com/blog/73265 在Witrix平臺中,各部署版本并不是直接修改主版本源代碼得到,而是將差異化代碼放在單獨的目錄中進行管理,由系統運行平臺負責將差異化定制代碼與主版本代碼進行動態融合,實現部署版本的客戶化。在這一過程中,系統模型本身支持逆元結構至關重要,否則某些多余的元素無法通過差異性描述去除,則將出現局部模型失效的情況。
Witrix平臺定義了特殊的_custom目錄,它的內部目錄結構與defaultroot目錄相同,系統平臺優先使用該目錄下文件所提供的功能實現。同時定義了系統參數global.app_id和global.default_app_id,它們分別用來區分當前程序版本以及程序主版本代碼。例如當global.app_id=beijing,global.default_app_id=main的時候,系統中裝載ui.xml這個標簽庫時經歷如下過程,
1. 裝載平臺內置的標簽庫,文件路徑為 /_tpl/ui.xml.
2. 根據global.default_app_id設置,裝載/_custom/main/_tpl/ui.xml, 其中定義的標簽實現將覆蓋平臺缺省提供的標簽實現。對于那些不需要特殊定制的標簽,繼續使用平臺提供的缺省實現。
3. 根據global.app_id設置,裝載/_custom/beijing/_tpl/ui.xml, 其中定義的標簽實現將覆蓋產品主版本的標簽實現。
基礎平臺中對于代碼動態融合定義了精細的融合策略,將通過編譯技術檢查擴展標簽的接口與缺省實現的接口相兼容,由此確保代碼擴展后不會破壞主版本中的已有調用代碼。
在基礎平臺的實現中,很多實現代碼都是類似
<df:WhenAllowFinishWf>
<df:FinishWfButton />
</df:WhenAllowFinishWf>
這樣的類似廢話的標簽調用。但是通過這些標簽的標記,我們確立了系統的邏輯結構,標定了系統中可以被安全替換的邏輯片斷。
在與一些年歲較大的C程序員接觸的過程中,可以比較明顯的感受到C的思維方式與面向對象思想的不同。C的世界很清澈,先做A, 再做B, 我們所期待發生的計算過程與源代碼的結構是直接一一對照的。這意味著程序將要執行的計算過程在編寫代碼的時刻就已經確定下來。面向對象首先需要確定的是類,對象等中間元素,而并不是最終的計算過程。對象可以之間可以產生很復雜的結構關系,透過這種中間邏輯結構我們來理解最終要發生的計算過程。在事件驅動的應用場景下,面向對象是一種更加有效的描述,
o.someFunc() o.onEventA();
sub1.someFunc(); ==> sub1.onEventA();
sub2.someFunc(); sub2.onEventB();
如果把對象看作是函數+狀態的集合,則對象組裝的關系實際上是函數集合之間的一種組裝關系。當具體的事件發生的時候,將觸發對象上確定的響應函數,此時在各個層面上所實際發生的計算才能被確定下來。
所謂WebMVC即Model2模型是目前Web開發領域的主流模型,Struts/Struts2框架是其典型實現。在概念層面上,這種程序組織模型是怎樣建立起來的?與其他Web開發模型(如面向對象模型)具有怎樣的聯系? 它未來可能的發展方向在哪里? 結合Witrix開發平臺的具體實踐,基于級列設計理論我們可以看到一條概念發展的脈絡。 http://canonical.javaeye.com/blog/33824

1. 外部視角:原始的servlet規范提供了一個簡單的面向IO的程序響應模型。一次前臺訪問由一個特定的servlet負責響應,它從request中讀取輸入流,在全局session中保持臨時狀態,向response中寫入輸出流。在此基礎上,JSP提供的模板概念翻轉了程序和輸出文本之間的相對地位,簡化了文本輸出過程。至此,這種整體的程序模型基本上只是規范化了外部系統訪問Web服務器的響應模型,并沒有對后臺程序的具體實現制定明確的約束條件。因此在最粗野的后臺實現中,讀取參數,業務處理,生成頁面等處理步驟是糾纏在一起的,很難理解,也很難重用。每一個后臺頁面都是一個不可分析的整體。
<%
String paramA = request.getParameter("paramA");
ResultSet rsA = 
%>
result = <%=rsA.getString(0) %>
String paramB = request.getParamter("paramB");
ResultSet rsB = 
<%
rsB.close();
rsA.close();
conn.close();
%>
2. 自發分離:在復雜的程序實踐中,我們會自發的對業務處理代碼和界面代碼進行一定程度的分離。因為我們可以直觀的感受到這兩種代碼的穩定性并不匹配。例如不同業務處理過程產生的結果都可以用一個html表格來展現,而同一個業務處理過程產生的結果頁面可能經常發生變化。一般我們傾向于將業務代碼寫在頁面上方,而界面代碼寫在頁面下方,并使用一些原始的分解機制,例如include指令。這種分離是隨意的,缺乏形式邊界的。例如我們無法表達被包含的頁面需要哪些參數,也難以避免全局變量名沖突。需要注意的是,分層的一般意義在于各個層面可以獨立發展,它的隱含假定是各層面之間的交互是規范化的,只使用確定的數據結構,按照確定的方式進行交互。例如業務層和界面層通過標準的List/Map等數據結構交互,而不是使用具有無限多種樣式的特殊的數據結構。(在弱類型語言環境中,實體對象的結構和Map是等價的).
<%
List header = 
List dataList = 
%>
<%@ include file="/show_table.jsp" %>
3. 規范分離:JSP所提供的useBean和tag機制,即所謂的Model1模型,是對程序結構分離的一種規范化。業務代碼封裝在java類中,一般業務函數與web環境無關,即不使用request和response對象, 允許單元測試。tag機制可以看作是對include指令的增強,是一種代碼重用機制。tld描述明確了調用tag時的約束關系。調用tag時需要就地指定調用參數,而include頁面所依賴的參數可能是在此前任意地方指定的,是與功能實現分離的。此外tag所使用的參數名是局部對象上的屬性名,從而避免了對全局變量的依賴。很遺憾的是,jsp tag所封裝的仍然是原始的IO模型,對程序結構缺乏精細的定義,在概念層面上只是對文本片段的再加工,難以支撐復雜的控件結構。早期jsp tag無法利用jsp模板本身來構造,無法構成一個層層遞進的概念抽象機制,更是讓這種孱弱的重用模型雪上加霜。在其位卻無能謀其政,這直接造成了整個j2ee前臺界面抽象層的概念缺失,以致很多人認為一種前臺模板重用機制是無用的。在Witrix平臺中所定義的tpl模板語言,充分利用了xml的結構特點,結合編譯期變換技術,成為Witrix平臺中進行結構抽象的基本手段。實際上,xml能夠有效表達的語義比一般人所想象的要多得多。
<jsp:useBean id="myBiz" class=" " />
<% List dataList = myBiz.process(paramA) %>
<ui:Table data="<%= dataList %>" />
4. 框架分離:在Model1模型中,頁面中存在著大量的粘結性代碼,它們負責解析前臺參數,進行類型轉換和數據校驗,定位特定的業務處理類,設置返回結果,控制頁面跳轉等。一種自然的想法是定義一個全局的程序框架,它根據集中的配置文件完成所有的粘結性操作。這也就是所謂面向action的WebMVC模型。這一模型實現了服務器端業務層和界面層在實現上的分離,但是對于外部訪問者而言,它所暴露的仍然是原始的自動機模型:整個網站是一個龐大的自動機,每次訪問都觸發一個action,在action中可能更改自動機的狀態(作為全局狀態容器的session對象或者數據庫)。struts作為面向action框架的先驅,它也很自然的成為了先烈。struts中所引入的FormBean, 鏈接管理等概念已經在實踐中被證明是無益的。一些新興的框架開始回歸到通用的Map結構,直接指定跳轉頁面,或者利用CoC(Convention Over Configuration)缺省映射.
public class RegisterAction extends Action {
public ActionForward perform (ActionMapping mapping,
ActionForm form,
HttpServletRequest req,
HttpServletResponse res)
{
RegisterForm rf = (RegisterForm) form;

return mapping.findForward("success");
}
5. 橫向延展:分層之后必然導向各個層面的獨立發展,我們的視野自然也會擴大到單個頁面之外,看到一個層面上更多元素之間的相互作用.在面向對象語言大行其道的今天,繼承(inheritance)無疑是多數人首先想到的程序結構組織手段.后臺action可以很自然的利用java語言自身的繼承機制,配置文件中也可以定義類似的extends或者parent屬性.但是對于前臺頁面一般卻很少有適用的抽象手段,于是便有人致力于前臺頁面的對象語言化:首先將前臺頁面采用某種對象語言表達,然后再利用對象語言內置的結構抽象機制.放棄界面的可描述性,將其轉化為某種活動對象,在我看來是一種錯誤的方向.而JSF(JavaServerFace)規范卻似乎想在這個方向上越走越遠.JSF早期設計中存在的一個嚴重問題是延續了面向對象語言中的狀態與行為綁定的組織方式.這造成每次訪問后臺頁面都要重建整個Component Tree, 無法實現頁面結構的有效緩存.而Witrix平臺中的tpl模板語言編譯出的結構是無狀態的,可以在多個用戶之間重用.
6. 相關聚合:對象化首先意味著相關性的局域化,它并不等價于對象語言化. 當面對一個大的集合的時候,最自然的管理手段便是分組聚合:緊密相關的元素被分配到同一分組,相關性被局域化到組內.例如,針對某個業務對象的增刪改查操作可以看作屬于同一分組. struts中的一個最佳實踐是使用DispatchAction, 它根據一個額外的參數將調用請求映射到Action對象的子函數上.例如/book.do?dispatchMethod=add. 從外部看來,這種訪問方式已經超越了原始的servlet響應模型,看起來頗有一些面向對象的樣子,但也僅僅局限于樣子而已.DispatchAction在struts框架中無疑只是一種權宜之計,它與form, navigation等都是不協調的,而且多個子函數之間并不共享任何狀態變量(即不發生內部的相互作用),并不是真正對象化的組織方式.按照結構主義的觀點,整體大于部分之和.當一組函數聚集在一起的時候,它們所催生的一個概念便是整體的表征:this指針.Witrix平臺中的Jsplet框架是一個面向對象的Web框架,其中同屬于一個對象的多個Action響應函數之間可以共享局部的狀態變量(thisObj),而不僅僅是通過全局的session對象來發生無差別的全局關聯. http://canonical.javaeye.com/blog/33873 需要注意的是,thisObj不僅僅聚集了后臺的業務操作,它同時定義了前后臺之間的一個標準狀態共享機制,實現了前后臺之間的聚合.而前臺的add.jsp, view.jsp等頁面也因此通過thisObj產生了狀態關聯,構成頁面分組.為了更加明確的支持前臺頁面分組的概念,Witrix平臺提供了其他一些輔助關聯手段.例如標準頁面中的按鈕操作都集中在std.js中的stdPage對象上,因此只需要一條語句stdPage.mixin(DocflowOps);即可為docflow定制多個頁面上的眾多相關按鈕操作.此外Witrix平臺中定義了標準的url構建手段,它確保在多個頁面間跳轉的時候,所有以$字符為前綴的參數將被自動攜帶.從概念上說這是一種類似于cookie,但卻更加靈活,更加面向應用的狀態保持機制.
class DaoWebAction extends WebContext{
IEntityDao entityDao;
String metaName;
public Object actQuery(){

thisObj.put("pager",pager);
return success();
}
public Object actExport(){
Pager pager = (Pager)thisObj.get("pager");

return success();
}
}
7. 描述分離:當明確定義了Action所聚集而成的對象結構之后,我們再次回到問題的原點:如何簡化程序基元(對象)的構建?繼承始終是一種可行的手段,但是它要求信息的組織結構是遞進式的,而很多時候我們實際希望的組織方式只是簡單的加和。通過明確定義的meta(元數據),從對象中分離出部分描述信息,在實踐中被證明是一種有效的手段。同樣的后臺事件響應對象(ActionObject),同樣的前臺界面顯示代碼(PageGroup),配合不同的Meta,可以產生完全不同的行為結果, 表達不同的業務需求。 http://canonical.javaeye.com/blog/114066 從概念上說,這可以看作是一種模板化過程或者是一種復雜的策略模式 ProductWebObject = DaoWebObject<ProductMeta>。當然限于技術實現的原因,在一般框架實現中,meta并不是通過泛型技術引入到Web對象中的。目前常見的開發實踐中,經常可以看見類似BaseAction<T>, BaseManager<T>的基類,它們多半僅僅是為了自動實現類型檢查。如果結合Annotation技術,則可以超越類型填充,部分達到Meta組合的效果。使用meta的另外一個副作用在于,meta提供了各個層面之間新的信息傳遞手段,它可以維系多個層面之間的共變(covariant)。例如在使用meta的情況下,后臺代碼調用requestVars(dsMeta.getUpdatableFields())得到提交參數,前臺頁面調用forEach dsMeta.getViewableFields()來生成界面. 則新增一個字段的時候,只需要在meta中修改一處,前后臺即可實現同步更新,自動維持前后臺概念的一致性。有趣的是,前后臺在分離之后它們之間的關聯變得更加豐富。
8. 切面分離: Meta一般用于引入外部的描述信息,很少直接改變對象的行為結構。AOP(Aspect Oriented Programming)概念的出現為程序結構的組織提供了新的技術手段。AOP可以看作是程序結構空間中定位技術和組裝技術的結合,它比繼承機制和模板機制更加靈活,也更加強大。 http://canonical.javaeye.com/blog/34941 Witrix平臺中通過類似AOP的BizFlow技術實現對DaoWebAction和前臺界面的行為擴展,它可以在不擴展DaoWebAction類的情況下,增加/修正/減少web事件響應函數,增加/修正/減少前臺界面展現元素。當前臺發送的$bizId參數不同的時候,應用到WebObject上的行為切片也不同,從而可以自然的支持同一業務對象具有多個不同應用場景的情況(例如審核和擬制)。在BizFlow中定義了明確的實體化過程,前臺提交的集合操作將被分解為針對單個實體的操作。例如前臺提交objectEvent=Remove&id=1&id=2,將會調用兩次<action id="Remove-default">操作。注意到AOP定位技術首先要求的就是良好的坐標定義, 實體化明確定義了實體操作邊界,為實體相關切點的構造奠定了基礎。 http://canonical.javaeye.com/blog/33784
9. 背景消除:在Witrix平臺中, (DaoWebAction + StdPageGroup + Meta + BizFlow)構成完整的程序模型,因此一般情況下并不需要繼承DaoWebAction類,也不需要增加新的前臺頁面文件,而只需要在BizFlow文件中對修正部分進行描述即可。在某種程度上DaoWebAction+StdPageGroup所提供的CRUD(CreateReadUpdateDelete)模型成為了默認的背景知識。如果背景信息極少泄漏,則我們可以在較高抽象層次上進行工作,而不再理會原始的構造機制。例如在深度集成hibernate的情況下,很少會有必須使用SQL語句的需求。BizFlow是對實體相關的所有業務操作和所有頁面展現的集中描述,在考慮到背景知識的情況下,它定義了一個完整的自給自足的程序模型。當我們的建模視角轉移到BizFlow模型上時,可以發展出新的程序構造手段。例如BizFlow之間可以定義類似繼承機制的extends算子,可以定義實體狀態驅動的有限自動機,可以定義不同實體之間的鉤稽關系(實體A發生變化的時候自動更新實體B上的相關屬性),也可以定義對Workflow的自然嵌入機制。從表面上看,BizFlow似乎回歸到了前后臺大雜燴的最初場景(甚至更加嚴重,它同時描述了多個相關頁面和多個相關操作),但是在分分合合的模型建立過程中,大量信息被分解到背景模型中,同時發展了各種高級結構抽象機制, 確保了我們注意力的關注點始終是有限的變化部分。而緊致的描述提高了信息密度,簡化了程序構造過程。 http://canonical.javaeye.com/blog/126467
<bizflow extends="docflow"> <!-- 引入docflow模型,包括一系列界面修正和后臺操作 -->
<biz id="my">
<tpls>
<tpl id="initTpl">
<script src="my_ops.js" ></script>
<script>
stdPage.mixin(MyOps); // 引入多個頁面上相關按鈕對應的操作
</script>
</tpl>
</tpls>
</biz>
</bizflow>
|