冒號和他的學(xué)生們
——程序員提高班紀(jì)事
22.抽象思維
是謂無狀之狀,無物之象,是謂惚恍
——《老子·道經(jīng)》
冒號健步走進(jìn)教室,學(xué)員們立刻正襟危坐,進(jìn)入戰(zhàn)備狀態(tài)。
“如果說咱們是在合演一場戲,那么前面五節(jié)課只是一個過門。”冒號儼然一副自導(dǎo)自演的架勢。
眾人暗暗吃驚:這過門也忒長了點(diǎn)吧。
冒號隨即探問:“還記得在范式總結(jié)中提到的迭代學(xué)習(xí)法吧?”
引號迅速應(yīng)答:“就是在具體知識與抽象理論之間做折返跑。”
“記性不錯。”冒號贊道,“在上本班之前,你們已經(jīng)具備了一定的編程語言的基礎(chǔ),因此我們先從抽象的編程范式談起,此后又回歸到編程語言的討論。”
句號推測:“照此邏輯,下面我們將再次返回編程范式?”
“我們的確要來個For Loop,但相信這是一個增量式的循環(huán)。”冒號用編程語言來強(qiáng)調(diào)他的學(xué)習(xí)理論。
逗號有些失望:“按計(jì)劃不是該對Java語言作專題討論嗎?”
“你放心,Java它跑不了。”冒號看出他的心事,“語言是形,范式是神,這次我們將二者融合,爭取做到形神兼?zhèn)洹>唧w地說,范式以OOP為主,語言以Java為主,同時可能涉及C、C++或C#等語言。另外,示例代碼也會明顯增多。”
逗號臉上的一抹烏云頓時消散開來。
冒號接著提醒道:“不過,秉承開班發(fā)言中的理念,我們的重心不在知識的枝節(jié),而在知識的本源。因此無論討論Java還是OOP,我們不追求系統(tǒng)和全面,但力求從不同的選點(diǎn)、角度和深度來展示知識的活性。”
講到此處,冒號冷不丁提問:“程序員最重要的能力是什么?”
眾人的答案五花八門:學(xué)習(xí)能力、邏輯思維能力、解決問題能力、專注力、溝通能力等等。
“毫無疑問,你們所說的都很重要。這本是個見仁見智的問題,我只是借此展開今后的話題。在我看來,抽象思維能力是最重要的。當(dāng)然,不獨(dú)計(jì)算機(jī)領(lǐng)域,其他科學(xué)同樣需要這種能力。更廣泛地說,抽象是人類認(rèn)識和描繪世界最首要的工具。”不知不覺冒號又走上了形而上的路線。
嘆號這時想起:“前面談切面范式時,似乎專門提到過抽象與分解的重要性。”
“我們也曾提到,不同的范式正是對軟件進(jìn)行了不同角度的抽象和分解。”冒號加以補(bǔ)充,“那么什么是抽象呢?不妨概括為:去粗取精以化繁為簡;由表及里以異中求同。再精煉些,抽象就是作減法和除法。”
問號半信半疑:“減法好理解,通過甄選減去非本質(zhì)和不重要的部分,即去粗取精。可除法呢?”
句號忽然悟道:“透過現(xiàn)象看本質(zhì),發(fā)現(xiàn)不同事物之間的相同之處,即異中求同。同類歸并,那就是除法了。”
冒號進(jìn)一步解釋:“用離散數(shù)學(xué)或抽象代數(shù)的語言來說,通過抽象而產(chǎn)生等價關(guān)系以及相應(yīng)的等價類,便是集合的商運(yùn)算。”
逗號嘀咕:“本來快明白了,經(jīng)這么一描述,重新糊涂了。”
冒號笑道:“如果嫌數(shù)學(xué)語言高深,就用算術(shù)語言吧。乘法可看作同類復(fù)制,作為逆運(yùn)算的除法自然是同類歸并了。”
逗號眼中的迷惘漸漸散去,若有所悟:“嗯,經(jīng)過減法和除法,大數(shù)變小數(shù),復(fù)雜變簡單。”
“能否把抽象說得再具體些?”問號話一出口便自感悖論之嫌:抽象的能具體嗎?
冒號自明其意:“首先,抽象有角度之分。相同的實(shí)體(entity)經(jīng)過不同角度的抽象,得到的模型(model)也會不同。就拿人這個實(shí)體來說,在拓?fù)鋵W(xué)家眼里是三維連通集合,在理論力學(xué)家眼里是質(zhì)點(diǎn),在化學(xué)家眼里是碳水化合物——”
嘆號接嘴:“在情人眼里是西施。”
“過濾缺點(diǎn),抽取優(yōu)點(diǎn),西施就是這樣煉成的。”冒號故意拉長了尾音。
眾人不禁一樂。
冒號繼續(xù)講解:“其次,抽象還有程度之別。抽象程度越高,細(xì)節(jié)越少,普適性越強(qiáng)。典型的例子如:從矩形到多邊形、從多邊形到一般形狀。”
引號問道:“抽象對于軟件設(shè)計(jì)有何現(xiàn)實(shí)意義?”
冒號回答:“軟件設(shè)計(jì)者的任務(wù)是將復(fù)雜混沌的現(xiàn)實(shí)世界映射到精確嚴(yán)格的虛擬世界,要完成這種多對一的映射,抽象無疑是必由之路。在軟件需求分析階段,多通過屬性導(dǎo)向式抽象(property-oriented
abstraction)用邏輯語言來描述系統(tǒng);在軟件設(shè)計(jì)階段,多通過模型導(dǎo)向式抽象(model-oriented
abstraction)用模型語言來設(shè)計(jì)系統(tǒng);在編碼階段,常用兩種抽象機(jī)制:一種是參數(shù)抽象(abstraction
by parameterization),一種是規(guī)范抽象(abstraction
by specification)。”
句號望文生義:“參數(shù)抽象是不是指將函數(shù)代碼中的一些特殊值作為參數(shù)來傳遞?”
“不錯,這是最普通最常用的一種抽象方式。函數(shù)的每一個參數(shù)都是一種泛化,是對它所代表的所有可能值的一種抽象。我們看一個簡單的例子。”冒號說完在黑板上寫下一段代碼——
int gcd(int a, int b)
{
while (a != b)
(a > b) ? (a -= b) : (b -= a);
return a;
}
冒號考問:“請問這個函數(shù)是干什么的?”
眾人看了好一陣,有人猶豫地說,好像是求最大公約數(shù)吧?
冒號肯定道:“它確實(shí)是求最大公約數(shù)的,由兩個正整數(shù)輾轉(zhuǎn)相減而得。該函數(shù)通過參數(shù)抽象讓a和b分別代表任意正整數(shù),使之具有更強(qiáng)的普適性和重用性。問題是除了該代碼的作者,其他人如何重用此函數(shù)?你不能假設(shè)用戶知道gcd是Greatest Common Divisior的縮寫,也不能假設(shè)他的數(shù)學(xué)程度,甚至不能確定他是否能看到源代碼。短短兩行代碼的函數(shù)尚且如此,何況更加復(fù)雜的函數(shù)?”
引號答道:“那只能靠文檔注釋了。”
“非常正確!”冒號頷首,“這是注釋文檔最重要的作用。沒有文檔的API如同沒有說明書的產(chǎn)品,用戶是不敢輕易使用的。合格的文檔注釋中至少應(yīng)包括先驗(yàn)條件(precondition)和后驗(yàn)條件(postcondition),分別指代碼執(zhí)行前后必須滿足的條件。對于函數(shù)gcd而言,先驗(yàn)條件是:a、b均為正整數(shù),后驗(yàn)條件是:返回輸入二數(shù)的最大公約數(shù)。有了文檔注釋或規(guī)范說明(specification)的函數(shù)成為使用者與實(shí)現(xiàn)者之間的一種契約——使用者只能依賴規(guī)范,實(shí)現(xiàn)者必須滿足規(guī)范。這種通過規(guī)范使代碼的功能與實(shí)現(xiàn)相分離的方法便稱為規(guī)范抽象。其好處是顯而易見的:一方面,使用者不必閱讀代碼即可了解并使用它們;另一方面,實(shí)現(xiàn)者不必閱讀或改寫其他代碼,只需在遵循規(guī)范的基礎(chǔ)上修改本地代碼,并且不用擔(dān)心影響客戶代碼。比如我們可以用輾轉(zhuǎn)相除法重新實(shí)現(xiàn)gcd。”
冒號隨手又寫了幾行代碼——
int gcd(int a, int b)
{
return (b != 0) ? gcd(b, a % b) : a;
}
逗號未見其妙:“這兩種抽象機(jī)制在實(shí)際編程中經(jīng)常用到,只是以前不知道它們的學(xué)名罷了。”
冒號正色道:“它們看起來雖然很基本,但平淡之中見真功。如果一個程序員能合理設(shè)計(jì)參數(shù)、嚴(yán)格遵循規(guī)范、有效制定規(guī)范,便已是難得之才了。”
眾人捫心自問,離此要求確有相當(dāng)距離。
冒號續(xù)道:“借助參數(shù)抽象和規(guī)范抽象,我們可以實(shí)現(xiàn)過程抽象(procedural
abstraction)和數(shù)據(jù)抽象(data abstraction)。其中過程抽象容易理解,任何一個函數(shù)都是過程抽象的結(jié)果,它賦予程序員定義新運(yùn)算或子程序的能力,是結(jié)構(gòu)化編程(Structured
programming)和命令式編程(Imperative
programming)的關(guān)鍵。下面我們重點(diǎn)談?wù)剶?shù)據(jù)抽象,它是對象式編程(OOP)的起源。”