冒號和他的學(xué)生們(連載23)——數(shù)據(jù)抽象
冒號和他的學(xué)生們
——程序員提高班紀(jì)事
23.?dāng)?shù)據(jù)抽象
善張網(wǎng)者引其綱,不一一攝萬目而后得 ——《韓非子·外儲說右下》
問號搶著說:“我知道了:過程抽象的結(jié)果是函數(shù),數(shù)據(jù)抽象的結(jié)果應(yīng)該是數(shù)據(jù)類型。”
冒號首肯:“數(shù)據(jù)類型與數(shù)據(jù)運(yùn)算是程序語言的基本要素,除了內(nèi)建的類型與運(yùn)算外,程序語言還提供了用戶定義(user-defined)的擴(kuò)展機(jī)制,以提高編程者的效率。正如函數(shù)是一些基本運(yùn)算的復(fù)合,自定義類型通常是一些基本類型的復(fù)合。不過單純的復(fù)合類型并不是真正意義上的數(shù)據(jù)抽象,我們關(guān)注的是抽象數(shù)據(jù)類型(ADT)。”
逗號說了句老實(shí)話:“學(xué)數(shù)據(jù)結(jié)構(gòu)時(shí)常提到抽象數(shù)據(jù)類型,但二者究竟什么關(guān)系還真沒搞明白。”
冒號解析道:“作為數(shù)據(jù)與運(yùn)算的有機(jī)集合體,它們可看作同一事物的兩個(gè)方面。數(shù)據(jù)結(jié)構(gòu)強(qiáng)調(diào)具體實(shí)現(xiàn),側(cè)重應(yīng)用;抽象數(shù)據(jù)類型強(qiáng)調(diào)抽象接口,側(cè)重設(shè)計(jì)。比如棧、隊(duì)列、鏈表、二叉樹等作為數(shù)據(jù)結(jié)構(gòu),人們關(guān)心的是如何利用它們有效地組織數(shù)據(jù);而作為抽象數(shù)據(jù)類型,人們更關(guān)心的是類型的設(shè)計(jì)及其背后的數(shù)學(xué)模型。”
引號推想:“既然有抽象數(shù)據(jù)類型,想必就有具體數(shù)據(jù)類型吧?”
“這是當(dāng)然。”冒號肯定道,“具體數(shù)據(jù)類型主要用于數(shù)據(jù)儲存,除了getter和setter之外沒有其他的運(yùn)算。例如由省、市、街道和郵編組成的通訊地址便是一個(gè)典型的具體類型,誰能告訴我定義這種類型的意義?”
句號回答:“定義這種類型可以綁定省、市、街道和郵編這四個(gè)相關(guān)的數(shù)據(jù),便于統(tǒng)一管理,包括創(chuàng)建、復(fù)制、作為參數(shù)傳遞或作為函數(shù)返回值等等。”
“說得不錯(cuò)!”冒號滿意地點(diǎn)點(diǎn)頭,“J2EE中常用一種設(shè)計(jì)模式:數(shù)據(jù)傳輸對象(Data Transfer Objects或DTO),又稱值對象(Value Object或VO),這類對象不含任何業(yè)務(wù)邏輯,僅僅作為簡單的數(shù)據(jù)容器,實(shí)質(zhì)上也屬于具體數(shù)據(jù)類型。”
“究竟這里的‘具體’具體在哪里,‘抽象’又抽象在哪里?”嘆號的眼前飄浮的迷霧也是那么具體而抽象。
冒號輕輕撥開霧靄:“如果一個(gè)數(shù)據(jù)類型依賴于其具體實(shí)現(xiàn),它就是具體的,反之則是抽象的。再拿通訊地址為例,它所有的域即省、市、街道和郵編對于客戶都應(yīng)該是透明的——至于是通過getter、setter還是直接訪問并無本質(zhì)區(qū)別,如此用戶才能有針對性地進(jìn)行數(shù)據(jù)儲存、傳遞和獲取。如果對該類型進(jìn)行修改,比如增加一個(gè)代表國家的域或者減少代表郵編的域,必須知會用戶,否則毫無意義。顯然這種類型與實(shí)現(xiàn)細(xì)節(jié)密切相關(guān),因而是具體的。作為抽象類型的例子,讓我們看看隊(duì)列(Queue)吧。隊(duì)列是一種非常基本的數(shù)據(jù)結(jié)構(gòu),廣泛應(yīng)用于操作系統(tǒng)、網(wǎng)絡(luò)和現(xiàn)實(shí)生活中。請問它的特征是什么?”
引號最擅長這類問題:“隊(duì)列的特征是先進(jìn)先出(FIFO)——每次數(shù)據(jù)只能從隊(duì)尾加入,從隊(duì)首移除。”
“好的。隊(duì)列一般至少包括類似數(shù)據(jù)庫的CRUD(增刪改查)操作:創(chuàng)建操作——建隊(duì);刪除操作——撤隊(duì);修改操作——入隊(duì)、出隊(duì);查詢操作——是否為空隊(duì)、隊(duì)列長度、隊(duì)首。下面我們用C來表述隊(duì)列的操作接口。”冒號投影出一段代碼——
/* QueueType待定。。。 */
typedef QueueType* Queue;
/** 初始化隊(duì)列。成功返回0,否則返回-1。*/
int queue_initialize(Queue);
/** 終結(jié)化隊(duì)列 */
void queue_finalize(Queue);
/** 加入隊(duì)列尾部。成功返回0,否則返回-1。*/
int queue_add(Queue, ItemType);
/** 移除隊(duì)列頭部。成功返回0,否則返回-1。*/
int queue_remove(Queue, ItemType*);
/** 隊(duì)列是否為空?*/
int queue_empty(Queue);
/** 隊(duì)列長度 */
int queue_length(Queue);
/** 返回(但不移除)隊(duì)列頭部。成功返回0,否則返回-1。 */
int queue_head(Queue, ItemType*);
冒號解釋:“特意用C語言是為了表明ADT不獨(dú)OOP專有。由于C不支持異常(exception),因此用非零返回值來表示錯(cuò)誤發(fā)生。我們尚未定義隊(duì)列類型QueueType,其核心是隊(duì)列的成員集合。無論是用數(shù)組來實(shí)現(xiàn),還是用鏈表來實(shí)現(xiàn),用戶根本不需關(guān)心。這便是隊(duì)列的抽象所在——用戶不應(yīng)知道也不必知道它的具體實(shí)現(xiàn),只能通過指定接口來進(jìn)行‘暗箱操作’。這樣經(jīng)過數(shù)據(jù)抽象,隊(duì)列的本質(zhì)特征由API展現(xiàn),非本質(zhì)特征則屏蔽于客戶的視野之外。”
問號問道:“這種數(shù)據(jù)抽象與前面提到的參數(shù)抽象和規(guī)范抽象有何關(guān)系?”
“參數(shù)抽象使得數(shù)據(jù)接口普適化,規(guī)范抽象使得數(shù)據(jù)接口契約化。”冒號的回答簡明扼要,“此外,一個(gè)完整的數(shù)據(jù)抽象除了對每個(gè)接口作規(guī)范說明外,還需對該數(shù)據(jù)類型作整體規(guī)范說明,OOP中的類注釋文檔即作此用。”
逗號要求:“能不能給出完整的實(shí)現(xiàn)代碼?光有接口沒實(shí)現(xiàn),似乎不太過癮。”
冒號戲言:“我感覺你在把程序當(dāng)煙抽——光有煙嘴的接口,沒有香煙的實(shí)現(xiàn),的確不太過癮。”
眾笑。
冒號借題發(fā)揮:“許多程序員都有一個(gè)通病:重實(shí)現(xiàn),輕接口。在編寫代碼時(shí)表現(xiàn)為:不等接口設(shè)計(jì)好就技癢難忍,揎拳捋袖地開始大干;在閱讀代碼時(shí)表現(xiàn)為:看到API文檔便懨懨欲睡,看到代碼便兩眼放光。務(wù)必謹(jǐn)記:接口是綱,實(shí)現(xiàn)是目。綱若不舉,目無以張。也就是常說的:‘Programming to an Interface, not an Implementation’。不過為滿足你們的要求,我還是寫了一段基于循環(huán)數(shù)組的實(shí)現(xiàn)代碼。”
逗號正感當(dāng)靶子的滋味不好受,一見代碼便心旌搖蕩,寵辱皆忘了。
/* 隊(duì)列類型定義*/








/* 文件QueueImpl.c:隊(duì)列的循環(huán)數(shù)組(circular array)實(shí)現(xiàn) */






















































































“由于函數(shù)queue_resize并非公共接口,因此前面加上static,以避免被外部調(diào)用。與Java中的涵義不同,C中static函數(shù)表示文件內(nèi)部函數(shù)。作為對比,我們再看看隊(duì)列的鏈表實(shí)現(xiàn)。”冒號說罷投影出另兩段代碼——
/* 隊(duì)列類型定義*/














/* 文件QueueImpl.c:隊(duì)列的鏈表(linked list)實(shí)現(xiàn) */



































































嘆號發(fā)現(xiàn):“兩種實(shí)現(xiàn)方式看起來迥然不同啊。”
“不同的內(nèi)部數(shù)據(jù)結(jié)構(gòu),導(dǎo)致不同的算法。正是注意到這一點(diǎn),人們多采取‘整體設(shè)計(jì)以數(shù)據(jù)為中心,局部實(shí)現(xiàn)以算法為中心’的方針,以增強(qiáng)系統(tǒng)的可維護(hù)性。最后看看示例客戶代碼。”冒號繼續(xù)放幻燈——
/* 客戶測試代碼 */





















冒號指出:“盡管兩種實(shí)現(xiàn)方式大相徑庭,客戶代碼卻毫無二致。這種數(shù)據(jù)類型的接口與實(shí)現(xiàn)的分離,有利于開發(fā)時(shí)間的分離以及開發(fā)人員的分離。開發(fā)時(shí)間的分離指的是:開發(fā)人員可以推遲在不同實(shí)現(xiàn)方式中作抉擇,以保證整體開發(fā)進(jìn)程;開發(fā)人員的分離指的是:程序的修改和維護(hù)不局限于原作者。”
問號發(fā)現(xiàn)一個(gè)問題:“C語法中沒有private關(guān)鍵詞,用戶仍然有權(quán)訪問和修改隊(duì)列的域成員,整個(gè)代碼邏輯有可能被破壞。”
“沒錯(cuò)。但作為一個(gè)合格的程序員,寫出的代碼不僅要合法,還要合理。”冒號擲地有聲,“合法指合乎語法,合理指合乎語義。既然用到隊(duì)列這個(gè)數(shù)據(jù)結(jié)構(gòu),當(dāng)然要遵循其使用規(guī)范。打個(gè)比方,法律只是維護(hù)社會秩序的最低限度的規(guī)范,一個(gè)只遵守法律而不遵守通用規(guī)范的人必定與社會格格不入。從另一個(gè)角度看,假設(shè)所有程序員都是遵守規(guī)范的,那么類似C這種非OOP語言,只要將數(shù)據(jù)抽象與過程抽象有機(jī)結(jié)合,同樣具有與OOP不相上下的可維護(hù)性和可重用性。”
引號有些困惑:“OOP中的類是否就是ADT?”
冒號釋疑:“可以將類理解為具有繼承和多態(tài)機(jī)制的ADT。但嚴(yán)格說來,并不是所有的類都有抽象性,比如前面提到的僅作存儲用的值對象。在C#中有值類型與引用類型之分,分別用struct和class的關(guān)鍵字來指明。可以把ADT作為選擇原則:是ADT則采用引用類型,否則采用值類型。C++中struct與class在機(jī)制上沒有區(qū)別,只是前者成員缺省為public而后者缺省為private。但習(xí)慣上也是前者作具體類型,后者作抽象類型。Java和C中沒有類似的區(qū)分,一個(gè)只支持class,一個(gè)只支持struct。”
句號沉吟半晌,忽道:“能不能這樣總結(jié)一下抽象數(shù)據(jù)類型?抽象——接口與實(shí)現(xiàn)相分離;數(shù)據(jù)——以數(shù)據(jù)為中心組織邏輯;類型——單純而定義良好的概念。”
“精辟!”冒號贊賞有加,“許多人能將OOP中的封裝、繼承和多態(tài)說得頭頭是道,用得得心應(yīng)手,便自認(rèn)為精通OOP了。殊不知抽象——尤其是數(shù)據(jù)抽象——才是OOP的核心和起源,盡管它們并非OOP的專利。沒有抽象作基礎(chǔ),封裝、繼承和多態(tài)盡皆無本之木。只有貫徹ADT思想,設(shè)計(jì)出來的類才會是‘萬人迷’:有優(yōu)雅的外形——抽象,有豐富的內(nèi)涵——數(shù)據(jù),有鮮明的個(gè)性——類型。”
附:示例源代碼下載(queue.rar)
posted on 2008-07-16 12:32 鄭暉 閱讀(2250) 評論(6) 編輯 收藏 所屬分類: 冒號和他的學(xué)生們