前言
最近學(xué)習(xí)設(shè)計(jì)模式,看著設(shè)計(jì)模式的例子很經(jīng)典,至少自己覺(jué)得大部分人都可以理解,在這里分享一下單一職責(zé)原則簡(jiǎn)稱是 SRP,就是三個(gè)開(kāi)通字母的縮寫(xiě),這個(gè)設(shè)計(jì)原則備受爭(zhēng)議的一個(gè)原則,只要你想和 人爭(zhēng)執(zhí)、慪氣或者是吵架,這個(gè)原則是屢試不爽的,如果你是老大,看到一個(gè)接口或類是這樣…那樣…設(shè) 計(jì)的,你就問(wèn)一句“你設(shè)計(jì)的類符合 SRP 原則嗎?”,保準(zhǔn)立馬萎縮掉,而且還一臉崇拜的看著你“老大確 實(shí)英明”,這個(gè)原則爭(zhēng)論在什么地方呢?就是職責(zé)的定義,什么是類的職責(zé),怎么劃分類的職責(zé)。我們先講 個(gè)例子來(lái)說(shuō)明什么是單一職責(zé)原則。 只要做過(guò)項(xiàng)目,肯定要接觸到用戶、機(jī)構(gòu)、角色管理這個(gè)模塊,基本使用都是 RBAC 這個(gè)模型,確實(shí)是 很好的一個(gè)解決辦法,我們今天要講的是用戶管理,管理用戶的信息,增加機(jī)構(gòu)(一個(gè)人屬于多個(gè)機(jī)構(gòu)), 增加角色等,用戶有這么的信息和行為要維護(hù),我們就把這些寫(xiě)到一個(gè)接口中,都是用戶管理類嘛,我們 先來(lái)看類圖:

太 easy 的類圖了,我相信即使一個(gè)初級(jí)的程序員也可以看出這個(gè)接口設(shè)計(jì)的有問(wèn)題,用戶的屬性 (PRoperties)和用戶的行為(Behavior)沒(méi)有分開(kāi),這是一個(gè)嚴(yán)重的錯(cuò)誤!非常正確,確實(shí)是這個(gè)接口 設(shè)計(jì)的一團(tuán)糟,應(yīng)該把用戶的信息抽取成一個(gè)業(yè)務(wù)對(duì)象(Bussiness Object,簡(jiǎn)稱 BO),把行為抽取成另外一 個(gè)接口中,我們把這個(gè)類圖重新畫(huà)一下:

負(fù)責(zé)用戶的屬性 負(fù)責(zé)用戶的行為 重新拆封成兩個(gè)接口,IUserBO 負(fù)責(zé)用戶的屬性,簡(jiǎn)單的說(shuō)就是 IUserBO 的職責(zé)是收集和反饋用戶的屬 性信息;IUserBiz 負(fù)責(zé)用戶的行為,完成用戶的信息的維護(hù)和變更。各位可能要問(wèn)了,這個(gè)和我實(shí)際的工 作中用到的 User 類還是有差別的呀,別著急,我們先來(lái)想一想分拆成兩個(gè)接口怎么使用,想清楚了,我們 看是面向的接口編程,所有呀你產(chǎn)生了這個(gè) UserInfo 對(duì)象之后,你當(dāng)然可以把它當(dāng) IUserBO 接口使用,當(dāng) 然也可以當(dāng) IUserBiz 接口使用,這要看你在怎么地方使用了,你要獲得用戶想信息,你就當(dāng)時(shí) IUserBO 的 實(shí)現(xiàn)類;你要是想維護(hù)用戶的信息,就當(dāng)是 IUserBiz 的實(shí)現(xiàn)類就成了,類似于如下代碼:
.......IUserBiz userInfo = new UserInfo();//我要賦值了,我就認(rèn)為它是一個(gè)純粹的BOIUserBO userBO = (IUserBO)userInfo;userBO.setPassWord("abc");//我要執(zhí)行動(dòng)作了,我就認(rèn)為是一個(gè)業(yè)務(wù)邏輯類IUserBiz userBiz = (IUserBiz)userInfo;userBiz.deleteUser();.......確實(shí)可以如此,問(wèn)題也解決掉了,但是我們來(lái)回想一下我們剛剛的動(dòng)作,為什么要把一個(gè)接口拆分成 兩個(gè)?其實(shí)在實(shí)際的使用中,我們更傾向于使用兩個(gè)不同的類或接口,一個(gè)就是 IUserBO, 一個(gè)是 IUserBiz, 類圖應(yīng)該如如下圖:

以上我們把一個(gè)接口拆分成兩個(gè)接口的動(dòng)作,就是依賴了單一職責(zé)原則,那什么是單一職責(zé)原則呢? 單一職責(zé)原則:應(yīng)該有且僅有一個(gè)原因引起類的變更
我解釋到這里估計(jì)你現(xiàn)在很不屑了,“切!這么簡(jiǎn)單的東西還要講?!弱智!”,好,我們來(lái)講點(diǎn)復(fù)雜的。 SRP 的原話解釋是:There should never be more than one reason for a class to change。這句話初中 生都能看懂,不多說(shuō),但是看懂是一回事,實(shí)施就是另外一回事了,上面我說(shuō)的例子很好理解,大家已經(jīng) 都是這么做了,那我再舉個(gè)例子來(lái)看看是否好理解。舉個(gè)電話的例子,電話通話的時(shí)候有四過(guò)程發(fā)生:撥 號(hào)、通話、回應(yīng)、掛機(jī),那我們寫(xiě)一個(gè)接口應(yīng)該如下這個(gè)類圖:
我不是有意要冒犯 iphone 的,同名純屬巧合,我們來(lái)看一個(gè)接口程序:
實(shí)現(xiàn)類我就不再寫(xiě)了,大家看看這個(gè)接口有沒(méi)有問(wèn)題?我相信大部分的讀者都會(huì)說(shuō)這個(gè)沒(méi)有問(wèn)題呀, 以前我就是這么做的呀,XXX 這本書(shū)上也是這么寫(xiě)的呀,還有什么什么的源碼也是這么寫(xiě)的,是的,這個(gè)接 口接近于完美,看清楚了是“接近于”。單一職責(zé)要求一個(gè)接口或類只有一個(gè)原因引起變化,也就是一個(gè)接 口或類只有一個(gè)職責(zé),它就負(fù)責(zé)一件事情,看看上面的接口只負(fù)責(zé)一件事情嗎?是只有一個(gè)原因引起變化 嗎?好像不是! IPhone 這個(gè)接口可不是只有一個(gè)職責(zé),它是由兩個(gè)職責(zé):一個(gè)是協(xié)議管理,一個(gè)是數(shù)據(jù)傳送,diag() 和 huangup()兩個(gè)方法實(shí)現(xiàn)的是協(xié)議管理,撥號(hào)接通和關(guān)閉;chat()和 answer()是數(shù)據(jù)的傳送,把我們說(shuō) 的話轉(zhuǎn)換成模擬信號(hào)或者是數(shù)字信號(hào)傳遞到對(duì)方,然后再把對(duì)方傳遞過(guò)來(lái)的信號(hào)還原成我們聽(tīng)的懂人話。 我們可以這樣考慮這個(gè)問(wèn)題,協(xié)議接通的變化會(huì)引起這個(gè)接口或?qū)崿F(xiàn)類的變化嗎?會(huì)的!那數(shù)據(jù)傳送(想 想看,電話不僅僅可以通電話,還可以上網(wǎng),Modem 撥號(hào)上網(wǎng)呀)的變化會(huì)引起這個(gè)接口或者實(shí)現(xiàn)類的變化 嗎?會(huì)的!那就很簡(jiǎn)單了,這里有兩個(gè)原因都引起了類的變化,而且這兩個(gè)職責(zé)會(huì)相互影響嗎?電話通話, 我只要能接通就成,甭管是 2G 還是 3G 的協(xié)議;電話連接后還關(guān)心傳遞的是什么數(shù)據(jù)嗎?不關(guān)心,你要是 樂(lè)意使用 56K 的小貓傳遞一個(gè)高清的片子,那也沒(méi)有問(wèn)題(頂多有人說(shuō)你 13 了),也就說(shuō)我們現(xiàn)在提供的 這個(gè) IPhone 接口包含了兩個(gè)職責(zé),而且這兩個(gè)職責(zé)的變化不相互影響,那就考慮拆開(kāi)成兩個(gè)接口:
這個(gè)類圖看這有點(diǎn)復(fù)雜了,完全滿足了類和接口的單一職責(zé)要求,非常符合標(biāo)準(zhǔn),但是我相信你在設(shè) 計(jì)的時(shí)候肯定不會(huì)采用這種方式,一個(gè)手機(jī)類要把把兩個(gè) ConnectionManager 和 DataTransfer 組合在一塊 才能使用,組合是一種強(qiáng)耦合關(guān)系,你和我都是有共同的生命期,這樣的強(qiáng)耦合關(guān)系還不如使用接口實(shí)現(xiàn) 的方式呢,而且還增加了類的復(fù)雜性,多了兩個(gè)類呀,好,我們修改一下類圖:
這樣的設(shè)計(jì)才是完美的,一個(gè)手機(jī)實(shí)現(xiàn)了兩個(gè)接口,把兩個(gè)職責(zé)融合一個(gè)類中,你會(huì)覺(jué)得這個(gè) Phone 有兩個(gè)原因引起變化了呀,是的是的,但是別忘記了我們是面向接口編程,我們對(duì)外公布的是接口而不是 實(shí)現(xiàn)類;而且如果真要實(shí)現(xiàn)類的單一職責(zé)的話,這個(gè)就必須使用了上面組合的方式了,那這個(gè)會(huì)引起類間 耦合過(guò)重的問(wèn)題(我們等會(huì)再說(shuō)明這個(gè)問(wèn)題,繼續(xù)往下看)。那使用了單一職責(zé)原則有什么好處呢? 類的復(fù)雜性降低,實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義; 可讀性提高,復(fù)雜性降低,那當(dāng)然可讀性提高了; 可維護(hù)性提高,那當(dāng)然了,可讀性提高,那當(dāng)然更容易維護(hù)了;
變更引起的風(fēng)險(xiǎn)降低,變更是必不可少的,接口的單一職責(zé)做的好的話,一個(gè)接口修改只對(duì)相應(yīng)的實(shí) 現(xiàn)類有影響,與其他的接口無(wú)影響,這個(gè)是對(duì)項(xiàng)目有非常大的幫助。 講過(guò)這個(gè)例子后是不是有點(diǎn)凡反思了,我以前的設(shè)計(jì)是不是有點(diǎn)的問(wèn)題了?是的,單一職責(zé)原則最難 劃分的就是職責(zé),一個(gè)職責(zé)一個(gè)接口,但是問(wèn)題是“職責(zé)”是一個(gè)沒(méi)有量化的標(biāo)準(zhǔn),一個(gè)類到底要負(fù)責(zé)那 些職責(zé)?這些職責(zé)怎么細(xì)化?細(xì)化后是否都要有一個(gè)接口或類?這個(gè)都是需要從實(shí)際的項(xiàng)目區(qū)考慮的,從 功能上來(lái)說(shuō),定義一個(gè) IPhone 接口也沒(méi)有錯(cuò),實(shí)現(xiàn)了電話的功能呀,而且設(shè)計(jì)還很簡(jiǎn)單,就一個(gè)接口一個(gè) 實(shí)現(xiàn)類,真正的項(xiàng)目我想大家一般都是會(huì)這么設(shè)計(jì)的,從設(shè)計(jì)原則上來(lái)看就有問(wèn)題了,有兩個(gè)可以變化的 原因放到了一個(gè)接口中了,這就為以后的變化帶來(lái)了風(fēng)險(xiǎn),我從 2G 通訊協(xié)議修改到 3G 通訊,你看看你提 供出的接口 IPhone 是不是要修改了?接口修改對(duì)其他的 Invoker 是不是有很大影響?!
對(duì)于接口,我們?cè)谠O(shè)計(jì)的時(shí)候一定要做到單一,但是對(duì)于實(shí)現(xiàn)類就需要多方面考慮了,生搬硬套單一 職責(zé)原則會(huì)引起類的劇增,給維護(hù)帶來(lái)非常多的麻煩;而且過(guò)分的細(xì)分類的職責(zé)也為人為的制造系統(tǒng)的負(fù) 責(zé)性,本來(lái)一個(gè)類可以實(shí)現(xiàn)的行為非要拆成兩個(gè),然后使用聚合或組合的方式再耦合在一起,這個(gè)是人為 制造了系統(tǒng)的復(fù)雜性,所以原則是死的,人是活的,這句話是非常好的。 單一職責(zé)原則很難在項(xiàng)目中得到體現(xiàn),非常難,為什么?考慮項(xiàng)目環(huán)境,考慮工作量,考慮人員的技 術(shù)水平,考慮硬件的資源情況等等,最終融合的結(jié)果是經(jīng)常違背這一單一原則。而且我們中華文明就有很 多屬于混合型的產(chǎn)物,比如筷子,我們使用筷子可以當(dāng)做刀來(lái)使用,分割食物;還可以當(dāng)叉使用,把食物 從盤(pán)子中移動(dòng)到口中;而在西方的文化中,刀就是刀,叉就是叉,你去吃西餐的時(shí)候這兩樣肯定都是有的, 分工很明晰,刀就是切割食物,叉就是固定食物或者移動(dòng)食物,這個(gè)中國(guó)的文化有非常大的關(guān)系,中國(guó)文 化就是一個(gè)綜合性的文化,要求一個(gè)人或是一個(gè)事物能夠滿足多個(gè)用途,一個(gè)人既要求你會(huì)技術(shù)還要會(huì)管 理,還要會(huì)財(cái)務(wù),要計(jì)算成本呀,這個(gè)是從小兵就開(kāi)始要求了,哎,比較悲哀!我相信如果電腦是中國(guó)發(fā) 明的話,肯定是這個(gè)樣子:CPU、內(nèi)存、主板、電源全部融合在一起,什么邏輯運(yùn)算,數(shù)據(jù)處理,圖像顯示 全部放一塊,呵呵,有點(diǎn)憤青的成分在里面了。但是我相信隨著技術(shù)的深入,單一職責(zé)原則必然會(huì)深入到 項(xiàng)目的設(shè)計(jì)中去,而且這個(gè)原則是那么的簡(jiǎn)單,簡(jiǎn)單的我都不知道該怎么更加詳細(xì)的去描述,單從字面上 大家都應(yīng)該知道是什么意思,單一職責(zé)嘛! 單一職責(zé)使用于接口、類,同時(shí)也使用方法,什么意思呢?一個(gè)方法盡可能做一件事情,比如一個(gè)方 法修改用戶密碼,別把這個(gè)方法放到“修改用戶信息”方法中,這個(gè)方法的顆粒度很粗,比如這個(gè)一個(gè)方 法:
在 IUserManager 中定義了一個(gè)方法叫 changeUser,根據(jù)傳遞的 type 不同,把可變長(zhǎng)度參數(shù) changeOptions 修改到 userBo 這個(gè)對(duì)象上,并調(diào)用持久層的方法保存到數(shù)據(jù)庫(kù)中。在我的項(xiàng)目組中如果有 人寫(xiě)了這樣一個(gè)方法,我不管他寫(xiě)了多上程序化了多少工夫,一律重寫(xiě)!原因是:方法職責(zé)不清晰,不單 一,一般方法設(shè)計(jì)成這樣的:
你要修改用戶名稱,就調(diào)用 changeUserName 方法,你要修改家庭地址就調(diào)用 changeHomeAddress,你 要修改單位單戶就調(diào)用 changeOfficeTel 方法,每個(gè)方法的職責(zé)就非常清晰,這也是一個(gè)良好的設(shè)計(jì)習(xí)慣。 所以,不管是對(duì)接口、類、方法使用了單一規(guī)則原則,那么快樂(lè)的就不僅僅是你了,還有你項(xiàng)目的成 員,你的板,減少了因?yàn)樽兏鸬墓ぷ髁垦剑庸龠M(jìn)爵等著你幺! 你看到這里,就會(huì)問(wèn)我,你寫(xiě)是類的設(shè)計(jì)原則嗎?你通篇都在說(shuō)接口的單一職責(zé),類的單一職責(zé)你都 違背了呀,呵呵,這個(gè)還真是的,我的本意是想把這個(gè)原則講清楚,類的單一職責(zé)嘛,這個(gè)很簡(jiǎn)單,但當(dāng) 我回頭寫(xiě)的時(shí)候,發(fā)覺(jué)才不是這么回事,翻看了以前一些設(shè)計(jì)和代碼,基本上拿的出手的類設(shè)計(jì)都是和單 一職責(zé)向違背的,靜下心來(lái)回憶,發(fā)覺(jué)每一個(gè)類這樣設(shè)計(jì)都是有原因的。這幾天我查閱了 wikipedia、 oodesign 等幾個(gè)網(wǎng)站,專家和我也有類似的經(jīng)驗(yàn),基本上類的單一職責(zé)都用了類似的一句話來(lái)說(shuō)“This is sometimes hard to see”,這句話翻譯過(guò)來(lái)就是“這個(gè)有時(shí)候很難說(shuō)”,是的,類的單一職責(zé)確實(shí)受非常多 的因素制約,純理論的來(lái)講,這個(gè)原則是非常優(yōu)秀的,但是現(xiàn)實(shí)有現(xiàn)實(shí)難處,你必須去考慮項(xiàng)目工期、成 本、人員技術(shù)水平、硬件情況、網(wǎng)絡(luò)情況甚至有時(shí)候還要考慮政府政策、壟斷協(xié)議等等原因,比如 04 年就 做過(guò)一個(gè)項(xiàng)目,做加密處理的,甲方就甩過(guò)來(lái)一句話,你什么都不用管,就調(diào)用這個(gè) API 就可以了,不用 考慮什么傳輸協(xié)議、異常處理、安全連接等等,所以我們就直接使用了 JNI 與加密廠商提供的 API 通信, 什么單一職責(zé)原則,根本就不用考慮,因?yàn)槿思也还纪ㄐ沤涌凇惓L幚怼?對(duì)于單一職責(zé)原則,我的建議是接口一定要做到單一職責(zé),類設(shè)計(jì)盡量只有一個(gè)原因引起變化。
引用
24種設(shè)計(jì)模式介紹與6大設(shè)計(jì)原則 前面博客已有鏈接地址
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注