摘要:在面向對象設計中,如何抽象出對象。不完善的,想與大家交流的經(jīng)驗。
作者:
cuichaox@gmail.com 軟件分析和設計的階段,開(kāi)發(fā)人員學(xué)習問(wèn)題領(lǐng)域的各種概念,分析系統要完成的功能,思考如何去分割并征服,想象未來(lái)系統的樣子。在整個(gè)過(guò)程中,不斷地學(xué)習,不斷地發(fā)現又不斷地創(chuàng )造。模糊的概念,錯綜的流程和捉摸不定的靈感無(wú)序地出現在大腦,就像飄來(lái)浮去的幻影。有時(shí)候感覺(jué)腦中滿(mǎn)滿(mǎn)的,亂的像一鍋粥;有時(shí)候感覺(jué)腦中空空的,沒(méi)有什么明確的思路,無(wú)從著(zhù)手。
思維是十分復雜的活動(dòng),但仍然有規律。探索開(kāi)發(fā)過(guò)程的思維規律,無(wú)疑是有益的。抽象是人類(lèi)認識復雜世界的基本方法。同樣,其方法廣泛用于軟件分析和設計的過(guò)程中。無(wú)論結構化程序設計,還是在面向對象程序設計。
面向對象的設計,其主要任務(wù)是對象模型的構造。一個(gè)較勁朋友問(wèn)我什么是對象模型,我的答案是:對象和對象之間的關(guān)系組成的系統。在面向對象的技術(shù)中,抽象方法是發(fā)現對象的基本方法,十分重要。一個(gè)巧合:Abstraction 翻譯成中文是“抽象”,就像是“抽取對象”的縮寫(xiě)。
本文的探討,主要應用在面向對象的技術(shù)中。當然基本原理都是相通的:甚至,你編寫(xiě)SHELL腳本的時(shí)候,你也能使用面向對象的基本原理來(lái)提高你產(chǎn)品的質(zhì)量。
什么是抽象
抽象在軟件方法中有著(zhù)特殊的含義?“抽象”這個(gè)概念有點(diǎn)抽象。
現實(shí)世界的東西所以能夠被分類(lèi),是因為同一類(lèi)的東西表現出相似性,根據你的思考角度,在一個(gè)較高的層次上,考慮你關(guān)注的共性,忽略不同個(gè)體的個(gè)性。在這個(gè)角度上思考的時(shí)候,你就可以忽略其個(gè)體差異造成的復雜。這樣,你就得到了一個(gè)東西的抽象。在本文中,“抽象”這個(gè)詞有兩個(gè)含義:一個(gè)代表“思維方法”;一個(gè)代表“抽象出的概念”
抽象時(shí)除了關(guān)注東西的本質(zhì)屬性,還要考慮觀(guān)察問(wèn)題的角度。在一個(gè)人事管理系統中的人的抽象和在CS游戲中人的抽象,肯定具有不同的行為和屬性。
雖然說(shuō)是“發(fā)現”共性。有時(shí)更像是“發(fā)明”。比如:C語(yǔ)言的printf函數之所以能夠打印所有基本類(lèi)型的變量,是因為設計者創(chuàng )造了格式符的概念。抓住了的所有基本類(lèi)型的變量有共性:可以根據指定的格式打印,但設計者必須發(fā)明“格式符”的概念來(lái)體現他。這里我使用這個(gè)例子,也是說(shuō)明:抽象不光用在面向對象的領(lǐng)域。打印字符串、打印整數和打印小數,本來(lái)是多種行為,但作者發(fā)現(更像發(fā)明)其共性,才設計了這個(gè)更加通用的函數。而不是設計一堆函數:printString, printInt,PrintFloat等。
抽象、分類(lèi)法和層次
這是三個(gè)密切不可分的概念。在已有的抽象上再次抽象,就出現的分類(lèi)的層次。面向對象的概念中,繼承體現了層次。對象之間的父子關(guān)系,體現了父類(lèi)型和子類(lèi)型的關(guān)系。在對象建模的過(guò)程中,我常常使用分類(lèi)法來(lái)激發(fā)靈感。比如思考:我要處理的數據一共有幾類(lèi),同類(lèi)的數據是否可以統一處理?
對象來(lái)自哪里
對象可能來(lái)自問(wèn)題領(lǐng)域也可能來(lái)自實(shí)現領(lǐng)域。有下面的幾種情況:
1. 有的對象直接代表用戶(hù)需求描述或功能描述中的概念,這類(lèi)一般是真實(shí)世界的實(shí)體:如汽車(chē),拖拉機。
2. 有的對象直接是功能分割的一個(gè)單元,習慣結構化編程的人,特喜歡設計這種對象,如文件分析器,數據處理器。這種方法很容易誤導,要小心使用。構造幾個(gè)互相協(xié)作,責任明確的對象完成功能。一般使用角色和責任分配的說(shuō)法,不使用功能分配的方法。細想起來(lái),模塊化設計和面向對象設計的本質(zhì)都是“分割并且征服”,有些情況下許多東西也是相通的。
3. 有些對象直接對應計算機上硬件或系統的資源。如內存池,日志文件,線(xiàn)程對象。實(shí)現這些類(lèi)的時(shí)候,注意設計合理的接口,這種類(lèi)一旦實(shí)現,今后可以反復使用。
4. 用來(lái)實(shí)現其他類(lèi)的工具對象。如:C++的各種容器,String等,這些也是可以被反復重用的對象實(shí)現。
面向對象的應用。有三個(gè)最為成功的領(lǐng)域:模擬現實(shí)領(lǐng)域、計算機圖形領(lǐng)域和圖形界面開(kāi)發(fā)庫。是因為這三個(gè)領(lǐng)域,很容易抽象出來(lái)對象。而大多情況下,抽象對象并不是一個(gè)容易的工作。從這個(gè)角度看:一些傳統的開(kāi)發(fā)人員,特別是UNIX程序員反對面向對象技術(shù),也是有其道理的。
避免過(guò)度的抽象
對象模型本質(zhì)上也是分割并且征服。分割并且征服,是對付復雜度的基本方法。但有的時(shí)候過(guò)度分割反而導致復雜性。Linux之父Linus說(shuō):把一個(gè)復雜的東西,分割成兩部分,每一個(gè)部分是簡(jiǎn)單了,但多出了要處理兩個(gè)部分關(guān)系的復雜性。在對象建模的過(guò)程中,要避免過(guò)度的抽象。
比如:“狗和羊,成為不同類(lèi)的對象也許是合理的,但花狗和黑狗沒(méi)有必要使用不同的類(lèi)定義,只需要給狗這個(gè)類(lèi)型定義一個(gè)顏色屬性就可以了”。
在實(shí)踐中,我發(fā)現設計模式的不當使用,常常帶來(lái)過(guò)度抽象。我曾經(jīng)建議一個(gè)新手:減少flag功能的全局變量的使用。后來(lái)這個(gè)認真的人,把每一個(gè)flag變化都放到了一個(gè)singleton對象中封裝起來(lái),這樣看起來(lái)是沒(méi)有全局變量了,卻增加了一堆新的類(lèi)。哦,天哪!避免全局變量,是要避免使用這個(gè)全局變量的模塊之間的緊耦合,現在全局變量變成singleton對象了,并且因為一個(gè)類(lèi)帶來(lái)的接口要比一個(gè)全局變量復雜,實(shí)際上是耦合的更緊了。又有一個(gè)同志,對工廠(chǎng)類(lèi)模擬虛擬構造的原理研究的爐火純青,設計中,幾乎要把所有的對象都使用工廠(chǎng)來(lái)構造,給我的感覺(jué):一個(gè)闊少有很多工廠(chǎng)來(lái)給自己造內褲,不同的分廠(chǎng)生產(chǎn)不同樣式的內褲――有錢(qián)燒的。關(guān)于設計模式不當使用的情況,我今后將有一個(gè)文章詳細探討。
請記住著(zhù)名的KISS原則:Keep It Simple, Stupid!你能找到很多理由把簡(jiǎn)單問(wèn)題搞復雜,但往往能找到更好的理由把復雜問(wèn)題搞簡(jiǎn)單。抽象是為了處理復雜性,不要越抽象越復雜。
封裝驅動(dòng)抽象
封裝是面向對象的基本特征。我的一個(gè)經(jīng)驗,就是借助封裝的思路來(lái)抽象對象。每當碰到復雜并且容易變化的邏輯,我就想:是不是設計一個(gè)對象把它封裝起來(lái)。我把這種思考設計的方法稱(chēng)為:封裝驅動(dòng)抽象。
在軟件系統中,可以被封裝的東西很多。最常見(jiàn)的是一個(gè)實(shí)體的狀態(tài)和行為可以封裝,抽象出一個(gè)對象。整個(gè)系統的某個(gè)相對獨立的特性和行為也可以被封裝,如:一個(gè)程序中,許多模塊都要記錄日志,就可以把日志抽象成一個(gè)對象,提供日志記錄的接口。某種容易變化的業(yè)務(wù)邏輯,也可以被封裝。一個(gè)復雜的算法,也可以被封裝。
被封裝和抽象的東西,一般是:直接的概念、可以分離的特性、相對獨立的知識或是容易變化的因素。每當看到這種東西,我都下意識的想到抽象。
抽象不一定成為對象
前面也說(shuō)過(guò),抽象不是面向對象技術(shù)中獨有的方法。甚至在面向對象的設計中,抽象也不一定成為對象。使用對象封裝就是:把知識和邏輯封裝到對象中,對象的使用者就可以變得愚蠢和簡(jiǎn)單;如果設計通用的數據結構或靈活的配置文件,甚至可以把知識和邏輯封裝到數據或配置中,這樣你的程序就可以變得愚蠢和簡(jiǎn)單,所謂簡(jiǎn)單是可靠之母。我就喜歡傻壯的程序。我的另一個(gè)文章《被忽視的方法mini-language》專(zhuān)門(mén)對這種方法進(jìn)行了介紹。