1. 單一職責(zé)原則(Single Responsibility Principle, SRP)
單一職責(zé)原則指出一個(gè)類應(yīng)該只有一個(gè)原因引起變化,即一個(gè)類應(yīng)該只負(fù)責(zé)一項(xiàng)職責(zé)。如果一個(gè)類承擔(dān)了過(guò)多的職責(zé),那么在修改它以滿足一個(gè)職責(zé)的需求時(shí),可能會(huì)產(chǎn)生副作用,從而影響到其他職責(zé)的功能。遵循單一職責(zé)原則可以使代碼更加清晰,降低類的復(fù)雜性,提高模塊化程度。
2. 開(kāi)閉原則(Open/Closed Principle, OCP)
開(kāi)閉原則強(qiáng)調(diào)軟件實(shí)體(類、模塊、函數(shù)等)應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。這意味著在設(shè)計(jì)一個(gè)模塊的時(shí)候,應(yīng)該使得這個(gè)模塊可以在不被修改的前提下進(jìn)行擴(kuò)展。這樣做可以減少因?yàn)樾薷默F(xiàn)有代碼而引入的錯(cuò)誤,同時(shí)也使得系統(tǒng)更加靈活,易于添加新功能。
3. 里氏替換原則(Liskov Substitution Principle, LSP)
里氏替換原則是指子類型必須能夠替換掉它們的基類型,即子類對(duì)象應(yīng)該能夠替換掉父類對(duì)象被使用。這意味著在軟件中,子類繼承父類時(shí),應(yīng)該能夠保證父類的所有行為在子類中仍然有效。如果違反了這個(gè)原則,可能會(huì)導(dǎo)致在使用子類替換父類的情況下,程序出現(xiàn)錯(cuò)誤或者異常。
4. 接口隔離原則(Interface Segregation Principle, ISP)
接口隔離原則主張接口應(yīng)該小而專注,不應(yīng)該強(qiáng)迫客戶程序依賴于它們不用的方法。這個(gè)原則的目的是降低類與接口之間的耦合度,使得類可以實(shí)現(xiàn)它們需要的接口,而不是實(shí)現(xiàn)一個(gè)龐大的、包含許多不必要方法的接口。這樣可以提高系統(tǒng)的靈活性和可維護(hù)性。
5. 依賴倒置原則(Dependency Inversion Principle, DIP)
依賴倒置原則是指高層模塊不應(yīng)該依賴于低層模塊,兩者都應(yīng)該依賴于抽象;抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。這個(gè)原則的核心思想是通過(guò)抽象來(lái)減少模塊間的耦合,使得系統(tǒng)更加模塊化,從而提高代碼的可讀性、可維護(hù)性和可擴(kuò)展性。
這些設(shè)計(jì)原則,從字面上理解都不難。一看就感覺(jué)懂了,但真的用到項(xiàng)目中的時(shí)候,會(huì)發(fā)現(xiàn),“看懂”和“會(huì)用”是兩回事,而“用好”更是難上加難。從我之前的工作經(jīng)歷來(lái)看,很多同事因?yàn)閷?duì)這些原則理解得不夠透徹,導(dǎo)致在使用的時(shí)候過(guò)于教條主義,拿原則當(dāng)真理,生搬硬套,反而適得其反。
那么如何更好的理解這些原則呢?下面我通過(guò)一個(gè)例子來(lái)說(shuō)明,力求使大家能夠不僅懂而且會(huì)用。
如何理解單一職責(zé)原則(SRP)?
單一職責(zé)原則的英文是 Single Responsibility Principle,縮寫(xiě)為 SRP。這個(gè)原則的英文描述是這樣的:A class or module should have a single responsibility。如果我們把它翻譯成中文,那就是:一個(gè)類或者模塊只負(fù)責(zé)完成一個(gè)職責(zé)(或者功能)。
注意,這個(gè)原則描述的對(duì)象包含兩個(gè),一個(gè)是類(class),一個(gè)是模塊(module)。關(guān)于這兩個(gè)概念,有兩種理解方式。一種理解是:把模塊看作比類更加抽象的概念,類也可以看作模塊。另一種理解是:把模塊看作比類更加粗粒度的代碼塊,模塊中包含多個(gè)類,多個(gè)類組成一個(gè)模塊。
無(wú)論哪種理解方式,想象一下,單一職責(zé)原則就像是給每個(gè)工作角色分配一項(xiàng)特定的任務(wù)。不管是哪種情況,這個(gè)原則都是一個(gè)道理:每個(gè)角色(或者說(shuō)類)都應(yīng)該只做一件事,而且要做好?,F(xiàn)在,我們就聊聊在設(shè)計(jì)一個(gè)類的時(shí)候,怎么按照這個(gè)原則來(lái)操作。至于模塊怎么用這個(gè)原則,你可以自己想一想,原理是類似的。
這個(gè)原則其實(shí)很簡(jiǎn)單:一個(gè)類就負(fù)責(zé)一個(gè)任務(wù)。就像我們不喜歡一個(gè)員工同時(shí)做太多不同的工作一樣,一個(gè)類也不應(yīng)該承擔(dān)太多功能。如果一個(gè)類做了太多不相關(guān)的工作,我們就得把它分成幾個(gè)小類,每個(gè)小類只負(fù)責(zé)一個(gè)具體的工作。
比如說(shuō),你有一個(gè)類,它既處理訂單的事情,又處理用戶的事情。訂單和用戶是兩碼事,對(duì)吧?把這兩件事放在一個(gè)類里,就像讓一個(gè)人同時(shí)做廚師和會(huì)計(jì)的工作,這顯然是不合理的。按照單一職責(zé)原則,我們應(yīng)該把這個(gè)類分成兩個(gè):一個(gè)專門(mén)處理訂單的類,另一個(gè)專門(mén)處理用戶的類。這樣一來(lái),每個(gè)類都只關(guān)注一件事情,工作起來(lái)就更加得心應(yīng)手了。
如何判斷類的職責(zé)是否足夠單一?
從剛剛這個(gè)例子來(lái)看,單一職責(zé)原則看似不難應(yīng)用。那是因?yàn)槲遗e的這個(gè)例子比較極端,一眼就能看出訂單和用戶毫不相干。但大部分情況下,類里的方法是歸為同一類功能,還是歸為不相關(guān)的兩類功能,并不是那么容易判定的。在真實(shí)的軟件開(kāi)發(fā)中,對(duì)于一個(gè)類是否職責(zé)單一的判定,是很難拿捏的。我舉一個(gè)更加貼近實(shí)際的例子來(lái)給你解釋一下。
在一個(gè)社交產(chǎn)品中,我們用下面的 UserInfo 類來(lái)記錄用戶的信息。你覺(jué)得,UserInfo 類的設(shè)計(jì)是否滿足單一職責(zé)原則呢?
public class UserInfo {
private long userId;
private String username;
private String email;
private String telephone;
private long createTime;
private long lastLoginTime;
private String avatarUrl;
private String provinceOfAddress; // 省
private String cityOfAddress; // 市
private String regionOfAddress; // 區(qū)
private String detailedAddress; // 詳細(xì)地址
// ...省略其他屬性和方法...
}
關(guān)于UserInfo這個(gè)類,大家看法可能不同。有人覺(jué)得,既然UserInfo里裝的都是關(guān)于用戶的各種信息,那么它就符合那個(gè)所謂的單一職責(zé)原則,意思就是一個(gè)類只干一種活兒。但另一些人認(rèn)為,因?yàn)閁serInfo里地址信息占了很大一部分,所以可以把這部分信息單獨(dú)拿出來(lái),搞個(gè)新的UserAddress類,讓UserInfo只保留其他用戶信息。這樣一來(lái),每個(gè)類負(fù)責(zé)的活兒就更專一了。
那哪種說(shuō)法更靠譜呢?其實(shí),這得看我們用這個(gè)社交軟件的具體情況。如果這個(gè)軟件就是用來(lái)展示用戶的基本信息,那現(xiàn)在的UserInfo設(shè)計(jì)就挺好。但如果這個(gè)軟件后來(lái)要加個(gè)購(gòu)物功能,用戶的地址信息就得在物流中用到,那我們最好還是把地址信息單獨(dú)搞出來(lái),弄成個(gè)專門(mén)的用戶物流信息類。
再往深了想,如果這個(gè)公司越做越大,又開(kāi)發(fā)了一堆其他應(yīng)用,還想讓所有應(yīng)用都能用同一個(gè)賬號(hào)登錄,那我們就得再對(duì)UserInfo動(dòng)動(dòng)手腳,把跟登錄認(rèn)證相關(guān)的信息,比如郵箱、手機(jī)號(hào)這些,再抽出來(lái),單獨(dú)搞個(gè)類。
所以說(shuō),一個(gè)類要不要繼續(xù)拆,得看我們用它來(lái)干嘛,以及將來(lái)可能要干嘛。有時(shí)候,一個(gè)類現(xiàn)在看起來(lái)挺合適的,但換個(gè)環(huán)境或者將來(lái)需求變了,就可能不夠用了,得繼續(xù)拆。而且,從不同的角度看同一個(gè)類,也可能有不同的想法。比如,從“用戶”這個(gè)整體來(lái)看,UserInfo里的東西都跟用戶相關(guān),看起來(lái)挺專一的。但如果我們從更細(xì)的角度看,比如“用戶展示信息”、“地址信息”、“登錄認(rèn)證信息”,那我們可能就得繼續(xù)拆分UserInfo。
總的來(lái)說(shuō),判斷一個(gè)類是不是專一,這事兒挺主觀的,沒(méi)有絕對(duì)的標(biāo)準(zhǔn)。在實(shí)際編程時(shí),我們也不用太著急,一開(kāi)始就想得太完美??梢韵扰獋€(gè)簡(jiǎn)單的類,滿足現(xiàn)在的需要。等以后業(yè)務(wù)發(fā)展了,如果這個(gè)類變得越來(lái)越復(fù)雜,代碼一大堆,那時(shí)候再考慮把它拆成幾個(gè)小類。這個(gè)過(guò)程,其實(shí)就是我們常說(shuō)的不斷改進(jìn)和調(diào)整。
聽(tīng)到這里,你可能會(huì)說(shuō),這個(gè)原則如此含糊不清、模棱兩可,到底該如何拿捏才好???
這里還有一些小技巧,能夠很好地幫你,從側(cè)面上判定一個(gè)類的職責(zé)是否夠單一。而且,個(gè)人覺(jué)得,下面這幾條判斷原則,比起很主觀地去思考類是否職責(zé)單一,要更有指導(dǎo)意義、更具有可執(zhí)行性:
類中的代碼行數(shù)、函數(shù)或?qū)傩赃^(guò)多,會(huì)影響代碼的可讀性和可維護(hù)性,我們就需要考慮對(duì)類進(jìn)行拆分;
類依賴的其他類過(guò)多,或者依賴類的其他類過(guò)多,不符合高內(nèi)聚、低耦合的設(shè)計(jì)思想,我們就需要考慮對(duì)類進(jìn)行拆分;
私有方法過(guò)多,我們就要考慮能否將私有方法獨(dú)立到新的類中,設(shè)置為 public 方法,供更多的類使用,從而提高代碼的復(fù)用性;
比較難給類起一個(gè)合適名字,很難用一個(gè)業(yè)務(wù)名詞概括,或者只能用一些籠統(tǒng)的 Manager、Context 之類的詞語(yǔ)來(lái)命名,這就說(shuō)明類的職責(zé)定義得可能不夠清晰;
類中大量的方法都是集中操作類中的某幾個(gè)屬性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考慮將這幾個(gè)屬性和對(duì)應(yīng)的方法拆分出來(lái)。
不過(guò),你可能還會(huì)有這樣的疑問(wèn):在上面的判定原則中,我提到類中的代碼行數(shù)、函數(shù)或者屬性過(guò)多,就有可能不滿足單一職責(zé)原則。那多少行代碼才算是行數(shù)過(guò)多呢?多少個(gè)函數(shù)、屬性才稱得上過(guò)多呢?
比較初級(jí)的工程師經(jīng)常會(huì)問(wèn)這類問(wèn)題。實(shí)際上,這個(gè)問(wèn)題并不好定量地回答,就像你問(wèn)大廚“放鹽少許”中的“少許”是多少,大廚也很難告訴你一個(gè)特別具體的量值。
如果繼續(xù)深究一下的話,你可能還會(huì)說(shuō),一些菜譜確實(shí)給出了,做某某菜需要放多少克鹽,放多少克油的具體量值啊。我想說(shuō)的是,那是給家庭主婦用的,那不是給專業(yè)的大廚看的。類比一下做飯,如果你是沒(méi)有太多項(xiàng)目經(jīng)驗(yàn)的編程初學(xué)者,實(shí)際上,我也可以給你一個(gè)湊活能用、比較寬泛的、可量化的標(biāo)準(zhǔn),那就是一個(gè)類的代碼行數(shù)最好不能超過(guò) 200 行,函數(shù)個(gè)數(shù)及屬性個(gè)數(shù)都最好不要超過(guò) 10 個(gè)。
實(shí)際上, 從另一個(gè)角度來(lái)看,當(dāng)一個(gè)類的代碼,讀起來(lái)讓你頭大了,實(shí)現(xiàn)某個(gè)功能時(shí)不知道該用哪個(gè)函數(shù)了,想用哪個(gè)函數(shù)翻半天都找不到了,只用到一個(gè)小功能要引入整個(gè)類(類中包含很多無(wú)關(guān)此功能實(shí)現(xiàn)的函數(shù))的時(shí)候,這就說(shuō)明類的行數(shù)、函數(shù)、屬性過(guò)多了。實(shí)際上,代碼寫(xiě)多了,在開(kāi)發(fā)中慢慢“品嘗”,自然就知道什么是“放鹽少許”了,這就是所謂的“專業(yè)第六感”。
類的職責(zé)是否設(shè)計(jì)得越單一越好?
為了滿足單一職責(zé)原則,是不是把類拆得越細(xì)就越好呢?答案是否定的。我們還是通過(guò)一個(gè)例子來(lái)解釋一下。Serialization 類實(shí)現(xiàn)了一個(gè)簡(jiǎn)單協(xié)議的序列化和反序列功能,具體代碼如下:
/**
* Protocol format: identifier-string;{gson string}
* For example: UEUEUE;{"a":"A","b":"B"}
*/
public class Serialization {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Serialization() {
this.gson = new Gson();
}
public String serialize(Mapobject) {
StringBuilder textBuilder = new StringBuilder();
textBuilder.append(IDENTIFIER_STRING);
textBuilder.append(gson.toJson(object));
return textBuilder.toString();
}
public Mapdeserialize(String text) {
if (!text.startsWith(IDENTIFIER_STRING)) {
return Collections.emptyMap();
}
String gsonStr = text.substring(IDENTIFIER_STRING.length());
return gson.fromJson(gsonStr, Map.class);
}
}
如果我們想讓類的職責(zé)更加單一,我們對(duì) Serialization 類進(jìn)一步拆分,拆分成一個(gè)只負(fù)責(zé)序列化工作的 Serializer 類和另一個(gè)只負(fù)責(zé)反序列化工作的 Deserializer 類。拆分后的具體代碼如下所示:
public class Serializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Serializer() {
this.gson = new Gson();
}
public String serialize(Mapobject) {
StringBuilder textBuilder = new StringBuilder();
textBuilder.append(IDENTIFIER_STRING);
textBuilder.append(gson.toJson(object));
return textBuilder.toString();
}
}
public class Deserializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Deserializer() {
this.gson = new Gson();
}
public Mapdeserialize(String text) {
if (!text.startsWith(IDENTIFIER_STRING)) {
return Collections.emptyMap();
}
String gsonStr = text.substring(IDENTIFIER_STRING.length());
return gson.fromJson(gsonStr, Map.class);
}
}
雖然經(jīng)過(guò)拆分之后,Serializer 類和 Deserializer 類的職責(zé)更加單一了,但也隨之帶來(lái)了新的問(wèn)題。如果我們修改了協(xié)議的格式,數(shù)據(jù)標(biāo)識(shí)從“UEUEUE”改為“DFDFDF”,或者序列化方式從 JSON 改為了 XML,那 Serializer 類和 Deserializer 類都需要做相應(yīng)的修改,代碼的內(nèi)聚性顯然沒(méi)有原來(lái) Serialization 高了。而且,如果我們僅僅對(duì) Serializer 類做了協(xié)議修改,而忘記了修改 Deserializer 類的代碼,那就會(huì)導(dǎo)致序列化、反序列化不匹配,程序運(yùn)行出錯(cuò),也就是說(shuō),拆分之后,代碼的可維護(hù)性變差了。
實(shí)際上,不管是應(yīng)用設(shè)計(jì)原則還是設(shè)計(jì)模式,最終的目的還是提高代碼的可讀性、可擴(kuò)展性、復(fù)用性、可維護(hù)性等。我們?cè)诳紤]應(yīng)用某一個(gè)設(shè)計(jì)原則是否合理的時(shí)候,也可以以此作為最終的考量標(biāo)準(zhǔn)。
我們來(lái)一塊總結(jié)回顧一下。
1. 如何理解單一職責(zé)原則(SRP)?
一個(gè)類只負(fù)責(zé)完成一個(gè)職責(zé)或者功能。不要設(shè)計(jì)大而全的類,要設(shè)計(jì)粒度小、功能單一的類。單一職責(zé)原則是為了實(shí)現(xiàn)代碼高內(nèi)聚、低耦合,提高代碼的復(fù)用性、可讀性、可維護(hù)性。
2. 如何判斷類的職責(zé)是否足夠單一?
不同的應(yīng)用場(chǎng)景、不同階段的需求背景、不同的業(yè)務(wù)層面,對(duì)同一個(gè)類的職責(zé)是否單一,可能會(huì)有不同的判定結(jié)果。實(shí)際上,一些側(cè)面的判斷指標(biāo)更具有指導(dǎo)意義和可執(zhí)行性,比如,出現(xiàn)下面這些情況就有可能說(shuō)明這類的設(shè)計(jì)不滿足單一職責(zé)原則:
類中的代碼行數(shù)、函數(shù)或者屬性過(guò)多;
類依賴的其他類過(guò)多,或者依賴類的其他類過(guò)多;
私有方法過(guò)多;
比較難給類起一個(gè)合適的名字;
類中大量的方法都是集中操作類中的某幾個(gè)屬性。
3. 類的職責(zé)是否設(shè)計(jì)得越單一越好?
單一職責(zé)原則通過(guò)避免設(shè)計(jì)大而全的類,避免將不相關(guān)的功能耦合在一起,來(lái)提高類的內(nèi)聚性。同時(shí),類職責(zé)單一,類依賴的和被依賴的其他類也會(huì)變少,減少了代碼的耦合性,以此來(lái)實(shí)現(xiàn)代碼的高內(nèi)聚、低耦合。但是,如果拆分得過(guò)細(xì),實(shí)際上會(huì)適得其反,反倒會(huì)降低內(nèi)聚性,也會(huì)影響代碼的可維護(hù)性。