一
前兩天芯杀,有人專程跑到我的文章《類與封裝》留言端考,說數(shù)據結構更加抽象,更加穩(wěn)定揭厚,因而OO
的封裝不make sense
却特。為了證明其觀點,還專門引用了Fred Brooks
在《人月神話》里的敘述:
Show me your flowcharts, and conceal your tables, and I shall continue to be mystified; show me your tables and I won't usually need your flowcharts: they'll be obvious.
-- Fred Brooks, "The Mythical Man Month", chapter 9
這位朋友的觀點其實并不鮮見:我已經見過太多的反OO
碼農筛圆,以此為證據裂明,來支持其“OO
無用,封裝無用太援,對數(shù)據結構的直接依賴更好...”諸如此類的結論闽晦。而那位朋友更是在回復中對眾人呼吁:作為程序員扳碍,不要學什么OO
,SOLID
……
而每次聽到這類宗教戰(zhàn)爭般的言論仙蛉,我都會一邊苦笑笋敞,一邊心里嘀咕:這些人真正負責過復雜一點的系統(tǒng)開發(fā),交付和維護么荠瘪?
二
事實上液样,強調數(shù)據結構比算法更重要的觀點,我還可以舉出更多巧还。
比如Linus Torvalds
在一封郵件里所表達的觀點:
(*) I will, in fact, claim that the difference between a bad programmer
and a good one is whether he considers his code or his data structures
more important. Bad programmers worry about the code. Good programmers
worry about data structures and their relationships.
再比如鞭莽,在《The Art of Unix Programming》里,也表達了類似的觀點:
Rule of Representation: Fold knowledge into data so program logic can be stupid and robust.
Even the simplest procedural logic is hard for humans to verify, but quite complex data structures are fairly easy to model and reason about. To see this, compare the expressiveness and explanatory power of a diagram of (say) a fifty-node pointer tree with a flowchart of a fifty-line program. Or, compare an array initializer expressing a conversion table with an equivalent switch statement. The difference in transparency and clarity is dramatic. See Rob Pike's Rule 5.
Data is more tractable than program logic. It follows that where you see a choice between complexity in data structures and complexity in code, choose the former. More: in evolving a design, you should actively seek ways to shift complexity from code to data.
The Unix community did not originate this insight, but a lot of Unix code displays its influence. The C language's facility at manipulating pointers, in particular, has encouraged the use of dynamically-modified reference structures at all levels of coding from the kernel upward. Simple pointer chases in such structures frequently do duties that implementations in other languages would instead have to embody in more elaborate procedures.
如果愿意麸祷,我還可以舉出更多澎怒。
我從未懷疑過這些觀點的正確性,而這也正是我的觀點阶牍。
三
首先喷面,所有這些被引用的觀點,其實都是在強調:數(shù)據的清晰性走孽。
任何一個有經驗惧辈,有sense
的程序員,都會承認數(shù)據結構的重要性磕瓷。一個良好定義的數(shù)據結構以及它們之間的關系盒齿,往往比算法更清晰。
這是因為困食,一個良好的數(shù)據結構定義所要表達的概念边翁,以及概念之間的關系,是一種高度結構化的信息硕盹。而我們人類的大腦符匾,最善于理解的就是這類信息。相對于不那么結構化的瘩例,代表算法的流程圖啊胶,實體關系圖所需的智商指數(shù)要低的多。
而反過來垛贤,有了這些高度結構化的數(shù)據結構之后焰坪,我們就更容易推理和理解圍繞這些數(shù)據結構的算法。
這也正是我們在分析一個業(yè)務領域南吮,建立其領域模型時琳彩,靜態(tài)視圖:包含類(概念實體)以及類與類之間的關系,對于理解一個領域至關重要的原因。
當然露乏,這一切碧浊,都是建立在一個良好的領域分析基礎上的。作為咨詢師瘟仿,我見過太多團隊箱锐,根本不重視數(shù)據結構的定義,完全不考慮數(shù)據結構所代表的概念劳较,也不考慮數(shù)據的內聚性驹止,只見到系統(tǒng)堆滿了隨機而凌亂的數(shù)據結構,從而讓系統(tǒng)極難理解观蜗。
這也正是為何Linus
強調:糟糕的程序員更關注代碼(算法)臊恋;而優(yōu)秀的程序員更關注數(shù)據結構和它們之間的關系。
數(shù)據結構定義的重要性墓捻,怎么強調都不為過抖仅。
四
數(shù)據結構,相對于算法砖第,不僅更清晰撤卢,在大多數(shù)情況下甚至會更穩(wěn)定。
首先梧兼,我們先看一個簡單的例子放吩。如下代碼定義了一個數(shù)據結構Rectangle
:
struct Rectangle
{
unsigned int height;
unsigned int width;
};
不難發(fā)現(xiàn),這個用來表現(xiàn)矩形的數(shù)據結構羽杰,是非常清晰的渡紫。
而圍繞它的算法,相對于它的數(shù)據忽洛,卻更加不穩(wěn)定腻惠。比如,現(xiàn)在某個需求需要求它的周長欲虚,因而,我們需要提供一個算法:
unsigned int calcPerimeter(Rectangle* rect)
{
return (rect->height + rect->width) * 2;
}
當然悔雹,也可以將算法實現(xiàn)為:
unsigned int calcPerimeter(Rectangle* rect)
{
return rect->height * 2 + rect->width * 2;
}
或者:
unsigned int calcPerimeter(Rectangle* rect)
{
return rect->height + rect->height + rect->width + rect->width;
}
不難看出复哆,對于同一個需求,我們可以基于同一個數(shù)據結構腌零,給出不同的算法實現(xiàn)梯找。因而,在這個例子中益涧,數(shù)據結構比算法更清晰锈锤,也更穩(wěn)定。
但這是否就意味著,封裝對于Rectangle
就沒有意義久免?
五
對于一個軟件系統(tǒng)浅辙,單純的數(shù)據結構是沒有太多意義的(除非它只是一個數(shù)據展現(xiàn)系統(tǒng))。數(shù)據結構和算法阎姥,都是為客戶的根本需要而服務记舆。沒有客戶的需要,則數(shù)據結構和算法呼巴,無論誰更清晰泽腮,更穩(wěn)定,都沒有任何意義衣赶。一個數(shù)據結構該怎么定義诊赊,一個算法該如何設計,這一切都是從客戶的需要出發(fā)府瞄,結合各種約束豪筝,程序員作出的選擇而已。
比如摘能,同樣都是矩形续崖,如果現(xiàn)在我們正在做的是一個畫圖系統(tǒng),則其數(shù)據并不必然使用width
和height
來表示团搞,這時候严望,使用坐標位置,或向量來表示矩形逻恐,會是更合理的選擇像吻。
因而,盡管在不同領域里复隆,有可能都能挖掘出相同的領域概念拨匆,以及相同的領域概念間關系。但其具體數(shù)據(屬性)挽拂,卻會伴隨著不同領域的需求不同而不同惭每。
另外,即便在同一個領域亏栈,對于同樣的業(yè)務需求台腥,當定義數(shù)據結構時,往往也會由于性能绒北,空間黎侈,便利性等非功能性需求和設計約束,而作出不同的決定闷游。比如峻汉,同樣都是1..N
的關系贴汪,我究竟該選擇Array
還是List
?如果選擇List
休吠,改選單向扳埂,還是雙向?對于每個有經驗的C
,C++
開發(fā)者蛛碌,這都是做一個真實系統(tǒng)時經常需要考慮的問題聂喇。
我們已經知道,Linus
極其重視數(shù)據結構的定義蔚携,如果我們去看Linux Kernel
的設計希太,就能知道,其數(shù)據結構的選擇酝蜒,和數(shù)據間的關聯(lián)選擇誊辉,會多大程度上受到非功能因素的影響。否則亡脑,那些數(shù)據結構的定義會更加清晰堕澄,穩(wěn)定和簡單。
但你無論如何選擇霉咨,最終都是為了滿足客戶的業(yè)務需要蛙紫。
六
回到我們的Rectangle
。從需求出發(fā)途戒,我們的系統(tǒng)存在Rectangle
這個概念坑傅,那么客戶需要這個概念的真正原因是什么?是Rectangle
的數(shù)據結構喷斋,還是calcPerimeter
內部的算法唁毒?
答案是:都不是。
客戶真正需要星爪,也真正依賴的是API: unsigned int calcPerimeter(Rectangle* rect)
浆西,而不是Rectangle
的數(shù)據結構,更不是calcPerimeter
的算法實現(xiàn)顽腾。
雖然數(shù)據結構比算法實現(xiàn)更穩(wěn)定近零,但它再穩(wěn)定,相對于API
崔泵,也依然只是一種實現(xiàn)細節(jié)秒赤。
而讓客戶向著更穩(wěn)定的方向依賴(參見《變化驅動:正交設計》),從而依賴API
憎瘸,而不是直接依賴數(shù)據結構,這就是封裝的核心價值陈瘦。
而如果不進行封裝幌甘,客戶擁有訪問數(shù)據,并定義算法的自由,就會讓客戶同時依賴數(shù)據結構和算法锅风。無論你認為數(shù)據結構更不穩(wěn)定酥诽,還是算法更不穩(wěn)定,總之都會讓用戶直接依賴在不穩(wěn)定的事物上皱埠。同時肮帐,在大產品下,極易造成重復边器,這會進一步導致更嚴重的耦合(見《類與封裝》)训枢。
當數(shù)據結構和算法還在爭論誰更抽象,更穩(wěn)定時忘巧,API笑了恒界。
七
而最最重要的部分,在Grady Booch
著名的《面向對象分析與設計》中砚嘴,對OOP
定義的第一個要點則是:
利用對象作為面向對象編程的基本邏輯構建塊十酣,而不是利用算法。
這與把Procedure
看做Building Block
的面向過程范式际长,把Function
看做Building Block
的函數(shù)式范式相比耸采,如果我們認為數(shù)據結構比算法更穩(wěn)定是一個事實,那么毫無疑問工育,面向對象才是更加尊重這個事實的范式虾宇。
因而,從數(shù)據結構比算法更穩(wěn)定出發(fā)翅娶,不僅不應該得到OO無用的結論文留,而應該恰恰相反:OO是在已有的范式中,最符合軟件問題本質的選擇竭沫。