作者:像風一樣
來源:https://www.cnblogs.com/yueshutong/p/10418226.html

HTTL (Hyper-Text Template Language) 是一個高性能的開源JAVA模板引擎, 適用於動態HTML頁面輸出, 可替代JSP頁面, 指令和Velocity相似。作者是阿里巴巴工程師樑飛,本文是在拜讀了HTTL的設計原則之後提煉出的部分通用設計原則。

模型劃分原則

按實體域,服務域,會話域劃分。

不管你做一個什麼產品,都一定有一個被操作的主體,比如:服務框架管理的Service,任務框架管理的Task,Spring管理的Bean等,這就是實體域。

即然有被操作者,就一定有操作者,它管理被操作者的生命週期,發起動作,比如:服務框架的ServiceInvoker,,任務框架的TaskScheduler,Spring的BeanFactory等,這就是服務域。

服務域發起動作,在執行過程中,會有一些臨時狀態需要存儲交換,比如:Invacation,Execution,Request等,這就是會話域。

從HTTL模板引擎看軟件設計原則

相應的,在HTTL中:

從HTTL模板引擎看軟件設計原則

  • Engine 爲服務域
  • 它是API的入口,並負責實體域Template的生命週期管理,它是Singleton單一實例的,加載後不可變,所以是線程安全的,它的初始化過程較重,請複用單例。
  • Template 爲實體域
  • 代表着被操作者,它是Prototype原型實例的,即每個模板產生一個實例,加載後不可變,同樣也是線程安全的,模板變化後,將產生不同的實例,而不改變原實例。
  • Context 爲會話域
  • 持有操作過程中的所有可變狀態,它是ThreadLocal線程內實例的,即不和其它線程競爭使用,所以也是線程安全的,請不要跨線程傳遞,它的初始化過程很輕量,每次模板執行前都新建實例,執行完即銷燬。

這樣劃分的好處是,職責清晰,可變狀態集中,每個域都是無鎖線程安全的,保證在大併發下,不會降低系統的活性。

這些核心領域模型也就是HTTL的API(Application Programming Interface),它是HTTL暴露給用戶的最少概念,也就是上面類圖中的第一列。

擴展點組裝原則

按“微核+插件”體系組裝。

但凡有生命力的產品,都是在擴展性方面設計的比較好的,因爲沒有哪個產品可以覆蓋所有需求,對於開源軟件尤其如此。

所以,產品只有具有良好的擴展性,允許用戶或第三方參與進來,進行二次開發,才能保持生命力。

怎麼樣的擴展性纔是最好的?通常來講,就是沒有任何功能是硬編碼的,所有的功能都可被用戶替換。

那要如何才能做到這樣?一個重要的原則就是:平等對待第三方。

也就是凡是原作者能實現的功能,第三方也要能夠在不改變源代碼的前提下實現。

換言之,原作者應把自己也當作擴展者,自己添加功能時,也要用第三方擴展者同樣的方式進行,而不要有特權。

要做到這一點,就需要一個良好的框架支撐,“微核+插件”是一個不錯的選擇,Eclipse, Maven等知名軟件都採用該體系。

從HTTL模板引擎看軟件設計原則

什麼是“微核+插件”?微核,即最小化核心,內核只負責插件的組裝,不帶任何功能邏輯,所有功能都由可替換的插件實現,

並且,組裝過程應基於統一的規則,比如基於setter注入,而不能對不同插件硬編碼組裝,這樣可以確保沒有任何功能在內核中硬編碼。

比如:Spring, OSGI, JMX, ServiceLoader等都是常見的微核容器,它們負責基於統一規則的組裝,但不帶功能邏輯。

當然,如果你不想帶這麼重的框架,也可以自行實現,HTTL就採用自行實現的httl.util.BeanFactory作爲組裝微核。

在Engine.getEngine()中調用了BeanFatory.createBean(Engine.class, properties),

其中,properties即爲httl.properties配置,BeanFatory基於setter方法,遞歸注入所有對象的屬性。

比如:httl.properties中配置了parser=httl.spi.parsers.CommentParser,

而DefaultEngine中有setParser(Parser parser)方法,就會被注入,並且Parser本身的屬性也會遞歸注入。

如果你需要擴展或替換HTTL的實現,請參見:擴展集成

既然非功能性的插件組裝過程,可以由微核框架來完成,那功能性的組裝怎麼辦呢?

我們應該把功能性的組裝過程也封裝成插件,即讓大插件組裝小插件,形成級聯組裝關係。

從HTTL模板引擎看軟件設計原則

比如,HTTL的入口類Engine的實例也是一個插件,它負責模板的緩存,加載,解析的總調度,即你可以替換DefaultEngine實現。

只需在httl.properties中配置:engine=com.your.YourEngine,可以將現有Parser等SPI注入你的Engine。

這些插件的接口,也就是HTTL的SPI(Service Provider Interface),它是HTTL暴露給擴展者的最小粒度的替換單元,也就是上面類圖中的第二列。

整體分包原則

按複用度,抽象度,穩定度分包。

  • 複用度:
  • 每種用戶所需用到的類,就是同一複用粒度的,比如:使用者和擴展者,這樣可以減少代碼幹擾,以及最大化複用。
  • 穩定度:
  • 被依賴包和依賴包的佔比,如果一個包依賴很多包,那別的包變化都會引起它跟隨變化,所以它就不穩定,反之即穩定, 保持被依賴者總是比依賴者的穩定度高,形成金子塔關係,這樣可以防止不穩定性傳染,比如a包只依賴3個包,而b包依賴10個包,那就不要讓a包去依賴b包。
  • 抽象度:
  • 包中抽象類個數佔比,比如包中有10個類,其中3個爲抽象類(包括接口),則抽象度爲3/10, 保持包的穩定度和抽象度成正比,即把抽象類(包括接口)放到穩定的包中,把具體實現類放到不穩定的包中,這樣可以保持每層都有足夠的擴展性。

穩定度與抽象度關係如下圖:

從HTTL模板引擎看軟件設計原則

也就是分包應該如下:

從HTTL模板引擎看軟件設計原則

其中上面那個包不依賴其它包。所以它很穩定,應儘量把抽象類或接口放在這一層,

而下面那個包依賴了三個包,三個包變化都會引起它跟隨變化,所以它是不穩定的,應儘量把具體實現類放在這一層。

因穩定度與抽象度成正比,所以不穩定度與抽象度成反比,用反比方便畫圖,計算方式如下:

  • (1) I = Ce / (Ca + Ce)
  • I: Instability (不穩定度)
  • Ca: Afferent Coupling (傳入依賴,也就是被其它包依賴的個數)
  • Ce: Efferent Coupling (輸出依賴,也就是依賴其它包的個數)
  • (2) A = Na / Nc
  • A: Abstractness (抽象度)
  • Na: Number of abstract classes (抽象類的個數)
  • Nc: Number of classes (類的個數,包括抽象類)
  • (3) D = abs(1 - I - A) * sin(45)
  • D: Distance (偏差)
  • I: Instability (不穩定度)
  • A: Abstractness (抽象度)

應該保持偏差越小越好,即下圖所示交點都落在綠色反比線左右:

從HTTL模板引擎看軟件設計原則

基於上面的原則,HTTL的包結構整體上劃分爲三層:(對應上面類圖中的三列)

  • API (Application Programming Interface)
  • 模板引擎的使用者依賴的接口類,也是核心領域模型所在,保持最少概念,並隱藏實現細節,其中Engine類相當於微內核,只管理非功能性的擴展點的加載,不硬編碼模板加載解析渲染的任何部分。
  • SPI (Service Provider Interface)
  • 模板引擎的擴展者依賴的接口類,它依賴於API的領域模型,它是模板引擎功能正交分解的抽象層,以保證用戶可以最小粒度替換需要改寫的地方,方便二次開發。
  • BUILT-IN (Built-in Implementation)
  • 內置擴展實現,它是SPI標準實現,也是可被用戶替換的類,它包含引擎所有做的事,包括擴展點之間的組裝過程(可替換DefaultEngine),以確保沒有功能換不掉,即平等對待擴展者。

採用子包依賴父包風格,所以將API放在根目錄,SPI接口獨立子包,各種實現放在SPI的下一級子包中。

  • 使用者API導入:import httl.*;
  • 擴展者SPI導入:import httl.spi.*;

下圖是HTTL所有包的不穩定度與抽象度的比值距陣:(下圖爲JDepend繪製)

從HTTL模板引擎看軟件設計原則

HTTL所有核心包都是靠近反比線的,即上圖中用綠色標識的點,表示分包是合理的。

注:圖中黑色的點爲util相關包,它們不抽象,卻被很多包依賴,只是內部複用代碼,不影響整體設計,用戶請不要依賴HTTL的util類。

相關文章