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