前車之覆,后車之鑒
——開源項目經驗談
(本文發表于《程序員》2005年第2期)
隨著開源文化的日益普及,“參與開源”似乎也變成了一種時尚。一時間,似乎大家都樂于把自己的代碼拿出來分享了。就在新年前夕,我的一位老朋友、一位向來對開源嗤之以鼻的
J2EE
架構師竟然也發布了一個開源的
J2EE
應用框架(姑且稱之為“
X
框架”),不得不令我驚嘆開源文化的影響力之強大。
可惜開源并非免費的午餐,把源碼公開就意味著要承受眾目睽睽的審視。僅僅幾天之后,國內幾位資深的 J2EE 架構師就得出一個結論:細看之下, X 框架不管從哪個角度都只能算一個失敗的開源項目。究竟是什么原因讓一個良好的愿望最終只能得到一個失敗的結果?本文便以 X 框架為例,點評初涉開源的項目領導者常犯的一些錯誤,指出投身開源應當遵循的一些原則,為后來的開源愛好者掃清些許障礙。
成熟度
打開 X 框架在 SourceForge 的項目站點,我們立刻可以看到:在“ Development Status ”一欄赫然寫著“ 5 – Production/Stable ”。也就是說,作者認為 X 框架已經成熟穩定,可以交付用戶使用。那么,現在對其進行評估便不應該有為時過早之嫌。可是, X 框架真的已經做好準備了嗎?
打開從 SourceForge 下載的 X 框架的源碼包,筆者不禁大吃一驚:壓縮包里真的 只有源碼 ——編譯、運行整個項目所需的庫文件全都不在其中。從作者自己的論壇得知,該項目需要依賴 JBoss 、 JDOM 、 Castor 、 Hibernate 等諸多開源項目,筆者只好自己動手下載了這些項目,好一番折騰總算是在 Eclipse 中成功編譯了整個項目。
不需要對開源文化有多么深刻的了解,只要曾經用過一些主流的開源產品,你就應該知道:一個開源軟件至少應該同時提供源碼發布包和二進制發布包,源碼包中至少應該有所有必需的依賴庫文件(或者把依賴庫單獨打包發布)、完整的單元測試用例(對于 Java 項目通常是 Junit 測試套件)、以及執行編譯構建的腳本(對于 Java 項目通常是 Ant 腳本或者 Maven 腳本),但這些內容在 X 框架的發布包中全都不見蹤影。用戶如果想要使用這個框架,就必須像筆者一樣手工下載所有的依賴庫,然后手工完成編譯和構建,而且構建完成之后也無從知曉其中是否有錯誤存在(因為沒有單元測試)。這樣的發布形式,算得上是“ Production/Stable ”嗎?
開源必讀:便捷構建
開源軟件應該提供最便捷的構建方式,讓用戶可以只輸入一條命令就完成整個項目的編譯、構建和測試,并得到可運行的二進制程序。對于 Java 項目,這通常意味著提供完整的 JUnit 測試套件和 Ant 腳本。你的潛在用戶可能會在一天之內試用所有類似的開源軟件,如果一個軟件需要他用半天時間才能完成構建、而且還無從驗證正確性、無從著手編寫他自己的測試用例,這個軟件很可能在第一時間被扔到墻角。 |
從 SourceForge 的項目頁面可以看到, X 框架的授權協議是 Apache License V2.0 ( APL )。然而在它的發布包中,筆者沒有看到任何形式的正式授權協議文本。眾所周知, SourceForge 的項目描述是可以隨時修改的( X 框架本身的授權協議就曾經是 GPL ),如果發布包中沒有一份正式的授權協議文本,一旦作者修改了 SourceForge 的項目描述,用戶又該到哪里去尋找證據支持自己的合法使用呢?
在 X 框架的源碼中,大部分源文件在開始處加上了 APL 的授權聲明,但有一部分源碼很是令人擔心。例如 UtilCache 這個類,開始處沒有任何授權聲明,而 JavaDoc 中則這樣聲明作者信息:
@author ???? <a href="mailto:jonesde@ ofbiz.org ">David E. Jones</a>
也就是說,這個類的源碼來自另一個開源項目
Ofbiz
。值得一提的是,
Ofbiz
一直是“商業開源”的倡導者,它的授權協議相當嚴格。凡是使用
Ofbiz
源碼,必須將它的授權協議一并全文復制。像
X
框架這樣復制
Ofbiz
源碼、卻刪掉了授權協議的行為,實際上已經構成了對
Ofbiz
的侵權。
另外,作者打包用的壓縮格式是 RAR ,而這個壓縮格式對于商業用戶是收費的。對于一個希望在商業項目中應用的框架項目來說,選擇這樣一個壓縮格式實在算不得明智。而且筆者在源碼包中還看到了好幾個 .jbx 文件,這是 JBuilder 的項目描述文件。把這些 JBuilder 專用的文件放在源碼包中,又怎能讓那些買不起或是不想買 JBuilder 的用戶放心呢?更何況,出于朋友的關心,筆者還不得不擔心 X 框架的作者是否會收到 Borland 公司的律師信呢。
開源必讀:授權先行
在啟動一個開源項目時,第一件大事就是要確定自己的授權協議,并在最醒目的地方用最正式的方式向所有人聲明——當然,在此之前你必須首先了解各種開源授權協議。譬如說,
GPL
(
Linux
采用的授權協議)要求在軟件之上的擴展和衍生也必須繼承
GPL
,因此這種協議對軟件的商業化應用很不友好;相反,
APL
則允許用戶將軟件的擴展產物私有化,便于商業應用,卻不利于開發者社群的發展。作為一個開源項目的領導者,對于各種授權協議的利弊是不可不知的。
除了源碼本身的授權協議之外,軟件需要使用的類庫、 IDE 、解壓工具等等都需要考慮授權問題。開源絕對不僅僅意味著“免費使用”,開源社群的人們有著更加強烈的版權意識和法律意識。如果你的開源軟件會給用戶帶來潛在的法律麻煩,它離著被拋棄的命運也就不遠了。 |
可以看到,不管從法律的角度還是從發布形式的角度, X 框架都遠夠不上“ Production/Stable ”的水準——說實在的,以它的成熟度,頂多只能算是一個尚未計劃周全的開源項目。雖然作者在自己的網站上大肆宣傳,但作為一個潛在的用戶,我不得不冷靜地說:即便 X 框架的技術真的能夠吸引我,但它遠未成熟的項目形態決定了它根本無法在任何有實際意義的項目中運用。要讓商業用戶對它產生興趣,作者需要做的工作還很多。
我剛才說“即便 X 框架的技術真的能夠吸引我”,這算得上是一個合理的假設嗎?下面,就讓我們進入這個被作者寄予厚望的框架內部,看看它的技術水平吧。
整體架構
在 X 框架的宣傳頁面上,我們看到了這樣的宣傳詞:
X
框架解決了以往
J2EE
開發存在的諸多問題:
EJB
難用、
J2EE
層次復雜、
DTO
太亂、
Struts
繞人、緩存難做性能低等。
X
框架是
Aop/Ico
[
注:應為“
IoC
”,此處疑似筆誤
]
的實現,優異的緩存性能是其優點。
下面是 X 框架的整體架構圖:
可以看到,在作者推薦的架構中, EJB 被作為業務邏輯實現的場所,而 POJO 被用于實現 Fa?ade 。這是一個好的技術架構嗎?筆者曾在一篇 Blog 中這樣評價它 [1] :
讓我們先回想一下,使用
EJB
的理由是什么?常見的答案有:可分布的業務對象;聲明性的基礎設施服務(例如事務管理)。那么,如果在
EJB
的上面再加上一
層
POJO
的
Fa?ade
,顯然你不能再使用
EJB
的基礎設施了,因為完整的業務操作(也就是事務邊界)將位于
POJO Fa?ade
的方法這里,所以你必須重新
——
以聲明性的方式
——
實現事務管理、安全性管理、
remoting
、緩存等基礎設施服務。換句話說,你失去了
session bean
的一半好處。另一方面,“可分布的業務對象”也不復存在,因為
POJO
本身是不能
——
像
EJB
那樣
——
分布的,這樣你又失去了
session bean
的另一半好處。
繼續回想,使用基于
POJO
的輕量級架構的理由是什么?常見的答案有:易于測試;便于移植;“開發
-
發布”周期短。而如果僅僅把
POJO
作為一層
Fa?ade
,把業務邏輯放在下面的
EJB
,那么你仍然無法輕易地測試業務邏輯,移植自然也無從談起了,并且每次修改
EJB
之后必須忍受漫長的發布周期。
即便是僅僅把
EJB
作為
O/R mapping
,而不是業務邏輯的居所,你最多只能通過
DAO
封裝獲得比較好的業務可測性,但“修改
-
發布”的周期仍然很長,因為仍然有
entity bean
存在。也就是說,即使是往最好的方面來說,這個架構至少損失了輕量級架構的一半優點。
作為一個總結,
X
框架即便是在使用得最恰當的情況下,它仍然不具備輕量級架構的全部優點,至少會對小步前進的敏捷開發造成損害(因為
EJB
的存在),并且沒有
Spring
框架已經實現的基礎設施(例如事務管理、
remoting
等),必須重新發明這些輪子;另一方面,它也不具備
EJB
的任何優點,
EJB
的聲明性基礎設施、可分布業務對象等能力它全都不能利用。因此,可以簡單地總結說,
X
框架是一個這樣的架構:
它結合了
EJB
和輕量級架構兩者各自的短處,卻拋棄了兩者各自的長處
。
在不得不使用 EJB 的時候,一種常見的架構模式是:用 session bean 作為 Fa?ade ,用 POJO 實現可移植、可測試的業務邏輯。這種模式可以結合 EJB 和 POJO 兩者的長處。而 X 框架推薦的架構模式,雖然乍看起來也是依葫蘆畫瓢,效果卻恰恰相反,正可謂是“取其糟粕、去其精華”。
開源必讀:架構必須正確
在開源軟件的初始階段,功能可以不完善,代碼可以不漂亮,但架構思路必須是正確的。即使你沒有完美的實現,參與開源的其他人可以幫助你;但如果架構思路有嚴重失誤,誰都幫不了你。從近兩年容器項目的更迭就可以看出端倪:
PicoContainer
本身只有
20
個類、數百行代碼,但它有清晰而優雅的架構,因此有很多人為它貢獻外圍的功能;
Avalon
容器盡管提供了完備的功能,但架構的落伍迫使
Apache
基金會只能將其全盤廢棄。
所以如果你有志于啟動一個開源項目(尤其是框架性的項目),務必先把架構思路拿出來給整個社群討論。只要大家都認可你的架構,你就有機會得到很多的幫助;反之,恐怕你就只能得到無盡的嘲諷了。 |
技術細節
既然整體架構已經無甚可取之處,那么
X
框架的實現是否又像它所宣稱的那樣,能夠解決諸多問題呢?既然
X
框架號稱是“
AOP/IoC
的實現”,我們就選中這兩項技術,看看它們在
X
框架中的實現和應用情況。
IoC
X 框架宣稱自己是一個“基于 IoC 的應用框架”。按照定義,框架本身就具有“業務代碼不調用框架,框架調用業務代碼”的特性,因此從廣義上來說,所有的框架必然是基于 IoC 模式的。所以,在框架這里,“基于 IoC ”通常是特指“對象依賴關系的管理和組裝基于 IoC ”,也就是 Martin Fowler 所說的 Dependency Injection 模式 [2] :由容器統一管理組件的創建和組裝,組件本身不包含依賴查找的邏輯。那么, X 框架實現 IoC 的情況又如何呢?
我們很快找到了 ContainerWrapper 這個接口,其中指定了一個 POJO 容器核心應該具備的主要功能:
public interface ContainerWrapper {
? public void registerChild(String name);
? public void register(String name, Class className);
? public void register(String name, Class className, Parameter[] parameters);
? public void register(String name, Object instance);
? public void start();
? public void stop();
? public Collection getAllInstances();
? public Object lookup(String name);
}
在這個接口的默認實現 DefaultContainerWrapper 中,這些功能被轉發給 PicoContainer 的對應方法。也就是說, X 框架本身并沒有實現組件容器的功能,這部分功能將被轉發給其他的 IoC 組件容器(例如 PicoContainer 、 Spring 或 HiveMind 等)來實現。在 ContainerWrapper 接口的注釋中,我們看到了一句頗可玩味的話:
/**
? * 封裝了 Container ,解耦具體應用系統和 PicoContainer 關系。
了解 IoC 容器的讀者應該知道,在使用 PicoContainer 或 Spring 等容器時,絕大多數 POJO 組件并不需要對容器有任何依賴:它們只需要是最普通的 JavaBean ,只需要實現自己的業務接口。既然對容器沒有依賴,自然也不需要“解耦”。至于極少數需要獲得生命周期回調、因此不得不依賴容器的組件,讓它們依賴 PicoContainer 和依賴 X 框架難道有什么區別嗎?更何況, PicoContainer 是一個比 X 框架更成熟、更流行的框架,為什么用戶應該選擇 X 框架這么一個不那么成熟、不那么流行的框架夾在中間來“解耦”呢?
不管怎么說,至少我們可以看到: X 框架提供了組件容器的核心功能。那么, IoC (或者說, Dependency Injection )在 X 框架中的應用又怎么樣呢?眾所周知,引入 IoC 容器的目標就是要消除應用程序中泛濫的工廠(包括 Service Locator ),由容器統一管理組件的創建和組裝。遺憾的是,不論在框架內部還是在示例應用中,我們仍然看到了大量的工廠和 Service Locator 。例如作者引以為傲的緩存部分,具體的緩存策略(即 Cache 接口的實現對象)就是由 CacheFactory 負責創建的,并且使用的實現類還是硬編碼在工廠內部:
? public ? CacheFactory() {
cache = new LRUCache();
也就是說,如果用戶需要改變緩存策略,就必須修改 CacheFactory 的源代碼——請注意,這是一個 X 框架內部的類,用戶不應該、也沒有能力去修改它。換句話說,用戶實際上根本無法改變緩存策略。既然如此,那這個 CacheFactory 又有什么用呢?
開源必讀:開放
-封閉原則
開源軟件應該遵守開放
-
封閉原則(
Open-Close Principle
,
OCP
):對
擴展
開放,對
修改
封閉。如果你希望為用戶提供任何靈活性,必須讓用戶以擴展(例如派生子類或配置文件)的方式使用,不能要求(甚至不能允許)用戶修改源代碼。如果一項靈活性必須通過修改源碼才能獲得,那么它對于用戶就毫無意義。
|
在示例應用中,我們同樣沒有看到 IoC 的身影。例如 JdbcDAO 需要使用數據源(即 DataSource 對象),它就在構造子中通過 Service Locator 主動獲取這個對象:
? public JdbcDAO() {
????? ServiceLocator sl = new ServiceLocator();
?????
dataSource = (DataSource) sl.getDataSource(JNDINames.DATASOURCE);
同樣的情況也出現在 JdbcDAO 的使用者那里。也就是說,雖然 X 框架提供了組件容器的功能,卻沒有(至少是目前沒有)利用它的依賴注入能力,僅僅把它作為一個“大工廠”來使用。這是對 IoC 容器的一種典型的誤用:用這種方式使用容器,不僅沒有獲得“自動管理依賴關系”的能力,而且也失去了普通 Service Locator “強類型檢查”的優點,又是一個“取其糟粕、去其精華”的設計。
開源必讀:了解你自己
當你決定要在開源軟件中使用某項技術時,請確定你了解它的利弊和用法。如果僅僅為了給自己的軟件貼上“基于 xx 技術”的標簽而使用一種自己不熟悉的技術,往往只會給你的項目帶來負面的影響。 |
AOP
在 X 框架的源碼包中,我們找到了符合 AOP-Alliance API 的一些攔截器,例如用于實現緩存的 CacheInterceptor 。盡管——毫不意外地——沒有找到如何將這些攔截器織入( weave in )的邏輯或配置文件,但我們畢竟可以相信:這里的確有 AOP 的身影。可是,甫一深入這個“基于 AOP 的緩存機制”內部,筆者卻又發現了更多的問題。
單從 CacheInterceptor 的實現來看,這是一個最簡單、也最常見的緩存攔截器。它攔截所有業務方法的調用,并針對每次方法調用執行下列邏輯:
??? IF 需要緩存
?????? key = ( 根據方法簽名生成 key);
?????? IF (cache.get(key) == null)
??? ?????? value = ( 實際調用被攔截方法 );
??? ??? ??? cache.put(key, value);
?????? RETURN (cache.get(key));
??? ELSE
?????? RETURN ( 實際調用被攔截方法 );
看上去很好,基于 AOP 的緩存實現就應該這么做……可是,清除緩存的邏輯在哪里?如果我們把業務方法分為“讀方法”和“寫方法”兩種,那么這個攔截器實際上只照顧了“讀方法”的情況。而“寫方法”被調用時會改變業務對象的狀態,因此必須將其操作的業務對象從緩存中清除出去,但這部分邏輯在 CacheInterceptor 中壓根不見蹤影。如果緩存內容不能及時清理的話,用戶從緩存中取出的信息豈不是完全錯誤的嗎?
被驚出一身冷汗之后,筆者好歹還是從幾個
Struts action
(也就是調用
POJO Fa?ade
的
client
代碼)中找到了清除緩存的邏輯。原來
X
框架所謂“基于
AOP
的緩存機制”只實現了一條腿:“把數據放入緩存”和“從緩存中取數據”的邏輯確實用攔截器實現了,但“如何清除失效數據”的邏輯還得散布在所有的客戶代碼中。
AOP
原本就是為了把緩存這類橫切性(
crosscutting
)的基礎設施邏輯集中到一個模塊管理,像
X
框架的這個緩存實現,不僅橫切性的代碼仍然四下散布,連緩存邏輯的相關性和概念完整性都被打破了,豈不是弄巧成拙么?
開源必讀:言而有信
如果你在宣傳詞中承諾了一項特性,請務必在你的軟件中完整地實現它。不要僅僅提供一個半吊子的實現,更不要讓你的任何承諾放空。如果你沒有把握做好一件事,就不要承諾它。不僅對于開源軟件,對于任何軟件開發,這都是應該記住的原則。 |
更有趣的是, X 框架的作者要求領域模型對象繼承 Model 基類,并聲稱這是為了緩存的需要——事實也的確如此: CacheInterceptor 只能處理 Model 的子對象。但只要對緩存部分的實現稍加分析就會發現,這一要求完全是作者憑空加上的:用于緩存對象的 Cache 接口允許放入任何 Object ;而 Model 盡管提供了 setModified() 、 setCacheable() 等用于管理緩存邏輯的方法,卻沒有任何代碼調用它們。換句話說,即便我們修改 CacheInterceptor ,使其可以緩存任何 Object ,對 X 框架目前的功能也不會有任何影響。既然如此,又為什么要給用戶憑空加上這一層限制呢?
退一萬步說,即使我們認為 X 框架今后會用 Model 的方法來管理緩存邏輯,這個限制仍然是理由不足的。畢竟,目前 X 框架還僅僅提供了緩存這一項基礎設施( infrastructure )而已。如果所有基礎設施都用“繼承一個基類”的套路來實現,當它真正提供企業級應用所需的所有基礎設施時, Model 類豈不是要變得碩大無朋?用戶的領域對象豈不是再也無法移植到這個框架之外?況且,“由領域對象判斷自己是否需要緩存”的思路本身也是錯誤的:如果不僅要緩存領域對象,還要緩存 String 、 Integer 等簡單對象,該怎么辦?如果同一個領域對象在不同的方法中需要不同的緩存策略,又該怎么辦? X 框架的設計讓領域對象背負了太多的責任,而這些責任原本應該是通過 AOP 轉移到 aspect 中的。在 X 框架這里, AOP 根本沒有發揮它應有的效用。
開源必讀:避免綁定
開源軟件(尤其是框架類軟件)應該盡量避免對你的用戶造成綁定。能夠在 POJO 上實現的功能,就不要強迫用戶實現你的接口;能夠通過接口實現的功能,就不要強迫用戶繼承你的基類。尤其是 Java 語言只允許單根繼承,一旦要求用戶的類繼承框架基類,那么前者就無法再繼承其他任何基類,這是一種非常嚴重的綁定,不論用戶和框架設計者都應當極力避免。 |
寫在最后
看完這篇多少有些尖刻的批評,恐怕讀者難免要怪責我“不厚道”——畢竟,糟糕的開源軟件堪比恒河沙數,為什么偏要選中 X 框架大加撻伐呢?在此,我要給各位讀者、各位有志于開源的程序員一個最后、卻是最重要的建議:
開源必讀:切忌好大喜功
開源是一件長期而艱巨的工作,對于只能用業余時間參與的我們更是如此。做開源務必腳踏實地,做出產品首先在小圈子里內部討論,然后逐漸擴大宣傳的圈子。切勿吹大牛、放衛星,把“未來的愿景”當作“今天的承諾”來說——因為一旦工作忙起來,誰都不敢保證這個愿景到哪天才能實現。
國人還有個愛好:凡事喜歡趕個年節“獻禮”,或是給自己綁上個“民族軟件”的旗號,這更是開源的大忌。凡是做過政府項目的程序員,想必都對“國慶獻禮”、“新年獻禮”之類事情煩不勝煩,輪到自己做開源項目時,又何苦把自己套進這個怪圈里呢?當然,如果你的開源項目原本就是做給某些官老爺看的,那又另當別論。
|
所以,我的這位朋友怕也不能怪我刻?。阂皇撬o趕著拿出個遠未完善的版本“新年獻禮”,要不是他提前放出“ AOP/IoC ”的衛星,要不是他妄稱這個框架“代表民族軟件水平”,或許我還會夸他的代碼頗有可看之處呢。有一句大家都熟悉的老話,筆者私以為所有投身開源者頗可借鑒,在此與諸位共勉:
長得丑不是你的錯……
[1]
這篇
Blog
的原文請看:
http://gigix.blogdriver.com/gigix/474041.html
。
[2] 關于 IoC 模式和 Dependency Injection 模式,詳見 Martin Fowler 的《Dependency Injection與模式IoC容器》 一文。(中譯本發表于《程序員》 2004 年第3 期。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=276486
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
