目錄介紹
- 00.問題思考分析
- 01.前沿基礎介紹
- 02.如何理解單一指責
- 03.如何判斷是否單一
- 04.單一判斷原則
- 05.單一就更好麼
- 06.總結回顧一下
00.問題思考分析
- 01.如何理解類的單一指責,單一指責中這個單一是如何評判的?
- 02.懂了,但是會用麼,或者實際開發中有哪些運用,能否舉例說明單一職責優勢?
- 03.單一指責是否設計越單一,越好呢?說出你的緣由和論證的思路想法?
- 04.單一職責原則,除了應用到類的設計上,還能延伸到哪些其他設計方面嗎?
01.前沿基礎介紹
- 學習一些經典的設計原則,其中包括,SOLID、KISS、YAGNI、DRY、LOD等。這些設計原則,從字面上理解,都不難。你一看就感覺懂了,一看就感覺掌握了,但真的用到專案中的時候,你會發現,“看懂”和“會用”是兩回事,而“用好”更是難上加難。從工作經歷來看,很多同事因為對這些原則理解得不夠透徹,導致在使用的時候過於教條主義,拿原則當真理,生搬硬套,適得其反。
02.如何理解單一指責
- 單一職責原則的英文是 Single Responsibility Principle,縮寫為 SRP。這個原則的英文描述是這樣的:A class or module should have a single reponsibility。如果我們把它翻譯成中文,那就是:一個類或者模組只負責完成一個職責(或者功能)。
- 注意,這個原則描述的物件包含兩個,一個是類(class),一個是模組(module)。關於這兩個概念,在專欄中,有兩種理解方式。一種理解是:把模組看作比類更加抽象的概念,類也可以看作模組。另一種理解是:把模組看作比類更加粗粒度的程式碼塊,模組中包含多個類,多個類組成一個模組。
- 不管哪種理解方式,單一職責原則在應用到這兩個描述物件的時候,道理都是相通的。為了方便你理解,接下來我只從“類”設計的角度,來講解如何應用這個設計原則。對於“模組”來說,你可以自行引申。
- 單一職責原則的定義描述非常簡單,也不難理解。一個類只負責完成一個職責或者功能。也就是說,不要設計大而全的類,要設計粒度小、功能單一的類。換個角度來講就是,一個類包含了兩個或者兩個以上業務不相干的功能,那我們就說它職責不夠單一,應該將它拆分成多個功能更加單一、粒度更細的類。
- 舉一個例子來解釋一下。比如,一個類裡既包含訂單的一些操作,又包含使用者的一些操作。而訂單和使用者是兩個獨立的業務領域模型,我們將兩個不相干的功能放到同一個類中,那就違反了單一職責原則。為了滿足單一職責原則,我們需要將這個類拆分成兩個粒度更細、功能更加單一的兩個類:訂單類和使用者類。
- 舉一個例子來解釋一下。比如,一個類裡既包含訂單的一些操作,又包含使用者的一些操作。而訂單和使用者是兩個獨立的業務領域模型,我們將兩個不相干的功能放到同一個類中,那就違反了單一職責原則。為了滿足單一職責原則,我們需要將這個類拆分成兩個粒度更細、功能更加單一的兩個類:訂單類和使用者類。
03.如何判斷是否單一
- 從剛剛這個例子來看,單一職責原則看似不難應用。那是因為我舉的這個例子比較極端,一眼就能看出訂單和使用者毫不相干。但大部分情況下,類裡的方法是歸為同一類功能,還是歸為不相關的兩類功能,並不是那麼容易判定的。在真實的軟體開發中,對於一個類是否職責單一的判定,是很難拿捏的。我舉一個更加貼近實際的例子來給你解釋一下。
在一個社交產品中,我們用下面的 UserInfo 類來記錄使用者的資訊。你覺得,UserInfo 類的設計是否滿足單一職責原則呢?
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; // 區 private String detailedAddress; // 詳細地址 // ...省略其他屬性和方法... }
對於這個問題,有兩種不同的觀點。
- 一種觀點是,UserInfo 類包含的都是跟使用者相關的資訊,所有的屬性和方法都隸屬於使用者這樣一個業務模型,滿足單一職責原則;
- 另一種觀點是,地址資訊在 UserInfo 類中,所佔的比重比較高,可以繼續拆分成獨立的 UserAddress 類,UserInfo 只保留除 Address 之外的其他資訊,拆分之後的兩個類的職責更加單一。
哪種觀點更對呢?實際上,要從中做出選擇,我們不能脫離具體的應用場景。
- 如果在這個社交產品中,使用者的地址資訊跟其他資訊一樣,只是單純地用來展示,那 UserInfo 現在的設計就是合理的。
- 如果這個社交產品發展得比較好,之後又在產品中新增了電商的模組,使用者的地址資訊還會用在電商物流中,那我們最好將地址資訊從 UserInfo 中拆分出來,獨立成使用者物流資訊(或者叫地址資訊、收貨資訊等)。
- 我們再進一步延伸一下。如果做這個社交產品的公司發展得越來越好,公司內部又開發出了跟多其他產品(可以理解為其他 App)。公司希望支援統一賬號系統,也就是使用者一個賬號可以在公司內部的所有產品中登入。這個時候,我們就需要繼續對 UserInfo 進行拆分,將跟身份認證相關的資訊(比如,email、telephone 等)抽取成獨立的類。
- 從剛剛這個例子,我們可以總結出,不同的應用場景、不同階段的需求背景下,對同一個類的職責是否單一的判定,可能都是不一樣的。在某種應用場景或者當下的需求背景下,一個類的設計可能已經滿足單一職責原則了,但如果換個應用場景或著在未來的某個需求背景下,可能就不滿足了,需要繼續拆分成粒度更細的類。
- 除此之外,從不同的業務層面去看待同一個類的設計,對類是否職責單一,也會有不同的認識。比如,例子中的 UserInfo 類。如果我們從“使用者”這個業務層面來看,UserInfo 包含的資訊都屬於使用者,滿足職責單一原則。如果我們從更加細分的“使用者展示資訊”“地址資訊”“登入認證資訊”等等這些更細粒度的業務層面來看,那 UserInfo 就應該繼續拆分。
- 綜上所述,評價一個類的職責是否足夠單一,我們並沒有一個非常明確的、可以量化的標準,可以說,這是件非常主觀、仁者見仁智者見智的事情。實際上,在真正的軟體開發中,我們也沒必要過於未雨綢繆,過度設計。所以,我們可以先寫一個粗粒度的類,滿足業務需求。隨著業務的發展,如果粗粒度的類越來越龐大,程式碼越來越多,這個時候,我們就可以將這個粗粒度的類,拆分成幾個更細粒度的類。這就是所謂的持續重構(後面的章節中我們會講到)。
04.單一判斷原則
聽到這裡,你可能會說,這個原則如此含糊不清、模稜兩可,到底該如何拿捏才好啊?我這裡還有一些小技巧,能夠很好地幫你,從側面上判定一個類的職責是否夠單一。而且,我個人覺得,下面這幾條判斷原則,比起很主觀地去思考類是否職責單一,要更有指導意義、更具有可執行性:
- 類中的程式碼行數、函式或屬性過多,會影響程式碼的可讀性和可維護性,我們就需要考慮對類進行拆分;
- 類依賴的其他類過多,或者依賴類的其他類過多,不符合高內聚、低耦合的設計思想,我們就需要考慮對類進行拆分;
- 私有方法過多,我們就要考慮能否將私有方法獨立到新的類中,設定為 public 方法,供更多的類使用,從而提高程式碼的複用性;
- 比較難給類起一個合適名字,很難用一個業務名詞概括,或者只能用一些籠統的 Manager、Context 之類的詞語來命名,這就說明類的職責定義得可能不夠清晰;
- 類中大量的方法都是集中操作類中的某幾個屬性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 資訊,那就可以考慮將這幾個屬性和對應的方法拆分出來。
不過,你可能還會有這樣的疑問:在上面的判定原則中,我提到類中的程式碼行數、函式或者屬性過多,就有可能不滿足單一職責原則。那多少行程式碼才算是行數過多呢?多少個函式、屬性才稱得上過多呢?
- 比較初級的工程師經常會問這類問題。實際上,這個問題並不好定量地回答,就像你問大廚“放鹽少許”中的“少許”是多少,大廚也很難告訴你一個特別具體的量值。
- 實際上, 從另一個角度來看,當一個類的程式碼,讀起來讓你頭大了,實現某個功能時不知道該用哪個函式了,想用哪個函式翻半天都找不到了,只用到一個小功能要引入整個類(類中包含很多無關此功能實現的函式)的時候,這就說明類的行數、函式、屬性過多了。實際上,等你做多專案了,程式碼寫多了,在開發中慢慢“品嚐”,自然就知道什麼是“放鹽少許”了,這就是所謂的“專業第六感”。
05.單一就更好麼
為了滿足單一職責原則,是不是把類拆得越細就越好呢?答案是否定的。我們還是透過一個例子來解釋一下。Serialization 類實現了一個簡單協議的序列化和反序列功能,具體程式碼如下:
/** * 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(Map<String, String> object) { StringBuilder textBuilder = new StringBuilder(); textBuilder.append(IDENTIFIER_STRING); textBuilder.append(gson.toJson(object)); return textBuilder.toString(); } public Map<String, String> deserialize(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(Map<String, String> object) { 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 Map<String, String> deserialize(String text) { if (!text.startsWith(IDENTIFIER_STRING)) { return Collections.emptyMap(); } String gsonStr = text.substring(IDENTIFIER_STRING.length()); return gson.fromJson(gsonStr, Map.class); } }
- 雖然經過拆分之後,Serializer 類和 Deserializer 類的職責更加單一了,但也隨之帶來了新的問題。如果我們修改了協議的格式,資料標識從“UEUEUE”改為“DFDFDF”,或者序列化方式從 JSON 改為了 XML,那 Serializer 類和 Deserializer 類都需要做相應的修改,程式碼的內聚性顯然沒有原來 Serialization 高了。而且,如果我們僅僅對 Serializer 類做了協議修改,而忘記了修改 Deserializer 類的程式碼,那就會導致序列化、反序列化不匹配,程式執行出錯,也就是說,拆分之後,程式碼的可維護性變差了。
06.總結回顧一下
1.如何理解單一職責原則(SRP)?
- 一個類只負責完成一個職責或者功能。不要設計大而全的類,要設計粒度小、功能單一的類。單一職責原則是為了實現程式碼高內聚、低耦合,提高程式碼的複用性、可讀性、可維護性。
2.如何判斷類的職責是否足夠單一?
- 不同的應用場景、不同階段的需求背景、不同的業務層面,對同一個類的職責是否單一,可能會有不同的判定結果。實際上,一些側面的判斷指標更具有指導意義和可執行性,比如,出現下面這些情況就有可能說明這類的設計不滿足單一職責原則:
- 類中的程式碼行數、函式或者屬性過多;
- 類依賴的其他類過多,或者依賴類的其他類過多;
- 私有方法過多;
- 比較難給類起一個合適的名字;
- 類中大量的方法都是集中操作類中的某幾個屬性。
3.類的職責是否設計得越單一越好?
- 單一職責原則透過避免設計大而全的類,避免將不相關的功能耦合在一起,來提高類的內聚性。同時,類職責單一,類依賴的和被依賴的其他類也會變少,減少了程式碼的耦合性,以此來實現程式碼的高內聚、低耦合。但是,如果拆分得過細,實際上會適得其反,反倒會降低內聚性,也會影響程式碼的可維護性。