前些日子寫了個小項目,它即簡單又有一定複雜度,正好可以用它來總結一下物件導向設計的一些原則和方法。
由於是公司的商業代碼,不便透露,我這裡就簡單總結描述一下核心需求
簡化版代碼下載(VS2010,C#,無Web無Windows Service)
業務需求:
1. 有三個第三方的Web Service:SourceReaderService、OASystemService和OrderService
2. 監控從SourceReaderService擷取的ReaderResults集合。篩選出合格Result列表,根據這些合格Result,通過OASystemService建立OATask
3. 等待人工確認建立的OATask是否需要建立Order
4. 可以手動在OA System中建立需要建立Order的OATask
5. 監控需要建立Order的OATask(包含手工建立的OATask),一旦發現存在需要建立Order的OATask,通過OrderService建立一條Order,並將Order ID寫回相應的OATask;若該OATask為手動建立的,則需要將它添加到程式的日誌記錄中
6. 監控OrderService中OrderStatus有變化的Order,將OrderStatus更新回OATask
7. 若ReaderResult(從SourceReaderService擷取回來的)在未完成的OATask中存在,不得建立新的OATask
8. 程式需要記錄動作記錄(Reader ID,Task ID,Task Create Date,Order ID,Order Create Date,Task Completed Date)
9. 使用Windows Service實現監控功能,此外還需要Web介面來查看日誌,設定由ReaderResult建立OATask的觸發條件,OATask的Subject、Assigned To等
三個Web Service我自己大體Mock了一下
下面是我寫這個程式時的思路
業務較簡單時的架構設計:透過表象看本質
當商務邏輯較簡單時(比如本例),可以拋開一切細節來看程式的行為,而面向行為編程就等於面向介面編程,因為介面描述的就是行為!
本例中,表面上是三個Web Service之間的互操作,但是實際上該程式的核心業務應該至少包含三個行為:建立Task,建立Order和更新Task。照這個思路,建立三個相應的介面:ITaskCreator、IOrderCreator和ITaskUpdater。同時,它們的行為當然也自然而然地出來了:CreateTask()、CreateOrder()和UpdateTask()。
public interface ITaskCreator { void CreateTask(); } public interface IOrderCreator { void CreateOrder(); } public interface ITaskUpdater { void UpdateTask(); }
寫出這三個介面之後,在我腦中就有了一個大概的構想:在一個Windows Service程式中使用三個Timer持有以上介面,然後在Timer的Elapsed事件中調用相應的介面方法就可以了
要寫易於測試的代碼
介面寫出來後,該寫具體實現了。
似乎ConcreteTaskCreator與SourceReaderService和OASystemService有直接關係,等等,如果ConcreteTaskCreator直接持有了SourceReaderService和OASystemService對象,那麼對它進行單元測試異常地困難。本身第三方的Web Service就對測試不友好,將這些Web Service直接耦合到ConcreteTaskCreator中,使得ConcreteTaskCreator也變得不易測試了。
這個時候就需要將第三方Web Service封裝起來了,具體思路如:
MockSourceReader專門為單元測試設計,而WebServiceSourceReader中則實際持有第三方API類(Web Service),它們實現了ISourceReader介面。
同理,其它兩個Web Service也要使用介面封裝:名稱分別為ITaskSystem和IOrderSystem。
上面的做法實際上是Facade模式的應用。
ConcreteTaskCreator中持有的則是ISourceReader和ITaskSystem介面的對象,這樣,可以通過傳入不同的具體實現來對ConcreteTaskCreator進行測試或者實際運行產品代碼。
同理,ConcreteOrderCreator和ConcreteTaskUpdater均持有ITaskSystem、IOrderSystem介面對象;對於手動建立的OATask,為了擷取ReaderResult的資訊以建立Order,ConcreteOrderCreator還需要持有ISourceReader介面對象。
為了記錄日誌,ConcreteTaskCreator、ConcreteOrderCreator和ConcreteTaskUpdater三個類都要持有資料持久層的對象。這個持久層對象也是使用和封裝第三方Web Service相同的思路,通過介面來封裝它,使得在運行單元測試時不使用真實的資料庫(除非單元測試的目的就是要測試操作真實的資料庫,否則在單元測試時使用真實的資料庫會帶來一系列問題,如測試回合速度慢、舊資料影響新測試等)。
本例是個簡單的例子,無法體現當商務邏輯比較複雜無法立刻得到高層抽象時,寫易於測試的代碼有時會逼出更好的設計這個特點。
不要讓第三方的類庫過度汙染你的程式
SourceReaderService返回ReaderResult,OASystemService中包含OATask和OATaskStatus,OrderService中包含Order和OrderStatus。以上這些都是第三方類庫中我們必須用到的類。
如果在ISourceReader、ITaskSystem和IOrderSystem中直接使用這些類當參數或傳回型別,那麼,我們的高層抽象介面就會被第三方類庫“綁架”。目前來看ITaskSystem使用的是OASystemService這個Web Service,如果變成另外一個第三方API而它需要的參數是XXTask類的執行個體的時候,被“綁架”的高層介面實際上已經無法與OASystemService分離了。所以,要限制第三方類庫的“汙染”範圍,尤其是在第三方類庫不穩定的情況下(不穩定的類庫遠比你想像的普遍,連微軟的.Net Framework都有好多過時的方法、成員)。
按照上面的想法,我們要建立自己的Task、Task Status、Order和ReaderData類(即使目前它們的屬性和三個Web Service提供的類中屬性可能完全一樣),把主動權掌握在自己的手裡!
資料庫是實現細節!
它太重要了,以至於我要再重複一遍:資料庫是實現細節!在項目設計時請不要先考慮資料庫Schema!
太多的程式因為在商務邏輯層摻進了資料庫細節而使得業務層十分笨重!太多的程式因為在設計時是按照Database First的設計思想而使得業務層過多地關注持久層的細節從而導致商務邏輯混亂無法維護!這樣的程式我自己也寫過,也被坑過。
實際上,如果一個程式在持久層無論用哪一種方法(資料庫、XML,甚至是記憶體對象)都不影響其它部分的運行,那麼它應該是一個好的項目,至少在隔離持久層方面,它是好的。
本例的代碼我不準備使用任何複雜的持久方法,只使用一個記憶體對象(IDbAccessor)就能讓程式(或者是單元測試)運行起來。
博弈:代碼量、代碼複雜度與可維護性之間的平衡
單元測試無疑會增加代碼量;
寫出可維護的單元測試是一門大學問,這也會增加工作量;
可測試的程式無疑比不可測試的程式複雜度更高;
設計模式會增加複雜度,在濫用設計模式的情況下更甚;
好的抽象、封裝和單元測試會大大增加代碼的可維護性;
如何取捨?
我個人是這樣認為的:
1. 如果一個項目要維護較長時間(個人認為一年半或兩年以上就足夠長了),好的單元測試必不可少
2. 好的抽象和封裝水平因人而異,總之努力吧(這方面我水平也不高)
3. 使用設計模式要謹慎。每當要應用一個設計模式時先問一下為什麼要用?如果你明確地知道應用DP的代碼在今後會產生變化(大多數DP的目的是封裝變化點)或者為了讓代碼可測試,那麼是應該應用DP的;如果是為了DP而DP(It’s cool,it’s 霸氣酷狂屌!),或者是你根本不確定這一塊代碼今後會不會變化,那麼,先做簡單的封裝也許會更好。
4. 如果Deadline很緊,要做的事情太多,根本沒時間寫單元測試或考慮抽象,《代碼之殤》裡有一節“向死亡進軍”,好好看一下,免費的迷你書裡面就有這一節。如果你改變不了這樣的專案管理制度,也許你應該考慮換個老闆吧