java設(shè)計(jì)模式之面向?qū)ο笤O(shè)計(jì)原則概述(詳解)

作者 薛之謙qxl? 轉(zhuǎn)載請(qǐng)注明出處

我的知乎原文:https://zhuanlan.zhihu.com/p/126674963



內(nèi)容簡(jiǎn)介


對(duì)于面向?qū)ο筌浖到y(tǒng)的設(shè)計(jì)而言,在支持可維護(hù)性的同時(shí),提高系統(tǒng)的可復(fù)用性是一個(gè)至關(guān)重要的問(wèn)題,如何同時(shí)提高一個(gè)軟件系統(tǒng)的可維護(hù)性和可復(fù)用性是面向?qū)ο笤O(shè)計(jì)需要解決的核心問(wèn)題之一。在面向?qū)ο笤O(shè)計(jì)中,可維護(hù)性的復(fù)用是以設(shè)計(jì)原則為基礎(chǔ)的。每一個(gè)原則都蘊(yùn)含一些面向?qū)ο笤O(shè)計(jì)的思想,可以從不同的角度提升一個(gè)軟件結(jié)構(gòu)的設(shè)計(jì)水平。

面向?qū)ο笤O(shè)計(jì)原則為支持可維護(hù)性復(fù)用而誕生,這些原則蘊(yùn)含在很多設(shè)計(jì)模式中,它們是從許多設(shè)計(jì)方案中總結(jié)出的指導(dǎo)性原則。面向?qū)ο笤O(shè)計(jì)原則也是我們用于評(píng)價(jià)一個(gè)設(shè)計(jì)模式的使用效果的重要指標(biāo)之一,在設(shè)計(jì)模式的學(xué)習(xí)中,大家經(jīng)常會(huì)看到諸如“XXX模式符合XXX原則”、“XXX模式違反了XXX原則”這樣的語(yǔ)句。

7種常用的面向?qū)ο笤O(shè)計(jì)原則

面向?qū)ο笤O(shè)計(jì)原則之單一職責(zé)原則:

單一職責(zé)原則是最簡(jiǎn)單的面向?qū)ο笤O(shè)計(jì)原則,它用于控制類(lèi)的粒度大小。單一職責(zé)原則定義如下:

單一職責(zé)原則(Single Responsibility Principle, SRP):一個(gè)類(lèi)只負(fù)責(zé)一個(gè)功能領(lǐng)域中的相應(yīng)職責(zé),或者可以定義為:就一個(gè)類(lèi)而言,應(yīng)該只有一個(gè)引起它變化的原因。

單一職責(zé)原則告訴我們:一個(gè)類(lèi)不能太“累”!在軟件系統(tǒng)中,一個(gè)類(lèi)(大到模塊,小到方法)承擔(dān)的職責(zé)越多,它被復(fù)用的可能性就越小,而且一個(gè)類(lèi)承擔(dān)的職責(zé)過(guò)多,就相當(dāng)于將這些職責(zé)耦合在一起,當(dāng)其中一個(gè)職責(zé)變化時(shí),可能會(huì)影響其他職責(zé)的運(yùn)作,因此要將這些職責(zé)進(jìn)行分離,將不同的職責(zé)封裝在不同的類(lèi)中,即將不同的變化原因封裝在不同的類(lèi)中,如果多個(gè)職責(zé)總是同時(shí)發(fā)生改變則可將它們封裝在同一類(lèi)中。

單一職責(zé)原則是實(shí)現(xiàn)高內(nèi)聚、低耦合的指導(dǎo)方針,它是最簡(jiǎn)單但又最難運(yùn)用的原則,需要設(shè)計(jì)人員發(fā)現(xiàn)類(lèi)的不同職責(zé)并將其分離,而發(fā)現(xiàn)類(lèi)的多重職責(zé)需要設(shè)計(jì)人員具有較強(qiáng)的分析設(shè)計(jì)能力和相關(guān)實(shí)踐經(jīng)驗(yàn)。

下面通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)進(jìn)一步分析單一職責(zé)原則:

Sunny軟件公司開(kāi)發(fā)人員針對(duì)某CRM(Customer Relationship Management,客戶(hù)關(guān)系管理)系統(tǒng)中客戶(hù)信息圖形統(tǒng)計(jì)模塊提出了如圖1所示初始設(shè)計(jì)方案:

圖1 初始設(shè)計(jì)方案結(jié)構(gòu)圖

在圖1中,CustomerDataChart類(lèi)中的方法說(shuō)明如下:getConnection()方法用于連接數(shù)據(jù)庫(kù),findCustomers()用于查詢(xún)所有的客戶(hù)信息,createChart()用于創(chuàng)建圖表,displayChart()用于顯示圖表。

現(xiàn)使用單一職責(zé)原則對(duì)其進(jìn)行重構(gòu)。

在圖1中,CustomerDataChart類(lèi)承擔(dān)了太多的職責(zé),既包含與數(shù)據(jù)庫(kù)相關(guān)的方法,又包含與圖表生成和顯示相關(guān)的方法。如果在其他類(lèi)中也需要連接數(shù)據(jù)庫(kù)或者使用findCustomers()方法查詢(xún)客戶(hù)信息,則難以實(shí)現(xiàn)代碼的重用。無(wú)論是修改數(shù)據(jù)庫(kù)連接方式還是修改圖表顯示方式都需要修改該類(lèi),它不止一個(gè)引起它變化的原因,違背了單一職責(zé)原則。因此需要對(duì)該類(lèi)進(jìn)行拆分,使其滿足單一職責(zé)原則,類(lèi)CustomerDataChart可拆分為如下三個(gè)類(lèi):

(1) DBUtil:負(fù)責(zé)連接數(shù)據(jù)庫(kù),包含數(shù)據(jù)庫(kù)連接方法getConnection();

(2) CustomerDAO:負(fù)責(zé)操作數(shù)據(jù)庫(kù)中的Customer表,包含對(duì)Customer表的增刪改查等方法,如findCustomers();

(3) CustomerDataChart:負(fù)責(zé)圖表的生成和顯示,包含方法createChart()和displayChart()。

使用單一職責(zé)原則重構(gòu)后的結(jié)構(gòu)如圖2所示:

圖2 重構(gòu)后的結(jié)構(gòu)圖

面向?qū)ο笤O(shè)計(jì)原則之開(kāi)閉原則:

開(kāi)閉原則是面向?qū)ο蟮目蓮?fù)用設(shè)計(jì)的第一塊基石,它是最重要的面向?qū)ο笤O(shè)計(jì)原則。開(kāi)閉原則由Bertrand Meyer于1988年提出,其定義如下:

開(kāi)閉原則(Open-Closed Principle, OCP):一個(gè)軟件實(shí)體應(yīng)當(dāng)對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。即軟件實(shí)體應(yīng)盡量在不修改原有代碼的情況下進(jìn)行擴(kuò)展。

在開(kāi)閉原則的定義中,軟件實(shí)體可以指一個(gè)軟件模塊、一個(gè)由多個(gè)類(lèi)組成的局部結(jié)構(gòu)或一個(gè)獨(dú)立的類(lèi)。

任何軟件都需要面臨一個(gè)很重要的問(wèn)題,即它們的需求會(huì)隨時(shí)間的推移而發(fā)生變化。當(dāng)軟件系統(tǒng)需要面對(duì)新的需求時(shí),我們應(yīng)該盡量保證系統(tǒng)的設(shè)計(jì)框架是穩(wěn)定的。如果一個(gè)軟件設(shè)計(jì)符合開(kāi)閉原則,那么可以非常方便地對(duì)系統(tǒng)進(jìn)行擴(kuò)展,而且在擴(kuò)展時(shí)無(wú)須修改現(xiàn)有代碼,使得軟件系統(tǒng)在擁有適應(yīng)性和靈活性的同時(shí)具備較好的穩(wěn)定性和延續(xù)性。隨著軟件規(guī)模越來(lái)越大,軟件壽命越來(lái)越長(zhǎng),軟件維護(hù)成本越來(lái)越高,設(shè)計(jì)滿足開(kāi)閉原則的軟件系統(tǒng)也變得越來(lái)越重要。

為了滿足開(kāi)閉原則,需要對(duì)系統(tǒng)進(jìn)行抽象化設(shè)計(jì),抽象化是開(kāi)閉原則的關(guān)鍵。在Java、C#等編程語(yǔ)言中,可以為系統(tǒng)定義一個(gè)相對(duì)穩(wěn)定的抽象層,而將不同的實(shí)現(xiàn)行為移至具體的實(shí)現(xiàn)層中完成。在很多面向?qū)ο缶幊陶Z(yǔ)言中都提供了接口、抽象類(lèi)等機(jī)制,可以通過(guò)它們定義系統(tǒng)的抽象層,再通過(guò)具體類(lèi)來(lái)進(jìn)行擴(kuò)展。如果需要修改系統(tǒng)的行為,無(wú)須對(duì)抽象層進(jìn)行任何改動(dòng),只需要增加新的具體類(lèi)來(lái)實(shí)現(xiàn)新的業(yè)務(wù)功能即可,實(shí)現(xiàn)在不修改已有代碼的基礎(chǔ)上擴(kuò)展系統(tǒng)的功能,達(dá)到開(kāi)閉原則的要求。

Sunny軟件公司開(kāi)發(fā)的CRM系統(tǒng)可以顯示各種類(lèi)型的圖表,如餅狀圖和柱狀圖等,為了支持多種圖表顯示方式,原始設(shè)計(jì)方案如圖3所示:

圖3 初始設(shè)計(jì)方案結(jié)構(gòu)圖

在ChartDisplay類(lèi)的display()方法中存在如下代碼片段:

在該代碼中,如果需要增加一個(gè)新的圖表類(lèi),如折線圖LineChart,則需要修改ChartDisplay類(lèi)的display()方法的源代碼,增加新的判斷邏輯,違反了開(kāi)閉原則。

現(xiàn)對(duì)該系統(tǒng)進(jìn)行重構(gòu),使之符合開(kāi)閉原則。

在本實(shí)例中,由于在ChartDisplay類(lèi)的display()方法中針對(duì)每一個(gè)圖表類(lèi)編程,因此增加新的圖表類(lèi)不得不修改源代碼。可以通過(guò)抽象化的方式對(duì)系統(tǒng)進(jìn)行重構(gòu),使之增加新的圖表類(lèi)時(shí)無(wú)須修改源代碼,滿足開(kāi)閉原則。具體做法如下:

(1) 增加一個(gè)抽象圖表類(lèi)AbstractChart,將各種具體圖表類(lèi)作為其子類(lèi);

(2) ChartDisplay類(lèi)針對(duì)抽象圖表類(lèi)進(jìn)行編程,由客戶(hù)端來(lái)決定使用哪種具體圖表。

重構(gòu)后結(jié)構(gòu)如圖4所示:

圖4 重構(gòu)后的結(jié)構(gòu)圖

在圖4中,我們引入了抽象圖表類(lèi)AbstractChart,且ChartDisplay針對(duì)抽象圖表類(lèi)進(jìn)行編程,并通過(guò)setChart()方法由客戶(hù)端來(lái)設(shè)置實(shí)例化的具體圖表對(duì)象,在ChartDisplay的display()方法中調(diào)用chart對(duì)象的display()方法顯示圖表。如果需要增加一種新的圖表,如折線圖LineChart,只需要將LineChart也作為AbstractChart的子類(lèi),在客戶(hù)端向ChartDisplay中注入一個(gè)LineChart對(duì)象即可,無(wú)須修改現(xiàn)有類(lèi)庫(kù)的源代碼。

注意:因?yàn)閤ml和properties等格式的配置文件是純文本文件,可以直接通過(guò)VI編輯器或記事本進(jìn)行編輯,且無(wú)須編譯,因此在軟件開(kāi)發(fā)中,一般不把對(duì)配置文件的修改認(rèn)為是對(duì)系統(tǒng)源代碼的修改。如果一個(gè)系統(tǒng)在擴(kuò)展時(shí)只涉及到修改配置文件,而原有的Java代碼或C#代碼沒(méi)有做任何修改,該系統(tǒng)即可認(rèn)為是一個(gè)符合開(kāi)閉原則的系統(tǒng)。

面向?qū)ο笤O(shè)計(jì)原則之里氏代換原則:

里氏代換原則由2008年圖靈獎(jiǎng)得主、美國(guó)第一位計(jì)算機(jī)科學(xué)女博士Barbara Liskov教授和卡內(nèi)基·梅隆大學(xué)Jeannette Wing教授于1994年提出。其嚴(yán)格表述如下:如果對(duì)每一個(gè)類(lèi)型為S的對(duì)象o1,都有類(lèi)型為T(mén)的對(duì)象o2,使得以T定義的所有程序P在所有的對(duì)象o1代換o2時(shí),程序P的行為沒(méi)有變化,那么類(lèi)型S是類(lèi)型T的子類(lèi)型。這個(gè)定義比較拗口且難以理解,因此我們一般使用它的另一個(gè)通俗版定義:

里氏代換原則(Liskov Substitution Principle, LSP):所有引用基類(lèi)(父類(lèi))的地方必須能透明地使用其子類(lèi)的對(duì)象。

里氏代換原則告訴我們,在軟件中將一個(gè)基類(lèi)對(duì)象替換成它的子類(lèi)對(duì)象,程序?qū)⒉粫?huì)產(chǎn)生任何錯(cuò)誤和異常,反過(guò)來(lái)則不成立,如果一個(gè)軟件實(shí)體使用的是一個(gè)子類(lèi)對(duì)象的話,那么它不一定能夠使用基類(lèi)對(duì)象。例如:我喜歡動(dòng)物,那我一定喜歡狗,因?yàn)楣肥莿?dòng)物的子類(lèi);但是我喜歡狗,不能據(jù)此斷定我喜歡動(dòng)物,因?yàn)槲也⒉幌矚g老鼠,雖然它也是動(dòng)物。

例如有兩個(gè)類(lèi),一個(gè)類(lèi)為BaseClass,另一個(gè)是SubClass類(lèi),并且SubClass類(lèi)是BaseClass類(lèi)的子類(lèi),那么一個(gè)方法如果可以接受一個(gè)BaseClass類(lèi)型的基類(lèi)對(duì)象base的話,如:method1(base),那么它必然可以接受一個(gè)BaseClass類(lèi)型的子類(lèi)對(duì)象sub,method1(sub)能夠正常運(yùn)行。反過(guò)來(lái)的代換不成立,如一個(gè)方法method2接受BaseClass類(lèi)型的子類(lèi)對(duì)象sub為參數(shù):method2(sub),那么一般而言不可以有method2(base),除非是重載方法。

里氏代換原則是實(shí)現(xiàn)開(kāi)閉原則的重要方式之一,由于使用基類(lèi)對(duì)象的地方都可以使用子類(lèi)對(duì)象,因此在程序中盡量使用基類(lèi)類(lèi)型來(lái)對(duì)對(duì)象進(jìn)行定義,而在運(yùn)行時(shí)再確定其子類(lèi)類(lèi)型,用子類(lèi)對(duì)象來(lái)替換父類(lèi)對(duì)象。

在使用里氏代換原則時(shí)需要注意如下幾個(gè)問(wèn)題:

(1)子類(lèi)的所有方法必須在父類(lèi)中聲明,或子類(lèi)必須實(shí)現(xiàn)父類(lèi)中聲明的所有方法。根據(jù)里氏代換原則,為了保證系統(tǒng)的擴(kuò)展性,在程序中通常使用父類(lèi)來(lái)進(jìn)行定義,如果一個(gè)方法只存在子類(lèi)中,在父類(lèi)中不提供相應(yīng)的聲明,則無(wú)法在以父類(lèi)定義的對(duì)象中使用該方法。

(2) 我們?cè)谶\(yùn)用里氏代換原則時(shí),盡量把父類(lèi)設(shè)計(jì)為抽象類(lèi)或者接口,讓子類(lèi)繼承父類(lèi)或?qū)崿F(xiàn)父接口,并實(shí)現(xiàn)在父類(lèi)中聲明的方法,運(yùn)行時(shí),子類(lèi)實(shí)例替換父類(lèi)實(shí)例,我們可以很方便地?cái)U(kuò)展系統(tǒng)的功能,同時(shí)無(wú)須修改原有子類(lèi)的代碼,增加新的功能可以通過(guò)增加一個(gè)新的子類(lèi)來(lái)實(shí)現(xiàn)。里氏代換原則是開(kāi)閉原則的具體實(shí)現(xiàn)手段之一。

(3) Java語(yǔ)言中,在編譯階段,Java編譯器會(huì)檢查一個(gè)程序是否符合里氏代換原則,這是一個(gè)與實(shí)現(xiàn)無(wú)關(guān)的、純語(yǔ)法意義上的檢查,但Java編譯器的檢查是有局限的。

在Sunny軟件公司開(kāi)發(fā)的CRM系統(tǒng)中,客戶(hù)(Customer)可以分為VIP客戶(hù)(VIPCustomer)和普通客戶(hù)(CommonCustomer)兩類(lèi),系統(tǒng)需要提供一個(gè)發(fā)送Email的功能,原始設(shè)計(jì)方案如圖5所示:

圖5 原始結(jié)構(gòu)圖

在對(duì)系統(tǒng)進(jìn)行進(jìn)一步分析后發(fā)現(xiàn),無(wú)論是普通客戶(hù)還是VIP客戶(hù),發(fā)送郵件的過(guò)程都是相同的,也就是說(shuō)兩個(gè)send()方法中的代碼重復(fù),而且在本系統(tǒng)中還將增加新類(lèi)型的客戶(hù)。為了讓系統(tǒng)具有更好的擴(kuò)展性,同時(shí)減少代碼重復(fù),使用里氏代換原則對(duì)其進(jìn)行重構(gòu)。

在本實(shí)例中,可以考慮增加一個(gè)新的抽象客戶(hù)類(lèi)Customer,而將CommonCustomer和VIPCustomer類(lèi)作為其子類(lèi),郵件發(fā)送類(lèi)EmailSender類(lèi)針對(duì)抽象客戶(hù)類(lèi)Customer編程,根據(jù)里氏代換原則,能夠接受基類(lèi)對(duì)象的地方必然能夠接受子類(lèi)對(duì)象,因此將EmailSender中的send()方法的參數(shù)類(lèi)型改為Customer,如果需要增加新類(lèi)型的客戶(hù),只需將其作為Customer類(lèi)的子類(lèi)即可。重構(gòu)后的結(jié)構(gòu)如圖6所示:

圖6 重構(gòu)后的結(jié)構(gòu)圖

里氏代換原則是實(shí)現(xiàn)開(kāi)閉原則的重要方式之一。在本實(shí)例中,在傳遞參數(shù)時(shí)使用基類(lèi)對(duì)象,除此以外,在定義成員變量、定義局部變量、確定方法返回類(lèi)型時(shí)都可使用里氏代換原則。針對(duì)基類(lèi)編程,在程序運(yùn)行時(shí)再確定具體子類(lèi)。

擴(kuò)展知識(shí):

里氏代換原則以Barbara Liskov(芭芭拉·利斯科夫)教授的姓氏命名。芭芭拉·利斯科夫:美國(guó)計(jì)算機(jī)科學(xué)家,2008年圖靈獎(jiǎng)得主,2004年約翰·馮諾依曼獎(jiǎng)得主,美國(guó)工程院院士,美國(guó)藝術(shù)與科學(xué)院院士,美國(guó)計(jì)算機(jī)協(xié)會(huì)會(huì)士,麻省理工學(xué)院電子電氣與計(jì)算機(jī)科學(xué)系教授,美國(guó)第一位計(jì)算機(jī)科學(xué)女博士。

面向?qū)ο笤O(shè)計(jì)原則之依賴(lài)倒轉(zhuǎn)原則:

如果說(shuō)開(kāi)閉原則是面向?qū)ο笤O(shè)計(jì)的目標(biāo)的話,那么依賴(lài)倒轉(zhuǎn)原則就是面向?qū)ο笤O(shè)計(jì)的主要實(shí)現(xiàn)機(jī)制之一,它是系統(tǒng)抽象化的具體實(shí)現(xiàn)。依賴(lài)倒轉(zhuǎn)原則是Robert C. Martin在1996年為“C++Reporter”所寫(xiě)的專(zhuān)欄Engineering Notebook的第三篇,后來(lái)加入到他在2002年出版的經(jīng)典著作“Agile Software Development, Principles, Patterns, and Practices”一書(shū)中。依賴(lài)倒轉(zhuǎn)原則定義如下:

依賴(lài)倒轉(zhuǎn)原則(Dependency Inversion Principle, DIP):抽象不應(yīng)該依賴(lài)于細(xì)節(jié),細(xì)節(jié)應(yīng)當(dāng)依賴(lài)于抽象。換言之,要針對(duì)接口編程,而不是針對(duì)實(shí)現(xiàn)編程。

依賴(lài)倒轉(zhuǎn)原則要求我們?cè)诔绦虼a中傳遞參數(shù)時(shí)或在關(guān)聯(lián)關(guān)系中,盡量引用層次高的抽象層類(lèi),即使用接口和抽象類(lèi)進(jìn)行變量類(lèi)型聲明、參數(shù)類(lèi)型聲明、方法返回類(lèi)型聲明,以及數(shù)據(jù)類(lèi)型的轉(zhuǎn)換等,而不要用具體類(lèi)來(lái)做這些事情。為了確保該原則的應(yīng)用,一個(gè)具體類(lèi)應(yīng)當(dāng)只實(shí)現(xiàn)接口或抽象類(lèi)中聲明過(guò)的方法,而不要給出多余的方法,否則將無(wú)法調(diào)用到在子類(lèi)中增加的新方法。

在引入抽象層后,系統(tǒng)將具有很好的靈活性,在程序中盡量使用抽象層進(jìn)行編程,而將具體類(lèi)寫(xiě)在配置文件中,這樣一來(lái),如果系統(tǒng)行為發(fā)生變化,只需要對(duì)抽象層進(jìn)行擴(kuò)展,并修改配置文件,而無(wú)須修改原有系統(tǒng)的源代碼,在不修改的情況下來(lái)擴(kuò)展系統(tǒng)的功能,滿足開(kāi)閉原則的要求。

在實(shí)現(xiàn)依賴(lài)倒轉(zhuǎn)原則時(shí),我們需要針對(duì)抽象層編程,而將具體類(lèi)的對(duì)象通過(guò)依賴(lài)注入(DependencyInjection, DI)的方式注入到其他對(duì)象中,依賴(lài)注入是指當(dāng)一個(gè)對(duì)象要與其他對(duì)象發(fā)生依賴(lài)關(guān)系時(shí),通過(guò)抽象來(lái)注入所依賴(lài)的對(duì)象。常用的注入方式有三種,分別是:構(gòu)造注入,設(shè)值注入(Setter注入)和接口注入。構(gòu)造注入是指通過(guò)構(gòu)造函數(shù)來(lái)傳入具體類(lèi)的對(duì)象,設(shè)值注入是指通過(guò)Setter方法來(lái)傳入具體類(lèi)的對(duì)象,而接口注入是指通過(guò)在接口中聲明的業(yè)務(wù)方法來(lái)傳入具體類(lèi)的對(duì)象。這些方法在定義時(shí)使用的是抽象類(lèi)型,在運(yùn)行時(shí)再傳入具體類(lèi)型的對(duì)象,由子類(lèi)對(duì)象來(lái)覆蓋父類(lèi)對(duì)象。

下面通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)加深對(duì)依賴(lài)倒轉(zhuǎn)原則的理解:

Sunny軟件公司開(kāi)發(fā)人員在開(kāi)發(fā)某CRM系統(tǒng)時(shí)發(fā)現(xiàn):該系統(tǒng)經(jīng)常需要將存儲(chǔ)在TXT或Excel文件中的客戶(hù)信息轉(zhuǎn)存到數(shù)據(jù)庫(kù)中,因此需要進(jìn)行數(shù)據(jù)格式轉(zhuǎn)換。在客戶(hù)數(shù)據(jù)操作類(lèi)中將調(diào)用數(shù)據(jù)格式轉(zhuǎn)換類(lèi)的方法實(shí)現(xiàn)格式轉(zhuǎn)換和數(shù)據(jù)庫(kù)插入操作,初始設(shè)計(jì)方案結(jié)構(gòu)如圖7所示:

圖7 初始設(shè)計(jì)方案結(jié)構(gòu)圖

在編碼實(shí)現(xiàn)圖7所示結(jié)構(gòu)時(shí),Sunny軟件公司開(kāi)發(fā)人員發(fā)現(xiàn)該設(shè)計(jì)方案存在一個(gè)非常嚴(yán)重的問(wèn)題,由于每次轉(zhuǎn)換數(shù)據(jù)時(shí)數(shù)據(jù)來(lái)源不一定相同,因此需要更換數(shù)據(jù)轉(zhuǎn)換類(lèi),如有時(shí)候需要將TXTDataConvertor改為ExcelDataConvertor,此時(shí),需要修改CustomerDAO的源代碼,而且在引入并使用新的數(shù)據(jù)轉(zhuǎn)換類(lèi)時(shí)也不得不修改CustomerDAO的源代碼,系統(tǒng)擴(kuò)展性較差,違反了開(kāi)閉原則,現(xiàn)需要對(duì)該方案進(jìn)行重構(gòu)。

在本實(shí)例中,由于CustomerDAO針對(duì)具體數(shù)據(jù)轉(zhuǎn)換類(lèi)編程,因此在增加新的數(shù)據(jù)轉(zhuǎn)換類(lèi)或者更換數(shù)據(jù)轉(zhuǎn)換類(lèi)時(shí)都不得不修改CustomerDAO的源代碼。我們可以通過(guò)引入抽象數(shù)據(jù)轉(zhuǎn)換類(lèi)解決該問(wèn)題,在引入抽象數(shù)據(jù)轉(zhuǎn)換類(lèi)DataConvertor之后,CustomerDAO針對(duì)抽象類(lèi)DataConvertor編程,而將具體數(shù)據(jù)轉(zhuǎn)換類(lèi)名存儲(chǔ)在配置文件中,符合依賴(lài)倒轉(zhuǎn)原則。根據(jù)里氏代換原則,程序運(yùn)行時(shí),具體數(shù)據(jù)轉(zhuǎn)換類(lèi)對(duì)象將替換DataConvertor類(lèi)型的對(duì)象,程序不會(huì)出現(xiàn)任何問(wèn)題。更換具體數(shù)據(jù)轉(zhuǎn)換類(lèi)時(shí)無(wú)須修改源代碼,只需要修改配置文件;如果需要增加新的具體數(shù)據(jù)轉(zhuǎn)換類(lèi),只要將新增數(shù)據(jù)轉(zhuǎn)換類(lèi)作為DataConvertor的子類(lèi)并修改配置文件即可,原有代碼無(wú)須做任何修改,滿足開(kāi)閉原則。重構(gòu)后的結(jié)構(gòu)如圖8所示:

圖8 重構(gòu)后的結(jié)構(gòu)圖

在上述重構(gòu)過(guò)程中,我們使用了開(kāi)閉原則、里氏代換原則和依賴(lài)倒轉(zhuǎn)原則,在大多數(shù)情況下,這三個(gè)設(shè)計(jì)原則會(huì)同時(shí)出現(xiàn),開(kāi)閉原則是目標(biāo),里氏代換原則是基礎(chǔ),依賴(lài)倒轉(zhuǎn)原則是手段,它們相輔相成,相互補(bǔ)充,目標(biāo)一致,只是分析問(wèn)題時(shí)所站角度不同而已。

面向?qū)ο笤O(shè)計(jì)原則之接口隔離原則:

接口隔離原則定義如下:

接口隔離原則(Interface Segregation Principle, ISP):使用多個(gè)專(zhuān)門(mén)的接口,而不使用單一的總接口,即客戶(hù)端不應(yīng)該依賴(lài)那些它不需要的接口。

根據(jù)接口隔離原則,當(dāng)一個(gè)接口太大時(shí),我們需要將它分割成一些更細(xì)小的接口,使用該接口的客戶(hù)端僅需知道與之相關(guān)的方法即可。每一個(gè)接口應(yīng)該承擔(dān)一種相對(duì)獨(dú)立的角色,不干不該干的事,該干的事都要干。這里的“接口”往往有兩種不同的含義:一種是指一個(gè)類(lèi)型所具有的方法特征的集合,僅僅是一種邏輯上的抽象;另外一種是指某種語(yǔ)言具體的“接口”定義,有嚴(yán)格的定義和結(jié)構(gòu),比如Java語(yǔ)言中的interface。對(duì)于這兩種不同的含義,ISP的表達(dá)方式以及含義都有所不同:

(1) 當(dāng)把“接口”理解成一個(gè)類(lèi)型所提供的所有方法特征的集合的時(shí)候,這就是一種邏輯上的概念,接口的劃分將直接帶來(lái)類(lèi)型的劃分??梢园呀涌诶斫獬山巧?,一個(gè)接口只能代表一個(gè)角色,每個(gè)角色都有它特定的一個(gè)接口,此時(shí),這個(gè)原則可以叫做“角色隔離原則”。

(2) 如果把“接口”理解成狹義的特定語(yǔ)言的接口,那么ISP表達(dá)的意思是指接口僅僅提供客戶(hù)端需要的行為,客戶(hù)端不需要的行為則隱藏起來(lái),應(yīng)當(dāng)為客戶(hù)端提供盡可能小的單獨(dú)的接口,而不要提供大的總接口。在面向?qū)ο缶幊陶Z(yǔ)言中,實(shí)現(xiàn)一個(gè)接口就需要實(shí)現(xiàn)該接口中定義的所有方法,因此大的總接口使用起來(lái)不一定很方便,為了使接口的職責(zé)單一,需要將大接口中的方法根據(jù)其職責(zé)不同分別放在不同的小接口中,以確保每個(gè)接口使用起來(lái)都較為方便,并都承擔(dān)某一單一角色。接口應(yīng)該盡量細(xì)化,同時(shí)接口中的方法應(yīng)該盡量少,每個(gè)接口中只包含一個(gè)客戶(hù)端(如子模塊或業(yè)務(wù)邏輯類(lèi))所需的方法即可,這種機(jī)制也稱(chēng)為“定制服務(wù)”,即為不同的客戶(hù)端提供寬窄不同的接口。

下面通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)加深對(duì)接口隔離原則的理解:

Sunny軟件公司開(kāi)發(fā)人員針對(duì)某CRM系統(tǒng)的客戶(hù)數(shù)據(jù)顯示模塊設(shè)計(jì)了如圖9所示接口,其中方法dataRead()用于從文件中讀取數(shù)據(jù),方法transformToXML()用于將數(shù)據(jù)轉(zhuǎn)換成XML格式,方法createChart()用于創(chuàng)建圖表,方法displayChart()用于顯示圖表,方法createReport()用于創(chuàng)建文字報(bào)表,方法displayReport()用于顯示文字報(bào)表。

圖9 初始設(shè)計(jì)方案結(jié)構(gòu)圖

在實(shí)際使用過(guò)程中發(fā)現(xiàn)該接口很不靈活,例如如果一個(gè)具體的數(shù)據(jù)顯示類(lèi)無(wú)須進(jìn)行數(shù)據(jù)轉(zhuǎn)換(源文件本身就是XML格式),但由于實(shí)現(xiàn)了該接口,將不得不實(shí)現(xiàn)其中聲明的transformToXML()方法(至少需要提供一個(gè)空實(shí)現(xiàn));如果需要?jiǎng)?chuàng)建和顯示圖表,除了需實(shí)現(xiàn)與圖表相關(guān)的方法外,還需要實(shí)現(xiàn)創(chuàng)建和顯示文字報(bào)表的方法,否則程序編譯時(shí)將報(bào)錯(cuò)。

現(xiàn)使用接口隔離原則對(duì)其進(jìn)行重構(gòu)。

在圖9中,由于在接口CustomerDataDisplay中定義了太多方法,即該接口承擔(dān)了太多職責(zé),一方面導(dǎo)致該接口的實(shí)現(xiàn)類(lèi)很龐大,在不同的實(shí)現(xiàn)類(lèi)中都不得不實(shí)現(xiàn)接口中定義的所有方法,靈活性較差,如果出現(xiàn)大量的空方法,將導(dǎo)致系統(tǒng)中產(chǎn)生大量的無(wú)用代碼,影響代碼質(zhì)量;另一方面由于客戶(hù)端針對(duì)大接口編程,將在一定程序上破壞程序的封裝性,客戶(hù)端看到了不應(yīng)該看到的方法,沒(méi)有為客戶(hù)端定制接口。因此需要將該接口按照接口隔離原則和單一職責(zé)原則進(jìn)行重構(gòu),將其中的一些方法封裝在不同的小接口中,確保每一個(gè)接口使用起來(lái)都較為方便,并都承擔(dān)某一單一角色,每個(gè)接口中只包含一個(gè)客戶(hù)端(如模塊或類(lèi))所需的方法即可。

通過(guò)使用接口隔離原則,本實(shí)例重構(gòu)后的結(jié)構(gòu)如圖10所示:

圖10 重構(gòu)后的結(jié)構(gòu)圖

在使用接口隔離原則時(shí),我們需要注意控制接口的粒度,接口不能太小,如果太小會(huì)導(dǎo)致系統(tǒng)中接口泛濫,不利于維護(hù);接口也不能太大,太大的接口將違背接口隔離原則,靈活性較差,使用起來(lái)很不方便。一般而言,接口中僅包含為某一類(lèi)用戶(hù)定制的方法即可,不應(yīng)該強(qiáng)迫客戶(hù)依賴(lài)于那些它們不用的方法。

擴(kuò)展知識(shí):

在《敏捷軟件開(kāi)發(fā)——原則、模式與實(shí)踐》一書(shū)中,RobertC. Martin從解決“接口污染”的角度對(duì)接口隔離原則進(jìn)行了詳細(xì)的介紹,大家可以參閱該書(shū)第12章——接口隔離原則(ISP)進(jìn)行深入的學(xué)習(xí)。

面向?qū)ο笤O(shè)計(jì)原則之合成復(fù)用原則:

合成復(fù)用原則又稱(chēng)為組合/聚合復(fù)用原則(Composition/Aggregate Reuse Principle, CARP),其定義如下:

合成復(fù)用原則(Composite Reuse Principle, CRP):盡量使用對(duì)象組合,而不是繼承來(lái)達(dá)到復(fù)用的目的。

合成復(fù)用原則就是在一個(gè)新的對(duì)象里通過(guò)關(guān)聯(lián)關(guān)系(包括組合關(guān)系和聚合關(guān)系)來(lái)使用一些已有的對(duì)象,使之成為新對(duì)象的一部分;新對(duì)象通過(guò)委派調(diào)用已有對(duì)象的方法達(dá)到復(fù)用功能的目的。簡(jiǎn)言之:復(fù)用時(shí)要盡量使用組合/聚合關(guān)系(關(guān)聯(lián)關(guān)系),少用繼承

在面向?qū)ο笤O(shè)計(jì)中,可以通過(guò)兩種方法在不同的環(huán)境中復(fù)用已有的設(shè)計(jì)和實(shí)現(xiàn),即通過(guò)組合/聚合關(guān)系或通過(guò)繼承,但首先應(yīng)該考慮使用組合/聚合,組合/聚合可以使系統(tǒng)更加靈活,降低類(lèi)與類(lèi)之間的耦合度,一個(gè)類(lèi)的變化對(duì)其他類(lèi)造成的影響相對(duì)較少;其次才考慮繼承,在使用繼承時(shí),需要嚴(yán)格遵循里氏代換原則,有效使用繼承會(huì)有助于對(duì)問(wèn)題的理解,降低復(fù)雜度,而濫用繼承反而會(huì)增加系統(tǒng)構(gòu)建和維護(hù)的難度以及系統(tǒng)的復(fù)雜度,因此需要慎重使用繼承復(fù)用

通過(guò)繼承來(lái)進(jìn)行復(fù)用的主要問(wèn)題在于繼承復(fù)用會(huì)破壞系統(tǒng)的封裝性,因?yàn)槔^承會(huì)將基類(lèi)的實(shí)現(xiàn)細(xì)節(jié)暴露給子類(lèi),由于基類(lèi)的內(nèi)部細(xì)節(jié)通常對(duì)子類(lèi)來(lái)說(shuō)是可見(jiàn)的,所以這種復(fù)用又稱(chēng)“白箱”復(fù)用,如果基類(lèi)發(fā)生改變,那么子類(lèi)的實(shí)現(xiàn)也不得不發(fā)生改變;從基類(lèi)繼承而來(lái)的實(shí)現(xiàn)是靜態(tài)的,不可能在運(yùn)行時(shí)發(fā)生改變,沒(méi)有足夠的靈活性;而且繼承只能在有限的環(huán)境中使用(如類(lèi)沒(méi)有聲明為不能被繼承)。

擴(kuò)展知識(shí):

對(duì)于繼承的深入理解,大家可以參考《軟件架構(gòu)設(shè)計(jì)》一書(shū)作者溫昱先生的文章——《見(jiàn)山只是山見(jiàn)水只是水——提升對(duì)繼承的認(rèn)識(shí)》。

由于組合或聚合關(guān)系可以將已有的對(duì)象(也可稱(chēng)為成員對(duì)象)納入到新對(duì)象中,使之成為新對(duì)象的一部分,因此新對(duì)象可以調(diào)用已有對(duì)象的功能,這樣做可以使得成員對(duì)象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)對(duì)于新對(duì)象不可見(jiàn),所以這種復(fù)用又稱(chēng)為“黑箱”復(fù)用,相對(duì)繼承關(guān)系而言,其耦合度相對(duì)較低,成員對(duì)象的變化對(duì)新對(duì)象的影響不大,可以在新對(duì)象中根據(jù)實(shí)際需要有選擇性地調(diào)用成員對(duì)象的操作;合成復(fù)用可以在運(yùn)行時(shí)動(dòng)態(tài)進(jìn)行,新對(duì)象可以動(dòng)態(tài)地引用與成員對(duì)象類(lèi)型相同的其他對(duì)象。

一般而言,如果兩個(gè)類(lèi)之間是“Has-A”的關(guān)系應(yīng)使用組合或聚合,如果是“Is-A”關(guān)系可使用繼承。"Is-A"是嚴(yán)格的分類(lèi)學(xué)意義上的定義,意思是一個(gè)類(lèi)是另一個(gè)類(lèi)的"一種";而"Has-A"則不同,它表示某一個(gè)角色具有某一項(xiàng)責(zé)任。

下面通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)加深對(duì)合成復(fù)用原則的理解:

Sunny軟件公司開(kāi)發(fā)人員在初期的CRM系統(tǒng)設(shè)計(jì)中,考慮到客戶(hù)數(shù)量不多,系統(tǒng)采用MySQL作為數(shù)據(jù)庫(kù),與數(shù)據(jù)庫(kù)操作有關(guān)的類(lèi)如CustomerDAO類(lèi)等都需要連接數(shù)據(jù)庫(kù),連接數(shù)據(jù)庫(kù)的方法getConnection()封裝在DBUtil類(lèi)中,由于需要重用DBUtil類(lèi)的getConnection()方法,設(shè)計(jì)人員將CustomerDAO作為DBUtil類(lèi)的子類(lèi),初始設(shè)計(jì)方案結(jié)構(gòu)如圖11所示:

圖11 初始設(shè)計(jì)方案結(jié)構(gòu)圖

隨著客戶(hù)數(shù)量的增加,系統(tǒng)決定升級(jí)為Oracle數(shù)據(jù)庫(kù),因此需要增加一個(gè)新的OracleDBUtil類(lèi)來(lái)連接Oracle數(shù)據(jù)庫(kù),由于在初始設(shè)計(jì)方案中CustomerDAO和DBUtil之間是繼承關(guān)系,因此在更換數(shù)據(jù)庫(kù)連接方式時(shí)需要修改CustomerDAO類(lèi)的源代碼,將CustomerDAO作為OracleDBUtil的子類(lèi),這將違反開(kāi)閉原則?!井?dāng)然也可以修改DBUtil類(lèi)的源代碼,同樣會(huì)違反開(kāi)閉原則?!?/p>

現(xiàn)使用合成復(fù)用原則對(duì)其進(jìn)行重構(gòu)。

根據(jù)合成復(fù)用原則,我們?cè)趯?shí)現(xiàn)復(fù)用時(shí)應(yīng)該多用關(guān)聯(lián),少用繼承。因此在本實(shí)例中我們可以使用關(guān)聯(lián)復(fù)用來(lái)取代繼承復(fù)用,重構(gòu)后的結(jié)構(gòu)如圖12所示:

圖12 重構(gòu)后的結(jié)構(gòu)圖

在圖12中,CustomerDAO和DBUtil之間的關(guān)系由繼承關(guān)系變?yōu)殛P(guān)聯(lián)關(guān)系,采用依賴(lài)注入的方式將DBUtil對(duì)象注入到CustomerDAO中,可以使用構(gòu)造注入,也可以使用Setter注入。如果需要對(duì)DBUtil的功能進(jìn)行擴(kuò)展,可以通過(guò)其子類(lèi)來(lái)實(shí)現(xiàn),如通過(guò)子類(lèi)OracleDBUtil來(lái)連接Oracle數(shù)據(jù)庫(kù)。由于CustomerDAO針對(duì)DBUtil編程,根據(jù)里氏代換原則,DBUtil子類(lèi)的對(duì)象可以覆蓋DBUtil對(duì)象,只需在CustomerDAO中注入子類(lèi)對(duì)象即可使用子類(lèi)所擴(kuò)展的方法。例如在CustomerDAO中注入OracleDBUtil對(duì)象,即可實(shí)現(xiàn)Oracle數(shù)據(jù)庫(kù)連接,原有代碼無(wú)須進(jìn)行修改,而且還可以很靈活地增加新的數(shù)據(jù)庫(kù)連接方式。

面向?qū)ο笤O(shè)計(jì)原則之迪米特法則:

迪米特法則來(lái)自于1987年美國(guó)東北大學(xué)(Northeastern University)一個(gè)名為“Demeter”的研究項(xiàng)目。迪米特法則又稱(chēng)為最少知道原則(LeastKnowledge Principle, LKP),其定義如下:

迪米特法則(Law of Demeter, LoD):一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用。

如果一個(gè)系統(tǒng)符合迪米特法則,那么當(dāng)其中某一個(gè)模塊發(fā)生修改時(shí),就會(huì)盡量少地影響其他模塊,擴(kuò)展會(huì)相對(duì)容易,這是對(duì)軟件實(shí)體之間通信的限制,迪米特法則要求限制軟件實(shí)體之間通信的寬度和深度。迪米特法則可降低系統(tǒng)的耦合度,使類(lèi)與類(lèi)之間保持松散的耦合關(guān)系。

迪米特法則還有幾種定義形式,包括:不要和“陌生人”說(shuō)話、只與你的直接朋友通信等,在迪米特法則中,對(duì)于一個(gè)對(duì)象,其朋友包括以下幾類(lèi):

(1) 當(dāng)前對(duì)象本身(this);

(2) 以參數(shù)形式傳入到當(dāng)前對(duì)象方法中的對(duì)象;

(3) 當(dāng)前對(duì)象的成員對(duì)象;

(4) 如果當(dāng)前對(duì)象的成員對(duì)象是一個(gè)集合,那么集合中的元素也都是朋友;

(5) 當(dāng)前對(duì)象所創(chuàng)建的對(duì)象。

任何一個(gè)對(duì)象,如果滿足上面的條件之一,就是當(dāng)前對(duì)象的“朋友”,否則就是“陌生人”。在應(yīng)用迪米特法則時(shí),一個(gè)對(duì)象只能與直接朋友發(fā)生交互,不要與“陌生人”發(fā)生直接交互,這樣做可以降低系統(tǒng)的耦合度,一個(gè)對(duì)象的改變不會(huì)給太多其他對(duì)象帶來(lái)影響。

迪米特法則要求我們?cè)谠O(shè)計(jì)系統(tǒng)時(shí),應(yīng)該盡量減少對(duì)象之間的交互,如果兩個(gè)對(duì)象之間不必彼此直接通信,那么這兩個(gè)對(duì)象就不應(yīng)當(dāng)發(fā)生任何直接的相互作用,如果其中的一個(gè)對(duì)象需要調(diào)用另一個(gè)對(duì)象的某一個(gè)方法的話,可以通過(guò)第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用。簡(jiǎn)言之,就是通過(guò)引入一個(gè)合理的第三者來(lái)降低現(xiàn)有對(duì)象之間的耦合度。

在將迪米特法則運(yùn)用到系統(tǒng)設(shè)計(jì)中時(shí),要注意下面的幾點(diǎn):在類(lèi)的劃分上,應(yīng)當(dāng)盡量創(chuàng)建松耦合的類(lèi),類(lèi)之間的耦合度越低,就越有利于復(fù)用,一個(gè)處在松耦合中的類(lèi)一旦被修改,不會(huì)對(duì)關(guān)聯(lián)的類(lèi)造成太大波及;在類(lèi)的結(jié)構(gòu)設(shè)計(jì)上,每一個(gè)類(lèi)都應(yīng)當(dāng)盡量降低其成員變量和成員函數(shù)的訪問(wèn)權(quán)限;在類(lèi)的設(shè)計(jì)上,只要有可能,一個(gè)類(lèi)型應(yīng)當(dāng)設(shè)計(jì)成不變類(lèi);在對(duì)其他類(lèi)的引用上,一個(gè)對(duì)象對(duì)其他對(duì)象的引用應(yīng)當(dāng)降到最低。

下面通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)加深對(duì)迪米特法則的理解:

Sunny軟件公司所開(kāi)發(fā)CRM系統(tǒng)包含很多業(yè)務(wù)操作窗口,在這些窗口中,某些界面控件之間存在復(fù)雜的交互關(guān)系,一個(gè)控件事件的觸發(fā)將導(dǎo)致多個(gè)其他界面控件產(chǎn)生響應(yīng),例如,當(dāng)一個(gè)按鈕(Button)被單擊時(shí),對(duì)應(yīng)的列表框(List)、組合框(ComboBox)、文本框(TextBox)、文本標(biāo)簽(Label)等都將發(fā)生改變,在初始設(shè)計(jì)方案中,界面控件之間的交互關(guān)系可簡(jiǎn)化為如圖13所示結(jié)構(gòu):

圖13 初始設(shè)計(jì)方案結(jié)構(gòu)圖

在圖13中,由于界面控件之間的交互關(guān)系復(fù)雜,導(dǎo)致在該窗口中增加新的界面控件時(shí)需要修改與之交互的其他控件的源代碼,系統(tǒng)擴(kuò)展性較差,也不便于增加和刪除新控件。

現(xiàn)使用迪米特對(duì)其進(jìn)行重構(gòu)。

在本實(shí)例中,可以通過(guò)引入一個(gè)專(zhuān)門(mén)用于控制界面控件交互的中間類(lèi)(Mediator)來(lái)降低界面控件之間的耦合度。引入中間類(lèi)之后,界面控件之間不再發(fā)生直接引用,而是將請(qǐng)求先轉(zhuǎn)發(fā)給中間類(lèi),再由中間類(lèi)來(lái)完成對(duì)其他控件的調(diào)用。當(dāng)需要增加或刪除新的控件時(shí),只需修改中間類(lèi)即可,無(wú)須修改新增控件或已有控件的源代碼,重構(gòu)后結(jié)構(gòu)如圖14所示:

圖14 重構(gòu)后的結(jié)構(gòu)圖

總結(jié):

單一職責(zé)原則: 一個(gè)類(lèi)只負(fù)責(zé)一個(gè)職責(zé)

開(kāi)閉原則: 開(kāi)放擴(kuò)展,封閉修改

里氏替換原則: 父類(lèi)對(duì)象替換成子類(lèi)對(duì)象能保證功能正常

依賴(lài)倒轉(zhuǎn)原則: 只依賴(lài)于抽象類(lèi)或者接口,不依賴(lài)具體實(shí)現(xiàn)類(lèi)

接口隔離原則: 接口單一職責(zé)原則,接口最小化

組合復(fù)用原則: 少用繼承,多組合復(fù)用

迪米特法則: 不需要直接通信的對(duì)象可以加一層中轉(zhuǎn),降低依賴(lài)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容