建造和管理一座城市,你能自己掌管一切細節(jié)不脯?
恐怕不行府怯。每個城市都有一組組人管理不同的部分,供水系統(tǒng)防楷、供電系統(tǒng)牺丙、交通、執(zhí)法域帐、立法赘被,諸如此類。需要有人負責(zé)全局肖揣,有人負責(zé)細節(jié)民假。
城市能運轉(zhuǎn),還因為它演化出恰當(dāng)?shù)某橄蟮燃壓湍K龙优,好讓個人和他們所管理的“組件”即便在不了解全局時也能有效運轉(zhuǎn)羊异。
盡管軟件團隊往往也是這樣組織起來,但他們所致力的工作卻常常沒有同樣的關(guān)注面切分及抽象層級彤断。本文將討論如何在較高的抽象層級——系統(tǒng)層級——上保持整潔野舶。
1. 將系統(tǒng)的構(gòu)造和使用分開
構(gòu)造和使用是非常不一樣的過程。
軟件系統(tǒng)應(yīng)該將啟始過程和啟始過程之后的運行時邏輯分離開宰衙,在啟始過程中構(gòu)建應(yīng)用對象平道,也會存在互相糾纏的依賴關(guān)系。
不幸的是供炼,多數(shù)應(yīng)用程序都沒有做分離處理一屋。啟始過程代碼很特殊窘疮,被混雜到運行時邏輯。下列就是典型的情形:
public Service getService() {
if (service == null)
service = new MyServiceImpl(...); // Good enough for most cases?
return service;
}
這就是所謂的延遲初始化冀墨,也有一些好處闸衫。在真正用到對象之前,無需操心這種架空構(gòu)造诽嘉,啟始時間也會更短蔚出,而且還能保證永遠不會返回null值。
然而虫腋,我們也得到了MyServiceImpl及其構(gòu)造器所需的一切的硬編碼依賴骄酗。不分解這些依賴關(guān)系就無法編譯,即便在運行時永不使用這種類型的對象岔乔。
如果MyServiceImpl是個重型對象酥筝,則測試也是個問題。我們必須確保在單元測試調(diào)用該方法之前雏门,就給servie指派恰當(dāng)?shù)臏y試替身嘿歌。由于構(gòu)造邏輯與運行過程想混雜,我們必須測所有的執(zhí)行路徑(例如茁影,null值測試及其代碼塊)宙帝。有了這些權(quán)責(zé),說明方法做了不止一件事募闲,輕微違反了單一職責(zé)原則步脓。
最糟糕的大概是我們不知道MyServiceImpl在所有情形中是否都是正確的對象。為什么該方法所屬類必須知道全局情形浩螺?我們是否真能知道在這里要用到正確的對象靴患?
我們應(yīng)該將對象構(gòu)造的啟始和設(shè)置過程從正常的運行時邏輯中分離出來。
1.1 分解main
將構(gòu)造與使用分開的方法之一是將全部構(gòu)造過程變遷到main或被稱之為main的模塊中要出,設(shè)計系統(tǒng)的其余部分時鸳君,假設(shè)所有對象都已正確構(gòu)造和設(shè)置。
控制流程很容易理解患蹂,main模塊創(chuàng)建系統(tǒng)所需的對象或颊,再傳遞給應(yīng)用程序,應(yīng)用程序只管使用传于。
1.2 工廠
應(yīng)用程序使用抽象工廠模式控制如何創(chuàng)建對象
1.3 依賴注入
有一種強大的機制可以實現(xiàn)分離構(gòu)造與使用囱挑,那就是依賴注入(Dependency Injection,DI)沼溜,控制反轉(zhuǎn)(Inversion of Control, IOC)在依賴管理中的一種應(yīng)用手段平挑。控制反轉(zhuǎn)將第二職責(zé)從對象中拿出來系草,轉(zhuǎn)移到另一個專注于此的對象中弹惦,從而遵循了單一職責(zé)原則否淤。
2. 擴容
“一開始就做對系統(tǒng)”純屬神話。我們應(yīng)該只去實現(xiàn)今天的用戶故事棠隐,然后重構(gòu),明天再擴展系統(tǒng)檐嚣、實現(xiàn)新的用戶故事助泽。這就是迭代和增量敏捷的精髓所在。測試驅(qū)動開發(fā)嚎京、重構(gòu)以及它們打造出來的整潔代碼嗡贺,在代碼層面保證了這個過程的實現(xiàn)。
與物理系統(tǒng)相比軟件系統(tǒng)比較獨特鞍帝。它們的架構(gòu)可以遞增式增長诫睬,只要我們持續(xù)將關(guān)注面恰當(dāng)?shù)厍蟹帧?/p>
面向方面編程(aspect-oriented programming, AOP)。在AOP中帕涌,被稱為方面(aspect)的模塊構(gòu)造指明了系統(tǒng)中哪些點的行為會以某種一致的方式被修改摄凡,從而支持某種特定的場景。這種說明是用某種簡介的聲明或編程機制來實現(xiàn)的蚓曼。
下面來看看Java中的三種方面或類似方面的機制亲澡。
3. Java代理
Java代理適用于簡單的情況,例如在單獨的對象或類中包裝方法調(diào)用纫版。然而床绪,JDK提供的動態(tài)代理僅能與接口協(xié)同工作。對于代理類其弊,你得使用字節(jié)碼操作庫癞己,比如CGLIB,ASM或Javassist梭伐。
Proxy API需要一個InvocationHandler對象痹雅,用來實現(xiàn)對代理的全部方法調(diào)用。
4. 純Java AOP框架
在數(shù)個Java框架中籽御,代理都是內(nèi)嵌的练慕,如Spring AOP和JBoss AOP等,從而能夠以純Java代碼(不適用AspectJ)實現(xiàn)面向方面編程技掏。
5. AspectJ的方面
通過方面來實現(xiàn)關(guān)注面切分的功能最全的工具是AspectJ語言铃将,一種提供“一流的”將方面作為模塊構(gòu)造處理支持的Java擴展。AspectJ的弱勢在于哑梳,需要采用幾種新工具劲阎,學(xué)習(xí)新語言構(gòu)造和使用方式。
6. 測試驅(qū)動系統(tǒng)架構(gòu)
通過方面式的手段切分關(guān)注面的威力不可低估鸠真。假設(shè)你能用POJO(舊式Java對象)編寫應(yīng)用程序的領(lǐng)域邏輯悯仙,在代碼層面與架構(gòu)關(guān)注面分離開龄毡,就有可能真正地用測試來驅(qū)動架構(gòu)。將架構(gòu)按需從簡單演化到精細锡垄,沒必要先做大設(shè)計沦零。
最佳的系統(tǒng)架構(gòu)由模塊化的關(guān)注面領(lǐng)域組成,每個關(guān)注面均用純Java(或其他語言)對象實現(xiàn)货岭。不同的領(lǐng)域之間用最不具有侵害性的方面或類方面工具整合起來路操。這種架構(gòu)能測試驅(qū)動,就像代碼一樣