面向?qū)ο笤O(shè)計原則之單一職責(zé)
大家都知道面向?qū)ο笫且环N編程思想,而面向?qū)ο笤O(shè)計(OOD)則可以說是每位程序員都琢磨過的。所謂三人行必有我?guī)?,下面看看ohmygodlzl總結(jié)的一些OOD的心得體會,其中著重講述了單一職責(zé)這一原則。
一直想跟同志們探討一下面向?qū)ο笤O(shè)計(OOD)的原則問題,但因為自己理解有限,怕說不好誤人子弟,一直就沒開始?,F(xiàn)在想做個嘗試,從淺處說起,便于理解,也希望能對我們?nèi)粘5拈_發(fā)起到幫助。
我們做軟件開發(fā),要做的事情無非就是:拿到一份需求,通過一系列步驟把它轉(zhuǎn)化為可運行的系統(tǒng)。這些步驟簡單的說就是需求分析――>面向?qū)ο蠓治觯òI(lǐng)域建模)――>架構(gòu)設(shè)計――>詳細(xì)設(shè)計――>編碼――>測試――>發(fā)布這樣的過程,其中架構(gòu)設(shè)計和詳細(xì)設(shè)計中都要用到OOD的一些原則。說起面向?qū)ο?,一般大家都會想到封裝,繼承,多態(tài),這是面向?qū)ο蟮奶卣?,還不是原則,我想說的原則是Robert C. Martin在他的《敏捷軟件開發(fā)-原則模式和實踐》中提到的原則。今天聊聊單一職責(zé)原則(SRP)。
做開發(fā)到現(xiàn)在,相信大家已經(jīng)聽到過這樣的說法:"類的職責(zé)越單一,越容易重用。"這話怎么理解呢?我舉個例子,校驗碼。設(shè)想你現(xiàn)在拿到一個任務(wù)就是實現(xiàn)我們系統(tǒng)中的登錄模塊的校驗碼功能,你怎么做?我覺得我們大部分開發(fā)人員都會做下面這樣的設(shè)計(當(dāng)然更壞的是有人干脆寫一個工具類,提供一個靜態(tài)的generateVerifyCode方法):
這個設(shè)計有什么問題呢?有接口有實現(xiàn),貌似夠合理了。我們拿實際發(fā)生的事來看夠不夠合理――我們系統(tǒng)剛上線的時候校驗碼的實現(xiàn)沒有現(xiàn)在這樣花哨,那個時候只是白色背景加上一組沒有經(jīng)過扭曲處理的數(shù)字;但是后來需求進(jìn)化了,因為原來的校驗碼可能會被破譯,所以需要對數(shù)字進(jìn)行扭曲,而且不能只產(chǎn)生數(shù)字,還要有字母,以增加破譯的難度。這時我們怎么辦?基于上面的設(shè)計,就需要修改VerifyCodeGeneratorImpl類(注意:即使在這種不合理的設(shè)計下,修改實現(xiàn)也是不好的做法,倒不如丟棄這個實現(xiàn),新增一個實現(xiàn)),修改其中產(chǎn)生隨機(jī)數(shù)的代碼,并且在生成圖片的時候?qū)Ξa(chǎn)生的隨機(jī)文本添加扭曲處理邏輯。因為需求的變化而修改代碼, 這說明原來的設(shè)計是不好的,違反了面向?qū)ο笤O(shè)計的另外一個原則"開放封閉原則(OCP)",這個原則另外再談,主要就是說一個類(或者模塊)只可以擴(kuò)展,但不可修改。
細(xì)分析可以看出,導(dǎo)致實現(xiàn)類需要做修改的原因是:它承擔(dān)了兩個本應(yīng)該分離的職責(zé)――產(chǎn)生隨機(jī)文本和生成校驗碼圖片。好,我們嘗試將產(chǎn)生隨機(jī)文本的職責(zé)分離出來,設(shè)計如下:
在這樣的設(shè)計下, 校驗碼隨機(jī)文本的生成職責(zé)被分離成一個單獨的演化體系,隨著需求的變化可以添加產(chǎn)生漢字隨機(jī)文本之類的新實現(xiàn),并且不影響校驗碼的顯示。但這個設(shè)計還沒有滿足需求的變更,因為現(xiàn)在的需求是不光隨機(jī)文本內(nèi)容從純數(shù)字變成了數(shù)字加字母,而且要求顯示的時候?qū)?shù)字進(jìn)行扭曲,在這個設(shè)計中,我們可以添加新的VerifyCodeGenerator實現(xiàn)如TransformedVerifyCodeGeneratorImpl以替換原來的VerifyCodeGeneratorImpl。這樣做可行,但還有更好點的設(shè)計,如下:
這樣做,把圖片的生成職責(zé)也單獨抽象成一個演化體系,這樣以來,將來如果需要在顯示校驗碼時加上背景色或者背景噪音,只需要添加新的ImageGenerator實現(xiàn)。而且ImageGenerator作為一個通用的類,可以被其他有相應(yīng)生成圖片需求的類所重用而不是只局限于生成校驗碼。上面的設(shè)計配合Spring的依賴注入,我們可以生成N * M種校驗碼(N是RandomTextGenerator的實現(xiàn)類數(shù),M是ImageGenerator的實現(xiàn)類數(shù)),而基于最初的設(shè)計,我們就需要創(chuàng)建N*M個VerifyCodeGenerator的實現(xiàn)類,且這些實現(xiàn)類可復(fù)用性很低。
從上面例子的探討中可以得出這么一個結(jié)論:一個類(或者大到模塊,小到方法)承擔(dān)的職責(zé)越多,它被復(fù)用的可能性越小。這就是單一職責(zé)原則(SRP)要表述的內(nèi)容:就一個類而言,應(yīng)該僅有一個引起它變化的原因。
本著這個原則,我們再看一個常見的DAO設(shè)計:
- interface DAO{
- Connection connect();
- void close();
- void executeUpdate();
- ResultSet executeQuery(String sql);
- }
這個接口有什么問題沒有,好像很多人都這么干。只要設(shè)想一下如果底層數(shù)據(jù)庫變化了,connect方法的代碼就可能需要改變(有人說我們不需要改變,那是因為我們使用了Spring提供的DataSource抽象隔離了取得數(shù)據(jù)庫鏈接的變化)。這個接口包含了兩個職責(zé):數(shù)據(jù)庫鏈接管理和數(shù)據(jù)操作。
在實際操作中,如何識別職責(zé)是一個說起來容易做起來難的問題,比如有人可能會說,"產(chǎn)生校驗碼圖片"本身就是一個獨立職責(zé)呀,我說是的,如果我們的校驗碼圖片一成不變,最初的那個設(shè)計不算很壞,只不過喪失了一點重用性而已(比如產(chǎn)生隨機(jī)文本的邏輯可能被其他的模塊重用),但是需求后來變化了,不能只顯示數(shù)字,還要字母,這說明產(chǎn)生隨機(jī)文本是一個變化緯度,將來很可能還有新的變化,那就應(yīng)該把這個職責(zé)獨立出來;需求又說顯示的文本需要扭曲,這說明圖片的生成也是一個變化緯度,沿著這個緯度將來很可能也有新的變化,那就也應(yīng)該把這個職責(zé)獨立出來。需求變化所影響的變化緯度,往往就是應(yīng)該被獨立的職責(zé)。所以如果你接到一個需求后發(fā)現(xiàn)需要修改一個已經(jīng)存在的類,那就要考慮一下是不是原來的設(shè)計不合理,沒有把應(yīng)該獨立出來的職責(zé)分離出來。需求變化結(jié)合經(jīng)驗、常識,就可以慢慢識別職責(zé)應(yīng)該分到什么粒度了。
單一職責(zé)原則不光對類設(shè)計有意義,對以模塊、子系統(tǒng)為單位的架構(gòu)設(shè)計一樣有意義,一個模塊、子系統(tǒng)也應(yīng)該僅有一個引起它變化的原因,不同的是模塊和子系統(tǒng)承擔(dān)的職責(zé)粒度跟類相比是另外一個層次了。
如果讓我列舉一下一個良好的設(shè)計應(yīng)該具備的素質(zhì),我會說:高內(nèi)聚,低耦合。單一職責(zé)原則正是實現(xiàn)高內(nèi)聚低耦合需要遵守的一個原則。
對于面向?qū)ο笤O(shè)計,你有什么新的想法么?
【編輯推薦】