現代的軟件科學中, 很多內容和概念, 實際上是從數學/語言學等相當古老的領域裡借來的, 為什麼呢? 因為軟件科學中的很多方面, 與其它學科中所碰到的問題並無不同. 一套數學理論,某個數學公式,無論從哪個層次去看,和它們有關的人分為兩種:發明者,使用者. 這和軟件也是相當一致的, 軟件首先要有人編制, 然後別人來使用(好不好另說). 數學的一個特性就是他的抽象性, 本文討論軟件設計中, 由抽象所展開的一些問題.
對抽象的理解的誤區可能使得很多人忽視了廣泛存在的抽象. 因為接口和類如何抽象現實事物的種種說法, 在很多人的觀念裡混淆了在設計時具有的抽象, 從而對抽象的本質進行的歪曲, 忽略了除OO建模以外抽象, 或者在OO建模這個過程中選擇了錯誤的抽象方式. 顯而易見的, 比如數學實際上是一種對現實事物相當高級的抽象, 與此同時, 數學也就成了一個相當良好的解決問題的工具. 而我們編程人員所擔負的責任導致我們的工作, 本質上是一個抽象和構造的過程. 所以如何抽象合理合法, 是我們首先要關注的一個問題. 那麼我們首先要知道的是,什麼是抽象方法?按照數學抽象方法的解釋變化一下,我們可以得到如下一個描述:
抽象方法的軟件設計版本
抽象方法是從考慮的問題出發,通過對各種經驗事實的觀察、分析、綜合和比較,在人們的思維中撇開事物現象的、外部的、偶然的東西,抽出事物本質的、內在的、必然的東西,從空間形式和數量關系上揭示客觀對象的本質和規律,或者在已有軟件設計成果的基礎上,抽出其某一種屬性作為新的軟件設計對象,以此達到表現事物本質和規律的目的的一種軟件設計領域的研究方法。
以上描述,基本是把數學換為“軟件設計”就能得出的結果. 比如在幾何中, “點”的概念是從現實世界中的水點、雨點、起點、終點等具體事物中抽象出來的,它捨棄了事物的各種物理、化學等性質,不考慮其大小、僅僅保留其表示位置的性質。從這裡我們可以看到, 為了研究和解決問題而進行的抽象, 在數學領域中和現實世界中多麼的不同. 但是進行抽象前, 一個潛在的前提被忽略了, 即幾何這個數學工具要解決的問題, 決定了抽象的結果. 很顯然換一個領域解決其它問題, 比如研究雨滴的物理特性, 抽象的結果就完全不同.
由於軟件解決的問題往往是千差萬別的, 這就導致我們抽象的結果, 從一個常規的角度看, 往往不能夠逼近現實世界, 且對相同的東西, 表示的方法很不統一. 甚至突然看去, 和現實世界是那麼的不同. 這也是很多OO狂熱者所誤解的一個想法: 與現實世界偏離太遠的一組模型不是好的模型, 從而忽視了一個人類解決問題而發明的比如數學(或其它科學)這些最高效的工具, 從來和現實世界是不同的. 比如 E = MC2, 它真實的表達了事物的本質, 卻並非是一個直觀上與現實世界相似的表示方法.
我們考慮軟件設計中, 存在某一客觀事物. 往往一個熟知面向對象的設計方法的人員, 就把這一事物抽象為一個對象, 然後抽象出它的屬性(數據)和方法(行為). 在這一過程中, 經常被忽視的一個問題是, 由於軟件設計所面對的客觀情況的復雜性, 我們常常會面臨解決不同的問題, 在解決不同的問題時, 應該使用不同的抽象. 由於在腦海裡沒有清晰的問題域, 在很多人看來, 這(同一事物不同抽象)就硬生生的把一個事物割裂開來(其實可以不同抽象同一實現來重新統一, 但設計的合理仍然依賴於不同抽象的必要性), 不自然, 於是不合理不合法.
比如貧血模型的好與壞, 比如Martin所劃分的"事物腳本", "表模塊", "領域模型", 都是在這樣情況下產生爭論的. 這個在上個關於充血貧血討論的回帖中已經有論述, 我已經把回帖單獨提出來, 有興趣的朋友可以討論. 拿Martin劃分的三種方式來說, 以本文開頭對於抽象的定義, 想必大家可以達成一個共識, 即無論采用那種方式, 都是對事物的一種抽象, 區別只是好壞問題. 看過我的回帖的, 應該有這麼一個印象, 即誰好誰壞, 並不是以符合那種方式就可以分辨的(Martin同學也承認).
在我們的某個項目實現和實施的過程中, 除了對象, 方法, 屬性這些東西, 還有什麼是對事物的抽象? 不知道大家還記不記得數據庫教科書裡面教我們的那些? 沒錯, 就是那些幾NF之類枯燥無味亂七八糟的東西. 我發現這樣一個事實: 討論面向對象軟件設計的各位牛人之中, 很少有關注數據庫設計的. 其實數據庫設計的書現在也是車載斗量; 從深度和實用性講, 數據倉庫是一門更博大精深的學問. 其實咱們大多數討論談到的數據庫, 在數據倉庫大牛, 如Ralph Kimball眼裡, 不過是個源操作環境(名詞可能記的不准確). 而百萬行級上百個表甚至更多的數據, 如果要產生價值, 不使用數據倉庫的種種設計方法, 那麼其實跟廢數據區別不大.Ralph是什麼觀點呢? 數據的設計是如此的重要, 如果沒有良好的數據表現形式, 沃爾瑪的老板永遠不會知道啤酒和尿布之間存在著什麼樣的聯系, 結果基於數據做出的決定讓啤酒大幅的增加了銷量.
某些兄弟提到, 將軟件(不包含數據庫等)設計的結果作為數據庫設計(作為數據的結構設計的一個子集)的前提和約束. 通過這樣的手段, 把數據設計穩定下來, 絕對是一種行之有效的手段,
但它很難變成一種放之四海皆准的設計手段. 這個是自頂而下方法論大師們的一個誤導(大嘴們經常犯這個錯誤): 光拿出一個方法, 卻忘了交代該方法所針對的問題.
請注意數據倉庫領域中"維度建模"這個詞, 不用搞清楚數據倉庫到底是什麼, 也不用搞清楚什麼維度不維度的; 存在這個就足以說明, 建模不只是純粹的軟件結構設計的問題. 至少在Ralph所研究的系統裡, 恐怕數據倉庫的設計要比軟件結構設計重要的多. 道理很簡單: 數據的結構是合理的, 那麼針對數據的操作可以有各種合理的版本, 未來業務變化, 新增不同數據和構築在不同數據之間的處理也很容易; 同時, 請不要忽視一點: 數據不應只是簡單的行的堆積, 數據本身是存在廣泛聯系的, 而且數據本身也要表現出這種聯系; 更要命的是, 數據如果被要求發揮最大的價值, 那麼其表現形式的抽象過程比其它任何東西都需要小心對待.
在我看來, 這個(數據驅動還是對象驅動)問題的關鍵點在於, 用戶到底要的是數據, 還是軟件? 可能大多數人回答是軟件. 其實拿沃爾瑪來說, 他要的是數據, 以及和數據相互作用之後數據. 當然如果沒有軟件, 沃爾瑪的全部員工翻所有數據翻10年也翻不完, 更不論計算了. 但是如果只有軟件, 那麼我只能一攤雙手, 告訴你這兒社麼都沒有. 軟件是人收集數據的簸箕, 觀看數據的窗口, 是計算數據的工具. 讓我們來看看Brooks曾經說過什麼吧:
原文引用更普遍的是, 戰略上突破常來自數據或表的重新表達--這是程序的核心所在. 如果提供了程序流程圖, 而沒有表數據, 我仍然會很迷惑. 而給我看表數據, 往往就不再需要流程圖, 程序結構是非常清晰的.
當然, 是不是真的不需要流程圖, 我看他在這裡也學了Martin大嘴了一把(雖然他年紀更老, 也更大牛一點). 當然也許Brooks的意思是他可以一直研究某種很復雜的表數據長達若干年直到完全搞定, 這樣看我也贊同他的觀點: 有表數據已經無敵了. 重要的是前半句, 如果我們把"程序流程圖"沒有指代但在軟件設計中除了數據設計外其它內容包含進來, 他們全都沒有會怎樣, 數據成了廢數據? 這就是千百萬的$所換來的結果? 那麼讓我們干脆點, 問一個問題, 沒有軟件會怎麼樣?
實際上, 數據庫已經存在千百萬年了, 只是在計算機上實現, 是這幾十年的事. 我們考慮60年代的人口普查表, 和現在的不會有什麼不同. 在這個表上就存在著種種抽象: 一個人不是姓名年齡性別出生日期就能代表的, 實際上表上行, 就是對真實存在的人的抽象. 因為人口普查這個問題域, 需要這種抽象. 往往這種抽象, 還是經歷幾十年甚至上百年反復調整和重構, 才得到的結果. 所以抽象並建模對於人類來說實際上早就開始了. 那麼進行數據驅動式的設計理由是充分的: 我們首先需要的是數據本身的表現形式的正確性. 看看Ralph和Brooks的潛台詞: 數據的表達應該包括足夠的信息. 在數據這一層面, 對於Brooks來說, 良好的抽象能造成軟件整體的突破(對計算機世界); 對於Ralph來說, 良好的抽象能更多的挖掘出數據所隱含的信息(對現實世界).
當然, 這不說明(在一些領域內)對象驅動是不合理的, 但這足夠說明, 數據庫僅僅用來存儲對象的狀態, 而由面向對象的設計來抽象整個世界, 這個情況並不常常發生, 其原因是從某種程度上來說, 至少對於沃爾瑪的老板, 追逐軟件而不是數據本身是捨本逐末的. 經常的, 對於居委會老大媽來說, 數據表現形式的設計就是對世界最好也最急需的抽象. 所以對於是否對象驅動通吃, 就引出了下一個問題(答案往往是層層撥開的) : 面向對象的設計最終產生的存儲對象狀態的數據結構, 是不是在任何領域內都能和直接抽象出來的數據結構一樣好? 抱歉我暫時沒法回答這個問題, 因為這篇文章簡直是走到哪兒, 寫到哪兒, 我還沒真正思考過這個問題. 當然, 如果我是大嘴Martin, 我會直接告訴你不能...
不過沒有答案, 不妨礙不能從其它周邊的角度來進行一下討論:
公元前二百二十一年, 秦始皇統一中國. 如果我們不過分的貶低古人的智慧, 可以考慮這樣一個問題, 如果OOA/OOD真的能夠得到良好的數據結構, 而且比直接數據設計合理, 那麼是不是OOA/OOD中, 最後能決定數據的表現形式的這一部分方法, 早在秦始皇記錄到底誰有幾本書好燒掉它們的時候, 就開始被人研究和發掘了呢? 畢竟薛定谔的代數方程和哥本哈根學派的矩陣力學最後的表達的都是一碼事, 只要有條路通到羅馬, 這條路就應該有人走才對.可歷史上對數據的建模並不存在這樣一個設計步驟, 當然你可以說這個那個OO裡也有, 但這是OO借用人家的; 這是這個考慮的一個方面.
另一個方面, 我們需要看看抽象出來的工具中最璀璨的明珠, 數學. 數學的抽象, 不但實際上和面向對象背道而馳, 而且他們的數據表現形式, 往往和居委會老大媽掰腳趾頭全無區別, 比如那些和自然數列一一對應的問題. 它們對現實世界面向過程的抽象方式, 跟真實世界簡直是驢唇不對馬嘴, 卻更貼切的表達了很多事物的本質.
與面向對象設計相反的, 提煉關系(函數)/元素/集合的面向過程抽象方法, 關系型數據庫本身和基於它的設計和抽象, 卻是從數學, 尤其是集合論等抽象的工具中, 直接衍生出來的. 從這裡,我給出一個個人對軟件設計的認識, 它對一些人來說是顛覆性的, 信不信由你:
面向過程和數據表現形式的設計, 是比面向對象更高層次的抽象, 而且優秀的面向過程和數據設計, 是對解決問題更有效的抽象, 因為它們拋棄了一切可以拋棄的無用負擔.
當然, 能合理駕馭面向過程和關系模型的人, 恐怕腦容量得遠遠高於常人, 以至於Martin這個大嘴說:
原文引用事實上, 我一直堅信面向對象的最大優點在於它能夠使復雜邏輯易於處理.
這很顯然就是在說: Martin的腦子不夠處理某些邏輯, 所以不得不借助面向對象這個拐棍. 當然估摸著腦筋夠用的人很少存在, 於是大多數復雜性夠高, 同時面向過程的項目失敗了. 不過請考慮一下我們在集合論中學過的那些, 函數的定義, 關系的定義, 等等等等, 由集合論的方法來看, 我們解決問題時,要找到的實際上常常是一個集合S, 一個a和一個b, 以及一個連接a和b的關系R.
在這裡需要申明的是我絕非一個DBA跳到程序員堆裡玩深沉, 我從來沒設計過真正需要數據倉庫的系統, 而且我現在的工作與數據庫完全無關. 同時我是一個堅定的面向對象的信徒, 我也絕對不會說, 實用主義的話應該怎麼怎麼樣, 因為我不相信什麼實用主義. 象遺留系統等問題, 是客觀存在的情況, 一個新的系統是可以對由於遺留系統產生的問題進行一次抽象, 讓新系統至少是新的; 上帝的歸上帝, 凱撒的歸凱撒, 本該如此就是最大的實用主義. 但是我要知道我使用的(面向對象這一)工具到底是干嗎地的, 幫我解決了什麼問題, 而不是神話之(過猶不及嘛), 這樣才能最大化這一設計工具的最大價值: 面向對象的抽象方法就是把我玩不轉的形式, 轉化為玩的轉形式的這麼一個工具. 在這裡, 給出第二個我個人的認識:
面向對象僅僅是幫我們前進的一個手段, 他通過增加一些從根本上講毫無意義的細節和表達形式等冗余, 使得我們能更好的組織我們的過程與數據.
接下來說說另外一個抽象過程中經常發生的問題: 忽略了對計算機世界進行抽象和建模的重要性. 而這個問題和如何對現實世界進行建模有著相同的重要性, 很顯然, Int32是一個抽象, String是一個抽象, 問題是當我們編制軟件的時候, 光是這些對象是不夠的(人家Martin Fowler說了, 這叫基本型迷戀), 很多人對這一點認識不深, 導致重視不足. 而對遠離基本型迷戀的原因, 大嘴們給出的解釋又過於浮淺, 將很多與基本型迷戀相似的問題給淹沒了.
我提請大家注意一個非常重要的事實: 在.Net Framework裡, 或者Java的各種框架裡, 實際上存在著非常大量的與現實世界無關的模型(即使一些模型與現實世界存在著相似性). 當我們構建一個軟件或系統的時候, 每個人數的都是自己的代碼行數, 其實要是將操作系統也作為提供的解決方案的一部分(甚至不包括操作系統僅包含用到的基礎框架), 即便上億美金的項目, 恐怕仍然是對非現實世界無關模型的使用占很大部分.
讓我們基於這一事實進行一個想像, 假設.NET Framework是一個十分蹩腳的設計(其實ASP.NET的一些方面就相當蹩腳), 那麼我們基於.NET Framework的項目, 綜合考慮時間/人力等等成本在解決來自Framework的問題, 還有幾個能成功呢? 而現在情況是, 基礎環境或多或少的和我們特定的問題不合拍, 而大多數項目中的設計, 恰恰是根本缺乏對計算機世界的合理建模而直接構築在通用框架或工具箱提供的模型上; 又或者有一個比如各種ORM這樣的通用工具, 拿一個持久化的概念, 就把我們可憐的計算機, 操作系統, 物理存儲的文檔或者數據庫給打發了. 這樣的設計必然會造成, 要麼數據表現形式看上去不合時宜, 要麼面向對象的設計方法看上去很別扭. 在這種情況下, 特別是那種數據驅動的項目, 不能本能的考慮去修改數據庫, 難道你能輕易抱怨.Net對String的實現嗎? 所以在設計伊始, 就要考慮到你這個設計如何對來自計算機世界的現有事物進行抽象, 或者對現有事物與設計中其它部分的關系進行抽象, 才能最大程度的保證設計的完備與統一.
至於面向過程比較容易適應, 恰恰是因為這種抽象的表達方式更本質, 除非問題域變了, 否則 f = ma 做成一個靜態方法, 擺在哪裡永遠不會廢棄. 有新情況? 來個新公式吧. 當然, 面向過程也能做到重用, 多態, 問題是邏輯復雜度高於Martin的7.42, 人腦就到了極限, 正巧這7.42到底是啥還沒個准譜. 所以說, 面向對象總是有益的, 但是如果對什麼是抽象, 怎麼個抽法, 都抽誰, 還存在著疑問, 那麼實際上降低復雜度的同時, 也提高了各種事故出現的概率. 實際上一些基本問題, 現代編程語言已經幫我們處理了(這個以後探討), 但是來自軟件概念性層面上的問題和復雜性, 只有我們自己能解決.
關於面向對象如何幫助我們, 我會在後續話題裡進行討論. 畢竟這篇文章是介紹抽象的, 同時很大篇幅的討論了數據驅動, 對象驅動和面向過程, 面向對象等抽象方式不同的意義. 讓我們回顧一下:
1. 軟件設計領域內, 抽象無處不在, 在面向對象中, 在面向過程中, 在數據表現形式中, 在界面設計中(這個沒有提到). 加一句在本文沒有體現的(也許未來我會加上相關論述, 這個認識相當重要),軟件設計, 歸根結底不應該是對現實世界抽象, 而是對"就某個問題, 使用計算機去更好的解決"這一過程的抽象, 雖然這一過程往往包含對現實世界的抽象, 但多了個大方向, 進行對現實世界的抽象時, 方式方法就不一樣了. 如果不認清這個問題, 無論是面向對象(多這個大方向並不會導致"不夠OO"這種問題, 只會OO的更正確)還是其它什麼方法, 無論你是Anders還是Gosling, 都不可能獲得正確的結果.
2. 好的抽象的唯一標准是是否在正確的問題下良好的運轉, 而並非是在直觀上和現實世界多麼的相似. 而且我們在生活中所能看到的很多最好的抽象, 往往在直觀上與現實世界的事物毫不相似, 卻在問題所在的領域內更接近現實世界事物的本質.
3. 面向過程和數據表現形式的設計, 是比面向對象更高層次的抽象(難道搞出一個汽車類, 不是具體化麼), 而且優秀的面向過程和數據設計, 是對解決問題更有效的抽象, 因為它們拋棄了一切可以拋棄的多余內容. 面向對象的抽象方式僅僅是幫我們前進的一個手段, 他通過為抽象增加一些從根本上講毫無意義的細節和表達方式等冗余, 使得我們能更好的組織我們的過程與數據.
4. 在一個大的問題域內, 如果數據表現形式的設計更根本, 就絕對不存在對象驅動還是數據驅動的問題, 只能數據驅動. 同時我們也要承認, 存在著很多領域, 數據只是操作模型的附屬, 在這樣的情況下, 數據到底怎麼設計, 甚至可以發展到毫無重要性的程度. 與Martin對復雜度的分辨不同, 到底用戶要的是什麼, 這不是一個7.42, 而總會偏向於某一方.
5. 在設計中, 尤其是數據驅動的設計中, 既要考慮對現實世界的抽象, 也要考慮對計算機世界的抽象. 軟件部分面向對象的產物與其它已知結構的沖突(比如與數據庫設計的沖突), 其原因是沒有把計算機世界某一部分(如關系型數據庫及構築於其上的數據表現形式)就軟件所處理的問題域進行再次的合理抽象, 或者太輕易的處理了它們(比如, 忽視了數據本質也是一個接口這個隱含的約束).
最後送給大家Brooks另一句話, 這一句話曾經指導過1000W行以上的單一項目的設計, 同時也指導了軟件設計長達40余年, 並且我想它還會作為一句少有的經典, 繼續存在下去。