很有經驗的面向對象開發者也會有這種感覺嗎?

[引言]

面向對象設計(OOD)在解決軟體設計複雜性上有其貢獻,但是也因為不當的使用、過於複雜的繼承樹、過度的設計等導致OO受到越來越多的詬病(ref: 孟岩、雲風、陳碩等人博客,以及一些國外軟體開發大牛之評論[Object Oriented Programming is Inherently Harmful])。

很多OO設計的代碼開始變得對程序員越來越不友好。

[問題]

這裡摘抄一段Unix大師Eric Raymond的評述(Eric Raymond談模塊化原則,膠合層和面向對象的缺陷 by孟岩),以下為孟岩的譯文:

所有的OO語言都有有一些傾向,吸引程序員跳進「過度分層」的陷阱里。對象框架和對象瀏覽器並不能取代好的設計和文檔,但是卻經常被看成一回事。太多的層次破壞了透明性——我們很難看穿下面的東西,很難在思想上對於代碼的功能建立清晰的模型。簡單性、明晰性和透明性一口氣全被破壞了,結果代碼充滿了晦澀的錯誤,帶來嚴重的維護性問題。

想知道Raymond的這種**難以看清**的感覺主要存在於老牌C程序員眼中,還是對於相當有經驗的OO語言開發者亦存在?

設計上的疊床架屋難道不是因為問題的複雜而導致嗎,開發者豈會是為了享受這種病態的樂趣?大家也都是為了抽象出一個clean的interface啊。

如果對OO的詬病是針對不成熟的面向對象開發者,那麼同樣的批評對於其他軟體開發範式的使用者也必然是少不了的啊。大牛們對此問題的關注難道是因為軟體開發領域充斥的是這種三流的OO designer?

另薦兩篇引人深思的趣文:

Java Developers,Why I hate frameworks(The HammerHammerFactory)


這裡說的透明性似乎和抽象相關。抽象應該是要令事物簡化,把重要概念變得更清晰。

閉源框架是不透明的,但只要它有清晰的模型及API,使用者不還是可以好好利用?

分層是一種設計方法,令每層有更精確的責任,並且顯示更清楚的依賴關係(上層依賴下層)。

每種方法論都可以被濫用、不當使用,造成過度設計等各種問題。很多時候是怎樣掌握「度」的問題。


真要是「有經驗」的,就不會自稱「面向對象開發者」了。

你看見過舒馬赫自稱「V型10缸賽車手」?


經驗有限,我還是直接回答你吧:『是的,有這種感覺,天天有,很久了』


想知道Raymond的這種**難以看清**的感覺主要存在於老牌C程序員眼中,還是對於相當有經驗的OO語言開發者亦存在?

不管是任何程序員都存在這種問題——「難以看清」。

設計上的疊床架屋難道不是因為問題的複雜而導致嗎,開發者豈會是為了享受這種病態的樂趣?

不會。

大牛們對此問題的關注難道是因為軟體開發領域充斥的是這種三流的OO designer?

不僅僅是因為水平問題,還有OO本身也有自己的適用範圍。

下面是個人看法:

編程是對現實世界的抽象。也就是建立一種簡化的模型來描述現實世界。

簡化抽象模型的目標本質上來說就是降低複雜度,只關注想要關心的東西。

正因為編程本身的這一本質特徵,所以複雜度就是編程最大的敵人之一。

如何降低複雜度也是編程設計里最大的挑戰之一。

面向對象的設計思路就是為了降低複雜度提供的一種解決方案。

這種方法從結構上來說關注的是什麼?

就是數據的封裝(類),數據關係(組合,繼承),

然後通過不同比例的映射(繼承和組合的搭配)達到多態的目的。

封裝,數據關係,組合,映射等概念不是面向對象才有的概念,

用其他編程範式一樣是需要的,只不過手法和思維不同。

而繼承這種思路是面向對象的特別特徵,而這個特徵是雙刃劍。

繼承的本質其實就是關係的特定化。

這種特定化帶來的好處就是能夠把N特化為1。

優點就是缺點,所以它帶來的壞處就是失去了靈活性。

只要是用了繼承,那麼就逃脫不了樹和分層的結構

而最開始說到的編程實際上是種簡化的模型,

所以如果遇到模型關注點本身就是錯誤或者有重大遺漏的時候,

使用繼承這種東西帶來的特化為1的性質就會成為重大的設計問題。

不同層次的人看同樣的問題,關注簡化的模型是不一樣的。

於是為了解決上面提到的重大的設計問題和保持一定的版本穩定

你會發現很多良好的OO框架看起來比較複雜且有種過度設計的感覺。

樓主你可以自己嘗試回答一下你自己提出來的問題:

很多OO設計的代碼開始變得對程序員越來越不友好是因為什麼?

這種**難以看清**的感覺主要存在於老牌C程序員眼中,還是對於相當有經驗的OO語言開發者亦存在?

如何解決編程中為了降低複雜度使用簡化模型和現實世界不斷增加的複雜度之間的衝突?


1. 面向對象本身是有問題,哪個一種設計沒有問題對不對,又沒有找到四海皆對的準則。

2. 從面向對象產生到現在,大規模的使用,仍然沒有被新的設計替換。就像c語言一樣,可見面向對象絕對是解決了很多問題的。

3. 根據我個人的感受,通常情況。使用了面向對象的代碼絕對是比沒有使用的,好維護好理解一些的。大家水品參差都用一個模式來,無論用的如何,也算是一個模子刻的了。

4. 未來一定有更好的模式出現,也一定會吸收面向對象的優點。拋開項目,拋開使用者的水平經驗,拋開具體應用場景的特性。單純的討論面向對象好壞是么有意義的。


這是人性的缺陷帶來的。所以才會提倡TDD嘛。TDD最關鍵的地方不是先寫test case(其實這是不可能的),而是要讓你的test case先編譯過。這就要求你先給出類的介面,然後寫test case。整個過程會有助於你思考你的類到底組織的合適不合適。等test case寫完了,你就可以開始填代碼了。再有經驗也很難一下子就拿出個好的設計來。


跟需求有很大關係。

現在普遍承認oo編程在gui最終實現中還是有很大好處的。

可以很大低降低使用複雜度。

但伺服器端則不一樣,很多情況下僅僅需求一個輸入一個輸出, 調用層次太多會造成性能降低和維護難度。

而且一般調用間,相關度不高(會從設計上盡量分開,不用oo還簡單點。)

題主列舉的名人大多不是做ui的


沒有壞語言,只有爛設計

所有的語言都是工具,都有最適合的應用條件,儘可能的為不同的任務分配正確的工具.

比如你發現你有一個Class是這樣的: 法拉利 &<- 義大利跑車 &<- 跑車 &<- 轎車 &<- 機動車

那麼我覺得,你第一個反應不應該是抱怨OOP的繼承太麻煩,而是應該重新考慮一下,是不是自己的設計太繁瑣.

總有人喜歡把抽象,耦合,過載,反射什麼的掛在嘴上,其實在OOP發明以前,很多程序已經寫的漂亮了. 嚴謹的邏輯,清晰的說明,明確而單一的功能,這才是一個好設計應該有的東西.

我認識一個教授,他就無數次的提醒說一個function 意味著一個輸入,一個輸出,如果違背了這個原則,那麼就是你的問題了. 我不能說他說的絕對正確,但是簡化功能設計,確實是解決現在很多設計模式混亂的好方法.


能分幾個類 就分幾個類,代碼行少點,命名好點 就謝天謝地了。。。


本來敲了一大段話,結果到後來反而越來越疑惑了,到底啥叫OO設計思想啊?劃分模塊、設計介面、解耦這些應該是所有軟體設計都要考慮的事,那OO在設計過程中到底還能扮演什麼角色?怎麼感覺OO就是個實現有些設計的工具,而本身在並不是一種設計思想呢

-----------------------------------------------------

注意重點:所有的OO語言都有一些傾向,吸引程序員跳進「過度分層」的陷阱里

OO沒問題,抽象沒問題,分層沒問題;但語言都有傾向,我見過把這些東西都用過度到極致的代碼,我跟我同事們都只能用「噁心」二字來形容。現在對OO的批判僅僅是對一些濫用的反思而已。

另外個人覺得層次的增加要極其慎重,增加層次除了增加代碼理解的難度外,還增加了它的上層和下層跟這層之間的介面上的耦合,是個在設計上就牽扯比較大的事。所以開源軟體里個人最喜歡類似工具庫的東西,完全是平面的,無任何層次,怎麼調都可以,想摳什麼出來就最多要拷一兩個文件;而相反框架類的東西就不太喜歡了,更不要說那種下面依賴了好幾層東西的框架。


無論是過程也好,OO也好,拯救不了沒經驗寫的垃圾代碼,誰都不是神仙,都是罵幾個月乃至一年以前的自己寫的代碼,然後重構或在下一個項目提升成長起來的。


再好的語言也會有人用得很爛,同樣一個語言,有人設計的很好,有人設計的很爛,可見跟語言本身是沒有關係,關鍵是用的人。

討論一個概念的好壞是無解的,除非面向對象有一個很具體的設計準則,否則任何討論都是沒有意義的。


那些年搞不懂的高深術語——依賴倒置?控制反轉?依賴注入?面向介面編程

那些年,空氣中彷彿還能聞到漢唐盛世的餘韻,因此你決不允許自己的臉上有油光,時刻保持活力。然而,你一定曾為這些「高深術語」感到過困擾。也許時至今日,你仍對它們一知半解。不過就在今天,這一切都將徹底改變!我將帶領你以一種全新的高清視角進入奇妙的編程世界,領略涵泳在這些「高深術語」中的活潑潑的地氣,以及翩躚於青萍之末的雲水禪心。

·內聚

內聚,通俗的來講,就是自己的東西自己保管,自己的事情自己做。

經典理論告訴我們,程序的兩大要素:一個是數據(data),一個是操作(opration)。而 PASCAL之父Nicklaus Wirth則進一步提出了「程序 = 數據結構 + 演算法」的著名公式。雖然提法上有所差異,但是其根本內涵卻是一致的,微妙的差別在於,「數據 + 操作」是微觀的視域,「數據結構 + 演算法」則是中觀的視域。而在宏觀的視域下,我認為「程序 = 對象 + 消息」。對象是什麼?對象就是保管好自己的東西,做好自己的事情的程序模塊——這就是內聚!傳統的面向過程編程方法由於割裂了數據結構和演算法,使得軟體的內聚性普遍低迷,曾一度引發了軟體危機。試想,大家都自己的東西不好好保管,自己的事情也不好好做,不引發危機才怪呢!當然,對象的內聚只是內聚的一個層次,在不同的尺度下其實都有內聚的要求,比如方法也要講內聚,架構也要講內聚。

《周易·彖傳》中講「乾道變化,各正性命,保合太和,乃利貞」,就是要求每一個個體因循著各自的稟賦而努力成就各自的品性,然後各自保全,彼此和合,最終達成宇宙的完滿狀態。《論語·憲問》中,子路問君子。子曰:「修己以敬。」曰:「如斯而已乎?」曰:「修己以安人」,更是明確的教導我們要不斷提高自身的內聚性,最大限度地減少給他人造成的麻煩,從而達到安人、安百姓、安天下的目標。我想,成長的過程就是一個不斷提升內聚的過程。「自己的東西自己保管,自己的事情自己做」,這些孩提時代的教誨,放到今天仍能讓不少「大人」臉紅不已。太多的人保管不好自己的「東西」,保管不好自己的身體,保管不好自己的婚姻,更保管不好自己如蛛絲般震顫飄蕩的狂亂的心。至於做好自己的事情,則更是惘然,甚至很多人連自己的事情是什麼都搞不清楚,因此渾渾噩噩,飽食終日。內聚,是一個值得我們好好反思的問題。

·依賴·耦合

在面向對象編程中,對象自身是內聚的,是保管好自己的數據,完成好自己的操作的,而對外界呈現出自己的狀態和行為。但是,沒有絕對的自力更生,對外開放也是必要的!一個對象,往往需要跟其他對象打交道,既包括獲知其他對象的狀態,也包括仰賴其他對象的行為,而一旦這樣的事情發生時,我們便稱該對象依賴於另一對象。只要兩個對象之間存在一方依賴一方的關係,那麼我們就稱這兩個對象之間存在耦合。 比如媽媽和baby,媽媽要隨時關注baby的睡、醒、困、哭、尿等等狀態,baby則要仰賴媽媽的餵奶、哄睡、換紙尿褲等行為,從程序的意義上說,二者互相依賴,因此也存在耦合。首先要說,耦合是必要的。我們來看以下這個實驗。

【王陽明與山中之花

View Code

由於王陽明這個對象不依賴山花這個對象,又沒有其他的方式來獲知山花的盛開狀態,所以他要麼選擇不說,要麼瞎說,但不說編譯是通不過,而瞎說作為王陽明來講也是通不過的,所以這個系統是無法成立的。要想系統成立,必須要這樣寫:

public bool AdmireFlowers()
{
return flower.IsBloomed; ;
}

無論這個山花對象是怎麼來的,作為參數傳入還是作為屬性設置、還是在內部構造出來,總之,王陽明與山花之間發生了依賴,二者之間產生了耦合。 當然,這是一個很淺顯的問題。有趣的是王陽明對此事的看法:「你未看花時,花與你同寂;你來看花,花於你則一時分明起來。可見心外無物!」王陽明講的是對的!「心外無物」翻譯技術語言是這樣的:不存在耦合的兩個對象必然拿不到對方的引用!

·耦合度·解耦和

耦合的程度就是耦合度,也就是雙方依賴的程度。上文所說的媽媽和baby就是強耦合。而你跟快遞小哥之間則是弱耦合。一般來說耦合度過高並不是一件好事。就拿作為IT精英的你來說吧,上級隨時敦促你的工作進度,新手頻繁地需要你指導問題,隔三差五還需要參加酒局飯局,然後還要天天看領導的臉色、關注老婆的心情,然後你還要關注代碼中的bug 、bug、bug,和需求的變化、變化、變化,都夠焦頭爛額了,還猝不及防的要關注眼睛、頸椎、前列腺和頭髮的狀態,然後你再炒個股,這些加起來大概就是個強耦合了。從某種意義上來說,耦合天生就與自由為敵,無論是其他對象依賴於你,還是你依賴其他對象。比如有人嗜煙、酗酒,你有多依賴它們就有多不自由;比如有人家裡生了七八個娃,還有年邁的父母、岳父母,他們有多依賴你,你就有多不自由。所以老子這樣講:「五音令人耳聾,五色令人目盲,馳騁狩獵令人心發狂,難得之貨令人行妨。」盧梭也是不無悲涼的說「人生而自由,卻又無往而不在枷鎖中」。因此,要想自由,就必須要降低耦合,而這個過程就叫做解耦和。

·依賴倒置(Dependence Inversion Principle)

解耦和最重要的原則就是依賴倒置原則:

高層模塊不應該依賴底層模塊,他們都應該依賴抽象。抽象不應該依賴於細節,細節應該依賴於抽象。

《資本論》中都曾闡釋依賴倒轉原則——在商品經濟的萌芽時期,出現了物物交換。假設你要買一個IPhone,賣IPhone的老闆讓你拿一頭豬跟他換,可是你並沒有養豬,你只會編程。所以你找到一位養豬戶,說給他做一個養豬的APP來換他一頭豬,他說換豬可以,但是得用一條金項鏈來換——所以這裡就出現了一連串的對象依賴,從而造成了嚴重的耦合災難。解決這個問題的最好的辦法就是,買賣雙發都依賴於抽象——也就是貨幣——來進行交換,這樣一來耦合度就大為降低了。

再舉一個編程中的依賴倒置的例子。我們知道,在通信中,消息的收發和消息的處理往往密不可分。就一般的通信框架而言,消息的收發通常是已經實現了的,而消息的處理則是需要用戶來自定義完成的。先看一個正向依賴的例子:輕量級通信引擎StriveEngine。tcpServerEngine是StriveEngine.dll提供通信引擎,它發布有一個MessageReceived事件。假設我定義了一個CustomizeHandler類來用於消息處理,那麼CustomizeHandler的內部需要預定tcpServerEngine的MessageReceived事件,因此customizeHandler依賴於tcpServerEngine,這就是一個普通的依賴關係,也就是高層模塊依賴於低層模塊。

而ESFramework通信框架應用了依賴倒轉原則。ESFramework定義了一個IcustomizeHandler介面,用戶在進行消息處理時,實現該介面,然後將其注入到rapidPassiveEngine客戶端通信引擎之中。

View Code

很明顯,相比於上一個例子,這裡的依賴關係變成了rapidPassiveEngine依賴於customizeHandler,也就是說依賴關係倒置了過來,上層模塊不再依賴於底層模塊,而是它們共同依賴於抽象。rapidPassiveEngine依賴的是IcustomizeHandler介面類型的參數,customizeHandler同樣是以實現的介面的方式依賴於IcustomizeHandler——這就是一個依賴倒置的典範。

·控制反轉(Inversion of Control)

控制反轉跟依賴倒置是如出一轍的兩個概念,當存在依賴倒置的時候往往也存在著控制反轉。但是控制反轉也有自己的獨特內涵。

首先我們要區分兩個角色,server 跟 Client,也就是服務方和客戶方。提供服務端的一方稱為服務方,請求服務的一方稱為客戶方。我們最熟悉的例子就是分散式應用的C/S架構,服務端和客戶端。其實除此之外,C/S關係處處可見。比如在TCP/IP協議棧中,我們知道,每層協議為上一層提供服務,那麼這裡就是一個C/S關係。當我們使用開發框架時,開發框架就是作為服務方,而我們自己編寫的業務應用就是客戶方。當Client調用server時,這個叫做一般的控制;而當server調用Client時,就是我們所說的控制反轉,同時我們也將這個調用稱為「回調」。控制反轉跟依賴倒置都是一種編程思想,依賴倒置著眼於調用的形式,而控制反轉則著眼於程序流程的控制權。一般來說,程序的控制權屬於server,而一旦控制權交到Client,就叫控制反轉。比如你去下館子,你是Client餐館是server。你點菜,餐館負責做菜,程序流程的控制權屬於server;而如果你去自助餐廳,程序流程的控制權就轉到Client了,也就是控制反轉。

控制反轉的思想體現在諸多領域。比如事件的發布/ 訂閱就是一種控制反轉,GOF設計模式中也多處體現了控制反轉,比如典型的模板方法模式等。而開發框架則是控制反轉思想應用的集中體現。比如之前所舉的ESFramework通信框架的例子,通信引擎回調用戶自定義的消息處理器,這就是一個控制反轉。以及ESFramework回調用戶自定義的群組關係和好友關係,回調用戶自定義的用戶管理器以管理在線用戶相關狀態,回調用戶自定義的登陸驗證處理,等等不一而足。再比如與ESFramework一脈相承的輕量級通信引擎StriveEngine,通過回調用戶自定義的通信協議來實現更加靈活的通信。

由此我們也可以總結出開發框架與類庫的區別:使用開發框架時,框架掌握程序流程的控制權,而使用類庫時,則是應用程序掌握程序流程的控制權。或者說,使用框架時,程序的主循環位於框架中,而使用類庫時,程序的主循環位於應用程序之中。框架會回調應用程序,而類庫則不會回調應用程序。ESFramework和StriveEngine中最主要的對象都以engine來命名,我們也可以看出框架對於程序主循環的控制——它會為你把握方向、眼看前方、輕鬆駕馭!

·依賴注入(Dependency Injection)

  依賴注入與依賴倒置、控制反轉的關係仍舊是一本萬殊。依賴注入,就其廣義而言,即是通過「注入」的方式,來獲得依賴。我們知道,A對象依賴於B對象,等價於A對象內部存在對B對象的「調用」,而前提是A對象內部拿到了B對象的引用。B對象的引用的來源無非有以下幾種:A對象內部創建(無論是作為欄位還是作為臨時變數)、構造器注入、屬性注入、方法注入。後面三種方式統稱為「依賴注入」,而第一種方式我也生造了一個名詞,稱為「依賴內生」,二者根本的差異即在於,我所依賴的對象的創建工作是否由我自己來完成。當然,這個是廣義的依賴注入的概念,而我們一般不會這樣來使用。我們通常使用的,是依賴注入的狹義的概念。不過,直接陳述其定義可能會過於詰屈聱牙,我們還是從具體的例子來看。

  比如OMCS網路語音視頻框架,它實現了多媒體設備(麥克風、攝像頭、桌面、電子白板)的採集、編碼、網路傳送、解碼、播放(或顯示)等相關的一整套流程,可以快速地開發出視頻聊天系統、視頻會議系統、遠程醫療系統、遠程教育系統、網路監控系統等等基於網路多媒體的應用系統。然而,OMCS直接支持的是通用的語音視頻設備,而在某些系統中,需要使用網路攝像頭或者特殊的視頻採集卡作為視頻源,或者其它的聲音採集設備作為音頻源,OMCS則提供了擴展介面——用戶自己實現這個擴展的介面,然後以「依賴注入」的方式將對象實例注入到OMCS中,從而完成對音、視頻設備的擴展。

「依賴注入」常常用於擴展,尤其是在開發框架的設計中。從某種意義上來說,任何開發框架,天生都是不完整的應用程序。因此,一個優秀的開發框架,不僅要讓開發者能夠重用這些久經考驗的的卓越的解決方案,也要讓開發者能夠向框架中插入自定義的業務邏輯,從而靈活自由地適應特定的業務場景的需要——也就是說要具備良好的可擴展性。比如上面提到的OMCS網路語音視頻框架可應用於音、視頻聊天系統、視頻會議系統、遠程醫療系統、遠程教育系統、網路監控系統等等基於網路多媒體的應用系統;以及ESFramework通信框架能夠應用於即時通訊系統,大型多人在線遊戲、在線網頁遊戲、文件傳送系統、數據採集系統、分散式OA系統等任何需要分散式通信的軟體系統中——這種良好的擴展性都與「依賴注入」的使用密不可分!

·面向介面編程

談到最後,「面向介面編程」已經是呼之欲出。無論是依賴倒置、控制反轉、還是依賴注入,都已經蘊含著「面向介面編程」的思想。面向介面,就意味著面向抽象。作為哲學範疇而言,規定性少稱為抽象,規定性多稱為具體。而介面,就是程序中的一種典型的「抽象」的形式。面向抽象,就意味著面向事物的本質規定性,擺脫感性雜多的牽絆,從而把握住「必然」——而這本身就意味著自由,因為自由就是對必然的認識。

也許以上的這段論述太過「哲學」,但是「一本之理」與「萬殊之理」本身就「體用不二」——總結來看,依賴倒置、控制反轉、依賴注入都圍繞著「解耦和」的問題,而同時自始至終又都是「面向介面編程」的方法——因此,「面向介面編程」天生就是「解耦和」的好辦法。由此也印證了從「抽象」到「自由」的這一段範疇的辯證衍化。

「面向對象」與「面向介面」並非兩種不同的方法學,「面向介面」其實是「面向對象」的內在要求,是其一部分內涵的集中表述。我們對於理想軟體的期待常被概括為「高內聚,低耦合」,這也是整個現代軟體開發方法學所追求的目標。面向對象方法學作為現代軟體開發方法學的代表,本身就蘊含著「高內聚,低耦合」的思想精髓,從這個意義上來說,「面向對象」這個表述更加側重於「高內聚」,「面向介面」的表述則更加側重於「低耦合」——不過是同一事物的不同側面罷了。

除此之外,我們也能從「面向介面編程」的思想中得到「世俗」的啟迪——《論語》裡面講,不患無位,患所以立;不患人之不己知,患其不能也——就是教導我們要面向「我有沒有的本事?」、「我有沒有能力?」這樣的介面,而不是面向「我有沒有搞到位子?」、「別人了不了解我?」這樣的具體。依我看,這是莫大的教誨!


這幫人比較有經驗,看過的爛代碼比自己寫得好代碼多得多,才會有這樣的提法

他們跳出來喊,估計是擦屁股的活干太多了,受不了了


我始終反對這種說法。

我記得以前看《代碼大全》的時候,裡面有個說法是:軟體的首要使命是」管理複雜度「。低複雜度的編碼過程可以讓你在一段時間內把精力集中在某些特定的問題上,不被其他的因素所影響。

OO的分層思想在降低複雜度方面無疑能夠提供巨大的幫助。


推薦閱讀:

怎麼樣知道自己有沒有掌握面向對象編程的思想?
Haskell中的class和其它語言(如Rust)中的Trait的區別和聯繫?
面向對象的RAII怎麼處理阻塞型的資源獲取過程?
在面向對象編程時對於類的劃分有哪些心得?
如何通俗易懂地舉例說明「面向對象」和「面向過程」有什麼區別?

TAG:編程 | 軟體設計 | 面向對象編程 | 設計模式 |