面向對象軟件開(kāi)發(fā)和過(guò)程(四)重用 ![]() |
![]() | 級別: 初級 林星, 項目經(jīng)理 2003 年 12 月 01 日 重用是面向對象開(kāi)發(fā)中的一個(gè)非常重要的特性,由于重用的特點(diǎn),它能夠降低開(kāi)發(fā)投入,并提高軟件的質(zhì)量。那么,在面向對象開(kāi)發(fā)中,究竟該如何掌握重用呢?又該如何將重用應用到開(kāi)發(fā)過(guò)程中呢? 上一章中所討論的分析框架是一種清晰的分析方法,但是接下來(lái)的內容中我們卻不能夠完全使用這個(gè)框架。一來(lái)內容較多,二來(lái)分析框架需要結合實(shí)際情況才有意義。所以在下面的討論中,我們仍然會(huì )按照框架的思路來(lái)處理問(wèn)題,但并不嚴格的描述所有的框架要素。 我們把面向對象中的問(wèn)題分為以下一些主題,每個(gè)主題都體現出了面向對象技術(shù)的價(jià)值,我們會(huì )針對每一個(gè)主題進(jìn)行框架分析,在后續的篇幅中,我們準備討論四個(gè)主題:
對于我們來(lái)說(shuō),最重要的是要了解面向對象技術(shù)對一個(gè)軟件開(kāi)發(fā)過(guò)程有何幫助,所以,我們不是像一般的面向對象的教科書(shū)那樣從繼承、多態(tài)開(kāi)始學(xué)習。我們的精力應該放在過(guò)程和設計上。 這四個(gè)主題涉及了大量的面向對象相關(guān)知識,它們都能夠大幅度的提高軟件的質(zhì)量,同時(shí)降低人員之間的溝通成本: 重用和優(yōu)化代碼組織有很多的關(guān)聯(lián),例如,代碼組織的優(yōu)化能夠幫助重用的實(shí)現,而重用的思路能夠引導代碼組織的方向。 針對契約設計為嚴謹的軟件設計提供了一種可行的操作思路。例如,針對契約設計就有利于業(yè)務(wù)模型的質(zhì)量。 業(yè)務(wù)建模是OOAD方法的核心,業(yè)務(wù)模型定義了軟件的設計原型,它的好壞直接影響到軟件的質(zhì)量。 后續文章我將逐步分析這四個(gè)方面。 在面向對象中,重用是一種基本的思路。XP方法的最佳實(shí)踐-重構的一個(gè)重要目標,就是使同樣功能的代碼只出現一次。這就是典型的實(shí)現重用,這種做法有兩個(gè)好處,一是帶來(lái)可重用的代碼,二是減少維護的成本。 繼承和泛型(或是模板)是兩種很基本的重用技術(shù)。為了能夠清楚的描述問(wèn)題,首先我們要弄懂類(lèi)型的概念: 1.1 類(lèi)型(Type)
在計算機看來(lái),任何數據(甚至質(zhì)量)都是一些bit流,但是要人類(lèi)看懂這些bit流那就太為難了。因此類(lèi)型的目的就是為了能夠對bit流進(jìn)行分類(lèi),告訴我們這部分的字節流表示數字,這部分的字節流表示字符串。因此,就像上面那段話(huà)中所說(shuō)的,在面向數值的編程中,類(lèi)型通常用作數據的表示。在Java這樣的強類(lèi)型語(yǔ)言中,在編譯期,每一個(gè)變量和表達式都有一個(gè)類(lèi)型與之相對應??赡苡行┤擞X(jué)得不方便(例如VB程序員),但是這種機制能夠有效的避免錯誤。所以我本人比較喜歡強類(lèi)型的語(yǔ)言,雖然有時(shí)候它并不是很方便,但是它能夠使我避免許多錯誤。如果你開(kāi)發(fā)一個(gè)企業(yè)應用,強類(lèi)型的語(yǔ)言雖然速度上可能稍遜于弱類(lèi)型語(yǔ)言,但是強類(lèi)型語(yǔ)言帶來(lái)的嚴謹性是更重要的。 在面向對象中,類(lèi)型除了用于表述值的含義之外,還包括了在這個(gè)值上的各種操作。所以呢,那句話(huà)中又說(shuō),在面向對象的世界中,類(lèi)型更重要的是引出行為。在現實(shí)世界中,類(lèi)型總是伴隨著(zhù)一定的特性的,所以?xún)H靠數值類(lèi)型是無(wú)法描述多姿多彩的現實(shí)世界的。 在OO中,類(lèi)可以是一個(gè)類(lèi)型,在一些純OO語(yǔ)言中,例如Eiffel、SmallTalk,類(lèi)代表了全部,但是在一些為了向前兼容的OO語(yǔ)言中,類(lèi)類(lèi)型和非類(lèi)的類(lèi)型是區分開(kāi)來(lái)的,典型的代表是Java和C#這兩種語(yǔ)言。在Java中,包括引用類(lèi)型(類(lèi)類(lèi)型)和原生類(lèi)型兩大類(lèi)類(lèi)型,原生類(lèi)型包括布爾值、數字值。在C#中,類(lèi)型也分為值類(lèi)型和引用類(lèi)型,為了統一的表示兩者,還引入了Boxing和UnBoxing的操作。由于篇幅所限,我們不可能在這個(gè)話(huà)題上花費太多的時(shí)間,如果要深入了解的話(huà),可以參考Java的虛擬機規范。 在引入泛型機制之后,類(lèi)可以接受各種各樣的類(lèi)型,那類(lèi)是否還能夠表示一個(gè)類(lèi)型呢。這個(gè)問(wèn)題等到我們討論泛型機制的時(shí)候再談。 1.2 繼承 面向對象系統中最主要的元素就是類(lèi)型,因此面向對象中的抽象的主要形式是類(lèi)型抽象,也就是使用類(lèi)型來(lái)表示現實(shí)生活中的各種事物的形式。 一般我們認為繼承可以分為兩種基本的形式:實(shí)現繼承和接口繼承。實(shí)現繼承的主要目標是代碼重用,我們發(fā)現類(lèi)B和類(lèi)C存在同樣的代碼,因此我們設計了一個(gè)類(lèi)A,用于存放通用的代碼,基于這種思路的繼承稱(chēng)為實(shí)現繼承。但接口繼承不同,它是基于現實(shí)生活中的語(yǔ)義的,表現了IsA的關(guān)系。例如,我們認為存款帳戶(hù)和結算帳戶(hù)都是帳戶(hù)的子類(lèi),這種繼承我們稱(chēng)之為接口繼承。注意,有些文章中一個(gè)類(lèi)實(shí)現一個(gè)接口的行為定義為接口繼承,這和我們討論的接口繼承是不同的概念,為了區分兩種概念,我們可以使用接口繼承的另一種稱(chēng)呼-類(lèi)型繼承。繼承的關(guān)鍵就在于如何靈活的運用兩種繼承方式。 實(shí)現繼承是實(shí)現代碼復用的關(guān)鍵技法,雖然接口繼承也能夠提供一定的代碼復用,但是效果不如前者。但是,這決不意味著(zhù)接口繼承不如實(shí)現繼承。相反,我認為接口繼承能夠提供更優(yōu)秀的重用性(這一點(diǎn)我們在下一篇中就能夠看到),因為接口繼承更為抽象,能夠適用更大的范圍。接口就是典型的例子,由于它什么代碼都沒(méi)有,什么都不做,所以接口的抽象性是最好的。這時(shí)候我突然想到"言多必失"這句話(huà),軟件設計是不是很有一些哲學(xué)的味道?但事實(shí)上,我們是兩種繼承方法同時(shí)使用,以達到最佳的效果。 繼承能夠獲得比委托(委托將會(huì )在下文提到)更大的靈活性,但是這種靈活性并不是沒(méi)有代價(jià)的。在一個(gè)軟件團隊中,繼承的靈活性可能會(huì )帶來(lái)代碼的混亂或失控。Effective Java的條款14給我們的建議是組合(也就是我們指的委托)要優(yōu)先于繼承,一個(gè)重要的理由是繼承破壞了封裝性,因為子類(lèi)需要了解父類(lèi)的相關(guān)信息。 我們做軟件設計的思路有兩種,如果我們希望客戶(hù)端的調用簡(jiǎn)單一些,那么服務(wù)類(lèi)就比較復雜,反之,如果希望服務(wù)類(lèi)簡(jiǎn)單一些,那么客戶(hù)端的調用就會(huì )復雜一些。繼承的語(yǔ)言也面臨類(lèi)似的問(wèn)題。Java、C++都傾向于將繼承的語(yǔ)法簡(jiǎn)單化,由編譯器來(lái)負責繼承語(yǔ)法的分析。但是這種做法會(huì )出現一些潛在的問(wèn)題。例如,脆弱基類(lèi)(fragile base class)的問(wèn)題。這個(gè)問(wèn)題說(shuō)的是當在基類(lèi)中增加新的功能時(shí),可能會(huì )對現存的類(lèi)產(chǎn)生影響,當基類(lèi)中增加一個(gè)虛方法時(shí),而子類(lèi)也存在一個(gè)同名的虛方法,那么子類(lèi)中的同名方法就會(huì )替換新加入的方法。這種做法是合法的,因此編譯器是不會(huì )報錯的,但這可能造成一些潛在的問(wèn)題。出現這樣的問(wèn)題是我們在書(shū)寫(xiě)繼承語(yǔ)法時(shí)的自動(dòng)化程度比較高,我們不需要對各個(gè)方法進(jìn)行顯示的指定,而是按照既定的規則進(jìn)行。而Eiffel語(yǔ)言的做法則不同,Eiffel語(yǔ)言為繼承定義了各種各樣的語(yǔ)法。在C#語(yǔ)言中,也有類(lèi)似作用的關(guān)鍵字。 繼承是面向對象中非常重要的技巧,而繼承樹(shù)的設計也特別的重要,在一些單根繼承的語(yǔ)言中更是如此,因為父類(lèi)只有一個(gè),不能夠輕易的使用繼承。一般來(lái)說(shuō),繼承樹(shù)在一個(gè)設計決策中占有非常重要的位置,是需要重點(diǎn)考慮的。這里是設計的重點(diǎn)。 繼承本身并不是什么特別難的技巧,但是要能夠把繼承運用的好卻是很難的。在一個(gè)軟件開(kāi)發(fā)團隊中,不同的成員有著(zhù)不同的背景,不同的知識,對繼承、面向對象也有不同的看法,如何協(xié)調這么多的要素,以保證設計的一致性呢?面向對象的老手和新手的最大區別,往往就表現在繼承的處理上。在軟件過(guò)程中,我比較提倡由架構師或老鳥(niǎo)級的程序員來(lái)負責主要的繼承層次的設計。這樣,軟件的結構不容易亂,千萬(wàn)不能夠象以前的非面向對象代碼那樣做一刀切,把不同模塊交給不同人了事,這樣最后代碼一定失控。 繼承的設計往往是比較難以進(jìn)行測試的。有其是那些為了擴展的目的設計的類(lèi),因為父類(lèi)中的代碼往往只是最終功能的一部分。這里有一個(gè)測試的思路,如果在你的代碼中還必須實(shí)現子類(lèi),那么針對子類(lèi)進(jìn)行測試。如果你的代碼中不實(shí)現子類(lèi),那么請設計一個(gè)測試子類(lèi),并對測試子類(lèi)進(jìn)行測試。 1.3 泛型 如果說(shuō)繼承實(shí)現了子類(lèi)型的多態(tài)的話(huà),那么泛型則是實(shí)現了參數的多態(tài),兩者都是抽象機制的重要組成部分,兩者能夠在一定程度上相互替代,因此一種觀(guān)點(diǎn)認為泛型是能夠在很大程度上替代繼承,在C++的標準模板庫中,泛型就被大量的使用。但泛型的思路和繼承的思路仍有差別,繼承往往代表了不同的算法,但泛型往往代表了相同的算法,不同的類(lèi)型。這也是為什么STL中大量使用泛型的原因,因為算法是類(lèi)似的。泛型對我們最大的好處是引入了靜態(tài)(編譯期)類(lèi)型檢查?;叵胛覀僇ava語(yǔ)言中的很多容器都是用于容納Object類(lèi)型的,其目的是為了讓容器更加的通用,但是導致了一個(gè)問(wèn)題,編譯器不會(huì )檢查你放入容器的到底是一個(gè)什么東西,比如我們把人放到存放貨物的容器中,雖然違反了現實(shí)世界中的規則,但編譯器仍然放行。
多么惡劣的行為,容器中裝的是貨物,可出來(lái)的卻是人,有販賣(mài)人口的嫌疑吧,但對編譯器來(lái)說(shuō)完全合法,而在運行期間會(huì )出現問(wèn)題,因為類(lèi)型轉換是非法的。這種方法能夠得到大量運用的關(guān)鍵技術(shù)幾乎所有的對象都是繼承自一個(gè)根對象(例如Object)。這在一定程度上實(shí)現了泛型,但這種行為不值得提倡。 這時(shí)候,類(lèi)型的不安全性對軟件質(zhì)量造成了影響。使用泛型就不會(huì )出現這樣的尷尬局面,編譯器會(huì )幫助你檢查類(lèi)型,保證類(lèi)型安全。泛型的另一個(gè)好處是減少了類(lèi)型轉換。從某個(gè)角度上來(lái)說(shuō),類(lèi)型轉換是一種非安全的編程方法,在現實(shí)生活中很少看到這種現象(基因突變),但是在編程語(yǔ)言中,我們大量使用這種技巧,更多時(shí)候是被迫的。 Java中的泛型機制 在名為"猛虎"的J2SE1.5中,Java引入了新的泛型機制(Java在1.4版本中就引入了泛型機制)。Java是否該引入泛型機制一直都是爭論的焦點(diǎn),畢竟,泛型機制是一種非常優(yōu)秀的抽象方法。引入了泛型機制的Java語(yǔ)言看起來(lái)像是這樣的:
從Java核心團隊的兩名成員-Joshua Bloch和Neal Gafter的談話(huà)(http://developer.java.sun.com/developer/community/chat/JavaLive/2003/jl0729.html)來(lái)看,Tiger只是對泛型機制做了一些編譯器上的處理,例如編譯器幫助你檢查類(lèi)型安全,從集合返回值時(shí)無(wú)需對類(lèi)型進(jìn)行轉化。不管如何,對程序員來(lái)講,這就足夠了,不是嗎? 泛型解決了我們的一大問(wèn)題,我們可以定義更加嚴格、自然的類(lèi)型處理機制。而不是把類(lèi)型轉換來(lái)轉換去。在軟件開(kāi)發(fā)過(guò)程中,盡可能引入泛型機制來(lái)解決現實(shí)中的問(wèn)題。在業(yè)務(wù)建模中,利用泛型機制來(lái)描述需要,能夠更加準確的解決問(wèn)題。例如,當我們對書(shū)店中貨架建模的時(shí)候,我們規定書(shū)架上只能夠存放書(shū)和CD之類(lèi)的產(chǎn)品。因此,我們使用下面的方法:
以上的代碼是使用Eiffel語(yǔ)言編寫(xiě),SHELF類(lèi)只能夠存放SHELF_ITEM類(lèi)型的物品。因此其子類(lèi)BOOK、CD都是合法的,但是其它的物品,例如生肉,那就是非法的。試想如果我們仍然使用類(lèi)型轉換的方式的話(huà),那么我們的SHELF又變成了百寶箱了,除了能夠存放書(shū)和CD,還能夠存放鋼琴、演員、核武器。哈!真是太牛了。 現代的語(yǔ)言都將引入或將要引入泛型機制,看來(lái)泛型機制成為通用技術(shù)的日子已經(jīng)不遠了。 繼承和泛型是兩種方向上的重用技術(shù),繼承提供的是類(lèi)型的縱向擴展,而泛型提供的是類(lèi)型的橫向抽象。兩種技術(shù)都能夠提供優(yōu)秀的重用性。而對于我們來(lái)說(shuō),關(guān)鍵的問(wèn)題仍然在于,如何在開(kāi)發(fā)過(guò)程中引入這些技術(shù)。
為重用定義規范是一件困難的事情。很難定義一個(gè)規范,要求開(kāi)發(fā)人員必須按照某種方式來(lái)設計繼承樹(shù)或泛型類(lèi)。但是,繼承和泛型保持統一的風(fēng)格是較好的處理方式。所以,合適的做法是采用指南和范例的形式。
不論是繼承還是泛型,都要求有豐富的面向對象的編碼和設計經(jīng)驗。重用的目標對設計師和程序員的要求很高,如果組織中的人員沒(méi)有能夠達到這種要求,那么就需要考慮在人員技能上進(jìn)行強化。 對于泛型來(lái)說(shuō),學(xué)習泛型機制意味著(zhù)原先處理集合的思路發(fā)生了變化,需要在一個(gè)新的抽象高度上理解集合操作。
重用涉及到設計的問(wèn)題。所以對于組織來(lái)說(shuō),問(wèn)題在于,誰(shuí)負責設計。正如我們在技能這一節中看到的,重用對技術(shù)的要求很高,我們無(wú)法要求組織的任何一個(gè)人都達到這種要求,所以,較好的方式是,把重用技術(shù)的職責交給合適的設計人員,由他們負責對軟件的整體結構進(jìn)行重用上的設計。 重用的最終目標是能夠在軟件組織范圍內建立起一個(gè)框架,軟件組織能夠利用這個(gè)框架降低開(kāi)發(fā)成本,提高開(kāi)發(fā)質(zhì)量。
我們之前討論繼承時(shí),曾經(jīng)提到,由架構師或老鳥(niǎo)級的程序員來(lái)負責主要的繼承層次的設計。重用最能夠發(fā)揮效用的地方是在設計的時(shí)候考慮重用。所以這就需要過(guò)程上的保證了,在設計活動(dòng)中,如果需要使用到繼承或是泛型技術(shù),那么就需要有相應的設計、測試方案、復審、文檔化的過(guò)程,并確保設計思路能夠順利的形成代碼。形成代碼的思路有兩種,一種是由程序員根據設計編寫(xiě)代碼,另一種是設計人員自己編寫(xiě)實(shí)現代碼。我比較傾向于第二種思路。第一種思路會(huì )產(chǎn)生很多額外的溝通成本,造成成本的浪費,影響代碼的質(zhì)量。
Java語(yǔ)言能夠優(yōu)雅的支持繼承和泛型,大部分的面向對象語(yǔ)言都能夠支持繼承,在未來(lái),泛型也將成為標準的技術(shù)。 C++中將泛型實(shí)現為模板的方式,在軟件開(kāi)發(fā)中,模板的運用是一個(gè)非常巧妙的方法。在企業(yè)應用程序中,數據庫的CRUD方法是非常常用的,但是不同表、不同目的的CRUD方法都有少許的不同。如果使用繼承或委托等方式來(lái)進(jìn)行代碼級別的重用,會(huì )造成代碼的意圖不清晰,令人難以理解。所以,這些方式難以得到好的效果,但是我們同時(shí)又應該看到,不同的代碼塊之間的重復程度也是很高的。所以,我們想到能不能象泛型機制那樣,對代碼本身進(jìn)行抽象呢?例如,一個(gè)標準的Create方法中,方法的流程基本相同,但是使用的值對象、表名、連接等都有所不同,我們把這些看作參數,將嵌入參數的代碼制作為模板,并提供這些參數的使用說(shuō)明和范例。在使用的時(shí)候,只要用具體的值替換相應的參數就可以了。很多的Case工具中都支持模板。這種方法要比你寫(xiě)大量的指導性文檔要好的多,原因在于它的操作性很強,程序員很容易接受。 |
聯(lián)系客服