OO設(shè)計(jì)的三寶
在講具體的原則之前匣吊,我想先明確一下面向?qū)ο笳Z(yǔ)言的三個(gè)特性儒拂。所有的面向?qū)ο笳Z(yǔ)言都首先必須支持這三個(gè)特性,才能稱之為OO語(yǔ)言色鸳。也只有這三個(gè)特性的支持社痛,才有了后面的各種原則和模式之說(shuō)。所以這是OO設(shè)計(jì)的“三寶”命雀。
首先是封裝蒜哀,封裝的本質(zhì)的是將行為寓于數(shù)據(jù)之中。注意到這是面向?qū)ο笳Z(yǔ)言與面向過(guò)程語(yǔ)言最大的區(qū)別吏砂,不用贅述撵儿。其次是繼承乘客,繼承對(duì)于老的教學(xué)方式,總是強(qiáng)調(diào)拓展屬性淀歇,即具體到更具體寨典。這是欠妥當(dāng)?shù)摹O鄳?yīng)的房匆,我們應(yīng)該更多的認(rèn)為繼承是一種橋梁耸成,把抽象和具體連接起來(lái)。
多態(tài)是三寶中最重要的一個(gè)浴鸿。封裝和繼承都是為了多態(tài)做鋪墊井氢。正因?yàn)橛辛硕鄳B(tài),才有了面向?qū)ο笤O(shè)計(jì)的那些原則和模式岳链,才有可能產(chǎn)生高內(nèi)聚低耦合的軟件系統(tǒng)花竞。所以說(shuō),對(duì)于軟件開(kāi)發(fā)掸哑,多態(tài)就像是普羅米修斯帶給人類的圣火约急。這種評(píng)價(jià)是毫不夸張的,越懂OO的設(shè)計(jì)苗分,越能理解多態(tài)的重要性厌蔽。Java成為OO語(yǔ)言的翹楚,我個(gè)人認(rèn)為與其天然的支持多態(tài)是有一定關(guān)系的摔癣。
我用一個(gè)形象的例子總結(jié)一下OO語(yǔ)言的這三個(gè)特性奴饮。假設(shè)我們有一個(gè)異質(zhì)鏈表,類型為OfficeTool择浊,這個(gè)抽象類對(duì)象代表一種Office工具戴卜。它會(huì)有很多的方法,例如有一個(gè)方法叫g(shù)etYourBestOutput琢岩,意即“返回自己最好的輸出”投剥。(方法寓于對(duì)象之中,這就是封裝担孔。)這個(gè)鏈表中有不同的對(duì)象江锨,它們都是OfficeTool這種對(duì)象的子類,其中三個(gè)就是Word攒磨,Excel和PowerPoint泳桦。(子對(duì)象擁有父對(duì)象的方法,可以以父對(duì)象的名字進(jìn)行引用娩缰,這就是繼承。)如果遍歷這個(gè)異質(zhì)鏈表谒府,訪問(wèn)剛才提到的方法時(shí)拼坎,我們知道浮毯,這三個(gè)工具各有所長(zhǎng),所以Word會(huì)輸出一部精心排版的書稿泰鸡,Excel會(huì)輸出一份內(nèi)容詳實(shí)的財(cái)報(bào)债蓝,而PowerPoint會(huì)輸出一個(gè)制作精美的演示文稿。(不同子類的相同方法盛龄,表現(xiàn)出不同的結(jié)果或輸出饰迹,這就是多態(tài)!)
迪米特法則
迪米特法則有多種表述:
Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.(每一個(gè)軟件單位對(duì)其他的單位都只有最少的知識(shí)余舶,而且局限于那些與本單位密切相關(guān)的軟件單位啊鸭。)
Each unit should only talk to its friends; don't talk to strangers.(每個(gè)軟件單位都只與自己的朋友對(duì)話,不要和陌生人說(shuō)話匿值。)
Only talk to your immediate friends.(只與你最親近的朋友通信赠制。)
作為法律,法則挟憔,它強(qiáng)調(diào)了一種對(duì)軟件系統(tǒng)普世的原則钟些,即“高內(nèi)聚,低耦合”绊谭。盡量減少通信政恍,保持內(nèi)部高度統(tǒng)一。實(shí)際上你去網(wǎng)上搜达传,該法則也不是完美的抚垃,但是它傳遞了一種思想,我認(rèn)為OO設(shè)計(jì)原則的源頭在此趟大。因?yàn)槟阋裱厦滋胤▌t鹤树,你就要考慮整個(gè)軟件系統(tǒng)哪些元素應(yīng)該聚合在一起,能夠產(chǎn)生什么行為才是高內(nèi)聚的逊朽,如何進(jìn)行交互才是低耦合的罕伯。這本身不就是設(shè)計(jì)的過(guò)程么?而且我認(rèn)為如果能考慮這些問(wèn)題叽讳,這還很有可能是一個(gè)優(yōu)秀的設(shè)計(jì)追他。
S.O.L.I.D.原則
Robert Martin有一本非常著名的書,《敏捷軟件開(kāi)發(fā):原則岛蚤、模式與實(shí)踐》邑狸。他在這里提到SOLID是最初的五個(gè)原則,我感覺(jué)這就像是說(shuō)亞當(dāng)和夏娃是最初的2個(gè)人一樣涤妒。其實(shí)還是強(qiáng)調(diào)原則重于模式单雾。我下面會(huì)談?wù)剢我宦氊?zé)和接口隔離,因?yàn)樗鼈冇幸欢ǖ南嗨菩裕踩菀渍莆展瓒选:笕齻€(gè)是OO設(shè)計(jì)的精髓屿储,體現(xiàn)了延遲實(shí)現(xiàn)和針對(duì)接口編程的核心思想。
單一職責(zé)原則
Every context (class, function, variable, etc.) should have a single responsibility, and that responsibility should be entirely encapsulated by the context.(每個(gè)實(shí)體都應(yīng)該只有一種職責(zé)渐逃,且這種職責(zé)被完全的包裹在該實(shí)體內(nèi)够掠。)
這個(gè)原則相對(duì)簡(jiǎn)單,只要你多想想是不是把2個(gè)以上無(wú)關(guān)的事情放到了一個(gè)單位里茄菊,就可以避免過(guò)大而冗余的類疯潭。記住,10000行代碼的類面殖,不是你的榮譽(yù)竖哩,而是你的恥辱。如果10000行的類需要復(fù)用畜普,請(qǐng)問(wèn)有復(fù)用的可能和切實(shí)可行的辦法么期丰?就一個(gè)類而言,應(yīng)該只有一個(gè)引起它變化的原因吃挑。我們經(jīng)常會(huì)遇到User一改需求钝荡,就要改同一個(gè)類,即使需求之間沒(méi)多大關(guān)聯(lián)舶衬,這就說(shuō)明我們違背了單一職責(zé)原則埠通,賦予了一個(gè)類太多的職責(zé)。
接口隔離原則
Once an interface has become too 'fat' it needs to be split into smaller and more specific interfaces so that any clients of the interface will only know about the methods that pertain to them.(一旦一個(gè)接口過(guò)于“臃腫”逛犹,需要把它拆分成更小和更專一的接口端辱,為的是實(shí)現(xiàn)接口的類,只需要知道和自己相關(guān)的方法虽画。)
對(duì)接口的設(shè)計(jì)同樣要遵循迪米特法則舞蔽。一旦一個(gè)接口過(guò)于“臃腫”,需要把它拆分成更小和更專一的接口码撰,為的是實(shí)現(xiàn)接口的類渗柿,只需要知道和自己相關(guān)的方法。最好的例子就是Java中的一些接口定義脖岛。比如Java類庫(kù)中提供的Comparable和Serializable接口朵栖。如果你通過(guò)compare方法給出了實(shí)現(xiàn)Comparable接口的類的兩個(gè)對(duì)象的比較結(jié)果,一個(gè)int值柴梆。你就可以在一些排序的數(shù)據(jù)結(jié)構(gòu)中很好的承載這些對(duì)象陨溅,達(dá)到你比較他們的目的,比TreeTable绍在;Serializable做法更絕门扇,是一個(gè)沒(méi)有方法的接口雹有,相當(dāng)于僅僅是一個(gè)帽子,是一個(gè)標(biāo)記悯嗓,說(shuō)明只要繼承這個(gè)接口的類才能被序列化件舵,否則就拋出異常卸察。
開(kāi)/閉原則
Software entities should be open for extension, but closed for modification.(軟件實(shí)體應(yīng)該只做擴(kuò)展脯厨,而不做修改。)
開(kāi)閉原則是最簡(jiǎn)單的但很難做到的坑质。繼承應(yīng)當(dāng)被看做是封裝變化的方法合武,而不應(yīng)當(dāng)被認(rèn)為是從一般的對(duì)象生成特殊的對(duì)象的方法。這是《Java與模式》那本書作者的原話涡扼。對(duì)于它的解讀是稼跳,完美的繼承是從抽象類到具體類的過(guò)程。即具體類通過(guò)繼承抽象類而封裝了不同的方法(方法接口在抽象類說(shuō)明)吃沪。錯(cuò)誤的繼承是在一般的對(duì)象基礎(chǔ)上汤善,通過(guò)加入特殊的方法,而形成特殊的對(duì)象票彪。
抽象化是開(kāi)/閉原則的關(guān)鍵红淡。在Java、C#等編程語(yǔ)言中降铸,可以為系統(tǒng)定義一個(gè)相對(duì)穩(wěn)定的抽象層在旱,而將不同的實(shí)現(xiàn)行為移至具體的實(shí)現(xiàn)層中完成。這是第一次提出行為的延后實(shí)現(xiàn)推掸,稍后會(huì)看到這個(gè)動(dòng)作的最后落腳點(diǎn)桶蝎。
里氏替換原則
In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).(如果對(duì)每一個(gè)類型為T1的對(duì)象o1,都有類型為T2的對(duì)象o2谅畅,使得以T1定義的所有程序P在所有的對(duì)象o1都代換成o2時(shí)登渣,程序P的行為沒(méi)有變化,那么類型T2是類型T1的子類型毡泻。)
里氏替換原則是實(shí)現(xiàn)開(kāi)/閉原則的重要方式之一胜茧,由于使用基類對(duì)象的地方都可以使用子類對(duì)象,因此在程序中盡量使用基類類型來(lái)對(duì)對(duì)象進(jìn)行定義牙捉,而在運(yùn)行時(shí)再確定其子類類型竹揍,用子類對(duì)象來(lái)替換父類對(duì)象。
依賴反轉(zhuǎn)原則
Abstractions should not depend upon details. Details should depend upon abstractions. Program to an interface, not an implementation.(抽象不應(yīng)該依賴于具體邪铲,具體應(yīng)該依賴于抽象芬位。)
由于有了開(kāi)/閉原則和里氏替換原則的鋪墊,這里提出了最核心的原則带到,針對(duì)接口編程昧碉,延緩細(xì)節(jié)的實(shí)現(xiàn)。如果說(shuō)開(kāi)/閉原則是目標(biāo),里氏替換原則是行為保證被饿,那么按接口編程的依賴反轉(zhuǎn)原則就是有理論保證的四康,有實(shí)際目標(biāo)的,真正的高質(zhì)量OO設(shè)計(jì)狭握。其實(shí)有人已經(jīng)把該原則叫做OO設(shè)計(jì)的標(biāo)志闪金,足見(jiàn)其重要性。
這三個(gè)重要的OO原則和三個(gè)OO語(yǔ)言特性的本質(zhì)關(guān)系是這樣的:開(kāi)/閉原則要求我們盡量在構(gòu)造軟件實(shí)體的時(shí)候论颅,應(yīng)該使用擴(kuò)展哎垦,而不是修改原來(lái)的對(duì)象;那么繼承是一個(gè)很好的方式恃疯,繼承在理想化的使用場(chǎng)景中漏设,應(yīng)該是從抽象到具體(將行為封裝到一個(gè)具體對(duì)象之中),而不是從一種具體到另外一種具體今妄。為什么郑口?因?yàn)槔锸咸鎿Q原則要求行為一致,才是繼承的關(guān)系盾鳞。這和我們理解的加一個(gè)extends就定義了子類和父類的關(guān)系是不同的犬性。由于抽象類沒(méi)有具體行為的實(shí)現(xiàn),所以對(duì)抽象類的繼承雁仲,天然的是符合里氏替換原則的真正的繼承仔夺。而具體到具體的繼承,很難保證里氏替換原則的實(shí)現(xiàn)缸兔。
在滿足開(kāi)/閉原則和里氏替換原則的基礎(chǔ)上吹艇,對(duì)同一個(gè)抽象類的行為,不同的實(shí)現(xiàn)了繼承的具體類表現(xiàn)了不同的行為受神,這就是多態(tài)〔浦回想到前面我們提到的Office異質(zhì)鏈表的例子撑碴,我們的遍歷操作,是針對(duì)抽象類的行為來(lái)進(jìn)行編程的醉拓,這就是針對(duì)“接口/抽象類”編程的意義收苏,這也是編程從依賴具體類(Word愤兵,Excel和PowerPoint)倒轉(zhuǎn)為依賴Office這個(gè)抽象類的過(guò)程。依賴倒轉(zhuǎn)原則的精髓就在于此懦鼠。
再論重構(gòu)
最后我想再絮叨兩句重構(gòu)的話題矫夷。重構(gòu)來(lái)源于那本著名的書憋槐。那些“壞味道”,也隨重構(gòu)的概念被程序員所熟知阳仔。但如果掌握了以上所說(shuō)的OO設(shè)計(jì)的原則并應(yīng)用于設(shè)計(jì)和實(shí)現(xiàn)階段,那么有些“壞味道”根本就不會(huì)發(fā)生嘶摊,那么重構(gòu)也不會(huì)發(fā)生了评矩。我把重構(gòu)分為2種,一種是簡(jiǎn)單的重構(gòu)斥杜,就像修改文章中的改正錯(cuò)別字,或者調(diào)整個(gè)別語(yǔ)句的順序蔗喂;另一種是結(jié)構(gòu)上的重構(gòu),是由于業(yè)務(wù)邏輯的變化或完善畦粮,導(dǎo)致設(shè)計(jì)方案的進(jìn)化(注意我并沒(méi)有說(shuō)完全推翻)乖阵,這時(shí)的重構(gòu)才是最有價(jià)值的。一個(gè)掌握了OO設(shè)計(jì)精髓和原則的程序員瞪浸,應(yīng)該著眼于結(jié)構(gòu)上的重構(gòu),而在正常的編碼中就要注意避免默终,數(shù)百行的函數(shù)犁罩,隨意定義變量和分配內(nèi)存两疚,大量的重復(fù)代碼等問(wèn)題。賈島有時(shí)間在“推”和“敲”上反復(fù)斟酌丐巫,而曹植只有七步的時(shí)間醞釀自己的詩(shī)篇勺美,講究的就是一氣呵成。程序員的能力和效率往往就體現(xiàn)在這些不經(jīng)意的地方赡茸。所以學(xué)無(wú)止境,以此共勉吧遗菠。