重用為什么那么難?
程序員都是聰明人,沒有誰愿意干重復勞動這樣的傻事,因此,程序中出現重復代碼是程序員的恥辱。就算不能消除重復代碼,至少也可以對于相同的功能,用不同的代碼來實現所以發明新輪子的程序員才會那么多。
面向對象作為一種橫空出世的新技術,首先承諾的就是“更好的重用性”,而“重用性”這樣一個閃閃發光的詞,也的確能夠吸引程序員的實現,那么多新的理論、新的技術、新的方法、新的框架、新的思想,用來說服別人接受的一個最大的理由,就是“更好的重用性”。然而,OO以及一直以來不斷發展的
OO相關技術,對于重用性的提高,作出了多大的貢獻呢?
JavaEye的age0有一段話特別讓我佩服:“我還是得反復強調,OO設計的價值并不在于所謂的“代碼重用”或者“接口重用”,任何一種設計方法都能夠獲得“重用”的能力,就算是寫匯編一樣可以“重用”。”一個同志能夠如此決絕的對于重用不屑一顧,真是了不起。我們還是來面向大多數希望更好的重用的程序員,分析一下在OO出現之后程序員是如何追求重用這一目標的吧。
在面向過程的時代,重用很簡單,就是代碼的重用,函數、函數庫、模板、模板庫。如此而已。在ADT出現之后,世界分裂了。代碼重用的需求,現在分裂為三個部分:數據類型的重用、ADT內部代碼的重用、操作ADT的代碼的重用。
這句話特別關鍵,讓我再仔細分析給大家看看:ADT=抽象數據類型。就是封裝了操作和數據的一種用戶自定義數據類型。
1、如果僅僅是ADT,那么相近的用戶自定義數據類型就無法重用,因此出現了一個數據類型的重用需求;
2、因為ADT封裝了對于數據的操作,對于A類數據的操作,就無法被B類數據重用,因此出現了一個ADT內部代碼的重用需求;
3、因為對于ADT的操作是借助ADT的外部界面執行的,也就是說,對于接近但是不同的類型的操作,必須寫出不同的代碼,因此出現了對于操作ADT的代碼的重用需求。
這樣的分裂的三個需求,在隨后的OO的發展歷史中,分別被兩種方法來初步滿足。第一、第二種需求,被繼承這樣的技術來試圖滿足;第三種技術被泛型類試圖滿足。這兩個技術滿足了三種重用的需求了嗎?沒有!不但沒有滿足,而且還帶來的諸多麻煩的問題,更在分別滿足三種需求的時候,起了沖突。
由于封裝與重用性之間,存在著本質性的沖突,因此,OO的分析、設計、編程方法就始終處于一個難學、難用、難懂的狀態。我們說給OO下定義非常困難,但是大家都應該承認,ADT是OO的根。數據與操作的封裝是一切OO思想的基礎,也是所有OO信奉者從來沒有懷疑的“前提”!
在繼承與泛型不能解決重用難題之后,OO大師們提出了OO設計原則,提出了OO設計模式,這是我接下來的文章里將要細細批駁的兩大“貢獻”。但是OO的原則、模式,依然沒有解決重用難題。在此之后,又有人提出了AOP、IoC這樣的概念,還有人真正開始和我一樣懷疑封裝的意義,而開發了
CGLib,Mixin這樣的動態改變對象行為與結構的技術,這也是我將要批判的“最新進展”。到了這個時候,真正理解OO本質的人,應該已經看出來了, OO時代即將結束,因OO而帶來的混亂也該結束了。現在唯一的問題是:“什么樣的技術,才是可行的、替代的方案呢?”
OO設計原則批判
OO設計原則,這是很多開發資源網站必備的一個欄目、專題、至少也要轉載一篇放在自己的網站上的東西。所有的程序員,如果你不開發面向對象的程序也就罷了——
反正你已經落伍很久了,如果你要想開發OO程序,而竟然沒有把那些OO設計原則熟讀背誦,搞得滾瓜爛熟。那么你就完了,一個公司面試你的時候,問你:“你對SRP的理解是怎么樣的?”,而你居然不知道SRP是什么,那么這家公司你也就別想進去了。作為OO程序員的《舊約圣經》(設計模式自然是《新約圣經》)他怎么就會那么神圣呢?
介紹OO設計原則的文章很多,我在google上搜索了一下:“約有58,200項符合OO設計原則的查詢結果”。真正能夠介紹得透徹的,還真是沒幾個。正好我手邊有一本Bob大叔的《UML
for JAVA Programmers》那上面的介紹,在我看來,是最好的OO設計原則介紹之一了。另外一本不在手邊的《敏捷軟件開發
原則、模式與實踐》也是Bob大叔的,還要詳盡一些。如果要批判,自然要找這樣的靶子來練!
1、單一職責原則(SRP)
一個類只能因為一個原因而改變。
“A class should have one, and only one, reason to change.”
這個原則何等的簡單,又是何等的模糊呢?什么叫做一個原因呢?我們來看下面這個類:
java代碼:
class User{
private String name;
private int age;
public void setName(String name){
this.name=name;
}
public void setAge(int age){
this.age=age;
}
}
請問,這個類是不是違反了SRP原則呢?設置用戶的名字與設置用戶的年齡,是一個原因,還是兩個原因呢?Bob大叔在自己的書里舉了一個例子,說明了違反SRP原則的情況,一個Employee類,包含了計算工資和扣稅金額、在磁盤上讀寫自己、進行XML格式的相互轉換、并且能夠打印自己到各種報表。我說拜托啊大叔!一個類里的方法多到如此驚人的程度,自然是違反了SRP原則,但是我們要為它瘦身,該瘦到什么程度呢?按照大叔繼續給出的自己的答案,它把計算工資和扣稅金額的兩個功能留給了Employee,其他的分離了出去。這個答案正確嗎?員工的工資和稅收是自己算的?還是有一個“財務部”對象來計算的呢?且不說那么掃興的事情,就看看那個類圖里分離出來的那幾個類:
EmployeeXMLConverter、EmployeeDatabase、TaxReport、EmployeeReport、 PayrollReport。這些類還需要有自己的內部數據嗎?請注意,他們事實上都是通過接受Employee對象的內部數據而工作的,換句話說,這些所謂的類,根本就不是什么類,只不過是一個個用Class關鍵字包裹起來的函數庫!當我們看到一個臃腫的Employee類,被拆成6個各不相同的類之后,內心自然升起了“房子打掃干凈之后的喜悅”。但是,且慢!灰塵到哪里去了呢?當我們把一個類拆成6個類之后,那個原本的類自然已經遵守了SRP原則,然后新誕生的5個類,是不是也該遵守SRP原則呢?如果我們不能將一個原則應用于整個系統設計中的所有的對象,僅僅像小孩打掃衛生一樣,把灰塵掃到隔壁房間,這剩下的事情,誰來處理呢?
好吧,我們不要這么嚴厲,畢竟這只是一個原則,追問太深似乎并不合適。我只想再搞清楚幾個問題:按照SRP原則,C++中是不是一律不應該出現多重繼承呢?按照SPR原則,Java中的一個類是不是一律不應該既繼承一個類,又實現一個對象呢?一個簡單的POJO,被動態增強之類的辦法,添加出來的新的持久化能力,是不是也是違反SRP原則的呢?歸根結蒂,我的問題是:按照SPR原則,我那些剩下的,但是又必須要找地方寫的代碼,究竟應該寫在哪里呢?
2、開放-封閉原則(OCP)
軟件實體(類、模塊、方法等)應該允許擴展,不允許修改。
“Software entities (classes, modules, functions, etc.) should be open for
extension, but closed for modification.”
這個原則倒是非常的清楚明白,你不能改已經寫好的代碼,而應該擴展已有的代碼!如何做到這一點呢?Bob大叔舉了一個經典的例子:個人認為這個例子說明的是一個使用接口,隔離相互耦合的類的通常做法。而且這個做法不應叫做OCP,而應該叫做DIP。查了一下c2.com里的OCP的解釋:
In other words, (in an ideal world...) you should never need to change
existing code or classes: All new functionality can be added by adding new
subclasses and overriding methods, or by reusing existing code through
delegation.
但是在Bob大叔的OCP解釋中,這個原則的具體實現被偷換了概念,從“鼓勵多使用繼承”,變成了“鼓勵面向接口編程”。為什么?因為繼承式OCP實踐已經被證明會帶來相當多的副作用,而面向接口編程又如何呢?我們在討論DIP的時候再詳細討論吧。
有一個在JavaEye的討論的連接可以參考:對于OCP原則的困惑
3、里斯科夫替換原則(LSP)
子類型必須能夠替代他們的基本類型。
“Subtype must be substitutable for their base types.”
對于這個問題,我都不用多說什么,只引用Bob大叔在c2上的一句話,以作為我的支持。
“I believe that LSP is falsely believed by some few to be a principle of OO
design, when really it isn't.”
4、依賴關系倒置原則(DIP)
A.上層模塊應該不依賴于下層模塊,它們都依賴于抽象。
B.抽象不應該依賴于細節,細節應該依賴于抽象。
“A. High level modules should not depend upon low level modules. Both should
depend upon abstractions. ”
“B. Abstractions should not depend upon details. Details should depend upon
abstractions.”
Bob大叔還補充說明了一下,這個原則的意思就又變了:更確切的說,不要依賴易變的具體類。也就是說,不容易變的具體類,還是可以依賴的。那么,當我們開始一次系統開放的時候,那些類是易變的具體類呢?那些類是不易變的具體類呢?怎么才算是易變、怎么才算是不易變呢?我們來看看代碼吧:
java代碼:
class A{
public void doA(){
}
}
class B{
A a=new A();
a.doA();
}
按照DIP原則,Class B依賴于一個具體實現類Class A,因此是可恥的!最起碼你應該寫成這樣:
java代碼:
interface A{
public void doA(){
}
}
class AImpl implements A{
public void doA(){
}
}
class B{
A a=new AImpl();
a.doA();
}
這樣,AImpl和B都依賴于Interface A,很漂亮吧。還不夠好,A a=new AImpl();還是很丑陋的,應該進一步隔離!A a=AFactory.createA();在AFactory里,再寫那些見不得人的new
AImpl(); 代碼。然后呢?這還沒完,更加Perfect的辦法,是采用XML配置+動態IOC裝配來得到一個A,這樣,B就能夠保證只知道這個世界上有一個
Interface A,而不知道這個Interface
A是從哪里來的了。這么做的理由是什么呢?有一個很嚇人的理由:“如果A被B直接使用,那么對于A的任何改動,就會影響到B了。這就叫依賴,而這樣的依賴會很危險!”
我們來看看這頗有說服力的話,漏洞何在?A的變化有兩種情況,一種是只修改A中的方法的內部實現,無論是直接使用A還是使用Interface
A的某一個實現,這時候B的代碼都不用改。另一種是修改了方法的調用接口,如果是直接使用A的Class
B,就需要修改相關的調用代碼,而如果是使用接口的呢?就需要同時修改Class B和Interface
A。這樣看來,采用接口方式,反而需要修改更多的代碼!這使用接口的好處何在?
5、接口隔離原則(ISP)
客戶端不應該依賴于自己不用的方法。
“The dependency of one class to another one should depend on the smallest
possible interface.”
這個我就不說了!因為這個原則和SPR原則是矛盾的!就像合成復用原則(CRP)與LSP原則矛盾一樣。
關于這個批判,我昨天晚上只寫了一半,今天算是虎頭蛇尾的寫完了。最后錄一段Bob大叔的話,作為結尾:
什么時候應該運用這些原則呢?首先要給您一個痛苦的告誡,讓所有系統在任何時候都完全遵循所有原則是不明智的。
運用這些原則的最好方式是有的放矢,而不是主動出擊。在您第一次發現代碼中有結構性的問題。或者第一次意識到某個模塊受到另一個模塊的改變的影響時,才應該來看看這些原則中是否有一條或者多條可以用來解決問題。
......
找到得分點的最佳辦法是大量寫單元測試。如果能夠先寫測試,再寫要測試的代碼,效果會更好。
讓我來翻譯一下上面的告誡。原則不是你可以用來預防問題的,而是當你已經陷入麻煩的時候,你可以停下來悔恨一下。至于解決之道,依然不是很清楚,因此,你需要寫大量的單元測試。而且,大量的單元測試并不是幫你檢查你的設計漏洞,而是幫你更真切的感受自己的設計是否正確。至于他究竟是不是正確,這就看個人自己的感覺了。更為驚人的是,在測試驅動開發的建議中,如何驅動開發的準則,竟然是循環的來自于OO設計原則的。
這樣的OO設計原則,就像老爸老媽給我們的人生教誨:“你要做好人啊”,別的什么都沒說。而且我們還遇到了話都說不清的糊涂爹娘,怎么才算好人,不清楚,怎么才算壞人呢?被警察抓了,肯定就是壞人了。至于如何才能做得更好?自己體會吧。
設計模式批判
為什么要批判設計模式?設計模式不是OO開發的救星嗎?作為“可復用的面向對象”的基礎設施,設計模式大大的超越了OO設計原則給予我們的承諾,還記得我們前面的分析嗎?OO設計原則并不擔保你在設計之前就能避免錯誤,相反的,你往往需要在屢屢受傷之后,才會明白設計原則的真諦。而設計模式是如此的偉大,他甚至可以幫你提前避免問題,只要你可能遇到的問題,符合設計模式手冊中,所描述的那種場景,基本上你就可以直接采用相應的設計方案了。如果找不到正好合適的,你也可以改造自己面對的問題,使得他看起來究就像設計模式手冊中描述的那樣,然后你就可以放心使用相應的設計方案了。如果你無法在那23個模式中找到合適的答案——你可真是太挑剔了——那么你只能自己想法組合一下那23個中的2~3模式,總之,一切盡在其中。
好吧,事實其實沒有那么夸張,“GoF”從來沒有宣稱“設計模式”能夠包治百病,更沒有說過使用“設計模式”可以預防疾病,他們也的確謙虛的承認,設計模式肯定不止23個。但是,GoF也必須承認的一點就是:“Design
Patterns原本是用來指導設計的。大多數時候,都是在實際開發之前完成的。”而且,按照設計模式原本的思維模式,應該把一個系統中的各個類,按照他們所說的一堆模式組織起來,其根本目的,就是不讓后來的改動,再去修改以前的代碼,所謂OCP原則在設計模式中的實際體現,就是對擴展開放、對修改封閉。
In other words, (in an ideal world...) you should never need to change
existing code or classes: All new functionality can be added by adding new
subclasses and overriding methods, or by reusing existing code through
delegation.
再強調一遍:“設計模式認為,靈活性、可復用性是設計出來的”,而在此之后的發展我們可以看到,新的大師們又偷換了概念,將“設計——實施”的兩階段過程,變成了一個“TDD+重構”的持續改進過程,他們不但提倡改以有的代碼,而且要大改特改,持續不斷的改,唯一還帶著的帽子是:“重構的目標是得到設計模式。”重構真的能以設計模式為目標嗎?我們下一篇再討論。
請允許我先借力打力,利用重構這一新生事物,攻擊一下設計模式這個老東西。為什么靈活性、可復用性不能是設計出來的?
軟件開發,一個很重要的工作,就是對付“需求變更”,軟件工程的辦法是盡可能的抵擋、或者有效控制變更的發生。技術人員的目標,則是努力開發出更加靈活的技術,以適應可能的變化。值得一提的是,現在軟件開發的管理者,也開始相信,擁抱變化比限制變更,是更為可取的手段了。
更加靈活的技術,更加容易理解,方便描述事實的語言,設計模式等等等等,都是用來適應可能的變化的。對于技術人員來說,如果能夠預測可能的變化,并在系統設計期就將其考慮進去,那么變化就成為計劃的一部分,注意這句話,因為實際的情況往往是:“計劃趕不上變化”。越是自信的技術人員,越是自以為能夠預測可能的變化,將變化提前設計進入自己的系統。所以,濫用設計模式的人,往往是那些自以為水平很高的半桶水。而重構這一思路的出現,就是對于設計模式這種“企圖預測變化”的否定。事實上,即使是重構,也是危險的,因為從原始狀態,到第一個變化發生時,我們能夠得到的只有兩個狀態點,這兩個點聯成直線所指向的一個方向,并不一定就是變化的方向,因此,重構是一個好辦法,而將得到設計模式作為重構的目標,卻相當危險。
設計模式背后的思路非常清楚,就是將可能變化納入設計模式的考慮之中,所以我們看到設計模式的目標“可復用”,其實是一個轉了一個彎以后的目標,“在盡可能重用已有代碼的前提下,適應變化”。我的觀點是:“首先需要滿足的不是復用,而是快速修改”,這個問題太大以后有機會再討論吧。
這篇關于設計模式的批判,我寫了好幾天,始終感覺難以下手。今天和徐昊討論,他的話我認為非常有道理:“設計模式的成功,正好證明了OO的失敗”。這個思路相當有用,我決定就按這個調子來寫。當然,設計模式并不是只有一個,而是有很多很多個,作為一種“專家經驗交流的規范描述格式”,設計模式已經非常多了。我們今天也不批判更多的模式,僅僅對GoF的23個模式下手吧。
GoF的23個設計模式,主要分為三類:創建型模式、結構型模式、行為型模式。我們就分別批判這三種模式吧。
創建型模式之所以需要,其實正好證明了OO的失敗。因為在OO的思想中,創建對象這種事情,天然就應該是對象自己處理的。一個類在定義時,可以定義一個以上的構造函數,來決定如何得到自己的實例,那為什么還需要把創建對象這個事情,交到別人手里?按照SRP原則,無論出于什么原因,創建自己總是自己的職責吧!所以按照SRP原則,所有的創建型模式都不應該出現,出現了也是錯誤的。但是為什么我們需要創建型模式呢?先看看GoF的解釋:“隨著系統演化得越來越依賴于對象復合而不是類繼承,創建型模式變得更為重要。當這種情況發生時,重心從對一組固定行為的硬編碼,轉移為定義一個較小的基本行為集,這些行為可以被組合成任意數目的復雜的行為,這樣創建有特定行為的對象要求的不僅僅是實例化一個類。”
這樣的解釋,有一定的道理,但是卻局限于“用組合取代繼承”這樣一個“當年的熱門話題”。在我看來,根本的原因在于:“在OO的經典思想中,對象之外的環境是不存在的,因此,所有的對象,都要考慮如何產生自己,如何銷毀自己,如何保存自己,總之,自己的事情自己做。”Java的一個巨大進步就在于,銷毀自己這件事情,不再強求對象自己去考慮了,因為這種事情過于瑣碎,而且復雜易錯。但是創建自己這件事情,java依然沒有考慮到也不該交給對象自己考慮的。于是設計模式中的種種創建模式被發明出來,用以滿足復雜多變的創建需求。這個根本原因同時也解釋了為什么單例模式會被發明,按照GoF的解釋,是無法說明為什么我們會需要單例模式地。而當我們有了對象環境的概念之后,各種創建自然有“容器環境”來完成,“單例”模式也只需要在環境中配置,有了OO容器之后,所有的創建模式都被一個概念所代替了。在沒有這樣的概念之前,我們需要用各種模式技巧,來實現“支離破碎”的環境。而在真正的容器環境出現之后,我們就不再需要這些設計模式了。
讓我再說一遍:“如果你能夠理解為什么現在會出現那么多容器,就能理解設計模式中的創建模式,只不過是用來解決OO概念欠缺的一種不完善的手段了。”
再來看結構型模式,個人認為將“Adapter、Bridge、Composite、Decorator、Facade、 Flyweight、Proxy”統統歸入結構型模式,是不恰當的。除了Composite模式算是結構模式,Flyweight算是一種“節約內存的技術手段”之外,其他的模式,都屬于打破OO靜態封裝的技巧。我們知道,按照OO的設定,一個類,就是一種類型,它在代碼寫下的時候,就已經決定了自己的內部數據與外部行為。怎么還能在運行的時候再改變呢?事實證明,這樣需求不但存在,而且重要,設計模式之所以被大家欣賞,一個重要的原因,就是他能夠幫助程序員部分擺脫“靜態封裝屬性與行為”的限制。
OO能從關系型數據庫借鑒些什么?
今天這篇是關于OO VS. RDB的,OO作為一種編程范型,主要是集中于處理“操作”,而RDBMS作為一種數據管理工具,主要是集中于“數據”。但是,在一個需要數據庫的系統中,必然的情況是:操作的對象自然是各種各樣的數據,而數據的管理,自然要通過操作。因此,OO與RDB,從最初淺的角度來理解,雖然分別位于“業務邏輯層”與“數據層”,但是相互之間卻又有著非常緊密的聯系。在OO與RDB之間存在著的緊張關系,其根源在于:“OO世界中的數據是被封裝的孤立數據;RDB世界中的操作是針對行集的。”
因此,一個對象實例內部的數據,應該保存到數據庫中的哪些表、哪些行、哪些列,是一個非常復雜的問題。反過來說,當我們要在內存中恢復一個對象實例時,要從多少表、多少行、多少列中采集數據,并正確轉化為對象實例的內部數據,也是相當的復雜。O/R
Mapping,需要考慮的問題還不止于此,在RDB中自然存在的“關系”這一概念,在OO當中,卻沒有明確的對應概念。在OO的世界里,對象之間存在各種各樣的關系,卻非常難以進行規范化的描述。再加上:“添加、修改、刪除、查詢”等等情況的O/R映射,以及與“關系”相關的“級聯操作”——“級聯添加、級聯修改、級聯刪除、級聯查詢”,一個好的O/R
Mapping工具,要做出來真是千難萬難。
很多人都意識到了這一點!是的,問題的確存在。但是,為什么呢?該怎么辦呢?幾乎沒有人會反思OO的不是,而是想當然的認為:“關系數據庫技術太老了,跟不上OO技術的進步,因此,我們需要OODB這樣的能夠直接支持OO的數據庫。”
“以其昏昏,使人昭昭”的事情,從來沒有發生過。依著我前面的分析,在OO這樣的基礎薄弱的理論上,怎么可能搞出有實用價值的數據庫呢?
在看到了徐昊的《關于面向對象設計與數據模型的精彩論述》之后,我相信自己找到了知音。他說:“OO在數據模型設計上不具有思維簡潔性。”并且提出了一個重要的詞匯:“邊語義”!這使我相信,和我有類似想法的同志,是存在的。后來又現場聽到了曹曉鋼同志的《ORM時代的數據訪問技術》的演講,并且在他的筆記本里看到了他做的一些代碼,居然與我的很多想法不謀而合!再加上后來與徐昊的幾次MSN交流,終于使我敢于開始寫這樣一篇“OO喪鐘”的文章,因為,我相信自己并不是孤獨的反對者。
OO可以從關系型數據庫那里借鑒些什么呢?
1、關系:也就是徐昊所說的邊語義。在
OO中,對象與對象之間是否存在關系,在對象之外是不知道的。當一個對象被封裝起來以后,他內部是否使用、關聯、組合了其他的對象,是不可知的。因此,我們看到的通常的OO圖,只能說是Object被剖開了以后的對象圖。事實上,關系是被隱藏起來的。而在RDB中,關系非常明確的被定義與標識出來,一目了然。這將帶來巨大的描述效果。相比起UML
Class圖,E-R要容易理解得多。
2、Primary Key:這是RDB
特有的概念,在OO中沒有對應概念。因此,我們要判斷兩個對象是否相等,就相當困難。如果每個對象都有一個“一次設置,終身不變的Primary
Key”,那么對象之間的比較語義,就能夠被清楚的區分為:IS和LIKE。IS就是Primary
Key相同的兩個對象,他們應該完全一致,甚至在內存中,也只應該保存一份。LIKE,就是成員數據相同的兩個對象,他們不是一個東西,僅僅是像而已。
3、SQL:這也是RDB特有的語言,而在OO的世界里,查找一個對象的工作,從來沒有被規范過。