前言
前面我們提到過 SOLID 原則,實際上 SOLID 由 5 個設計原則組成,分別是:單一職責原則、開閉原則、里氏替換原則、接口隔離原則和依賴反轉原則。它們分別對應 SLOID 中的 S、O、L、I、D 這 5 個英文字母。
今天來學習下 SOLID 原則中的第一個原則:單一職責原則。
如何理解單一職責原則(SRP)
單一職責原則 (Single Responsibility Principle),縮寫為 SRP。
英文原文的描述:A class or module should have a single responsibility。翻譯成中文就是,一個類或模塊只負責完成一個職責或功能。
注意,這個原則描述的對象包含兩個,一個是類,一個是模塊。不管是哪個對象,單一職責在應用到這兩個面對對象的時候,道理都是相通的。接下來,我會只從“類”設計的角度,來講解如何應用這個原則。
一個類只負責完成一個職責或者功能。也就是說,不要設計大而全的類,要設計粒度小、功能單一的類。要把大而全的類拆分成多個功能更加單一、粒度更細的類。
換個角度來說,一個類包含了兩個或者兩個以上業務不相干的功能,就不符合單一職責原則。
例如,一個類里面包含訂單的一些操作,又包含用戶的一些操作。而訂單和用戶是兩個獨立的業務領域模型,我們將兩個不相干的功能放到同一個類中,就違反了單一職責原則。為了滿足單一職責原則,需要將這個類拆分成兩個粒度更細、功能更單一的兩個類:訂單類和用戶類。
如何判斷類的職責是否足夠單一?
從剛剛的例子來看,單一職責原則看似不難應用。那是因為我舉的例子比較極端,一眼就能看出訂單和用戶毫不相干。但大部分情況下,類里的方法是歸類為同一類功能,還是歸為不相關的兩類功能,并不是那么容易判定的。在真實的軟件開發中,對一個類是否職責單一的判定,是很難拿捏的。
在一個社交產品中,用下面的 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 detailOfAddress; // 詳細地址// 省略其他屬性和方法...
}
對于這個問題,有兩種不同的觀點。
- 一種觀點是,
UserInfo
類包含的都是跟用戶相關的信息,所有的屬性和方法都隸屬于用戶這樣一個業務模型,滿足單一職責原則; - 另一種觀點是,地址信息在
UserInfo
類中,所占比重較高,可以繼續拆分成獨立的UserAddress
,UserInfo
只保留出UserAddress
之外的屬性,拆分之后的兩個類的職責更加單一。
實際上,要做出選擇,我們不能脫離具體的應用常見。如果,在這個社交產品中,用戶地址信息和其他信息一樣,只是單純地用于展示,那 UserInfo
現在的設計就是合理的。但是,如果這個社交產品發展地比較好,之后又在產品中添加了電商的模塊,用戶的地址信息還會用在電商物流中,那我們最好將地址信息從 UserInfo
中拆分出來,獨立成用戶物流信息。
再進一步延伸下,如果做這個社交產品的公司發展地越來越好,公司內部有開發出了很多其他產品。公司希望支持統一賬號系統,也就是用戶一個賬號可以在公司內的所有產品中登錄。這個時候,我們就需要對 UserInfo
進行拆分,將跟身份認證相關的信息(比如 email
、telephone
等)抽取成獨立的類。
從上面的例子,可以總結出,在不同的應用場景、不同階段的需求背景下,對同一個類的職責是否單一的判定,都是不一樣的。在某種應用場景或者當下的需求背景下,一個類的設計可能符合單一職責原則了,但是如果換個應用場景或者未來的某個需求背景下,可能就不滿足了,需要繼續拆分成粒度更細的類。
此外,從不同的業務層面去看待同一個類的設計,對類是否職責單一,也會有不同的認識。比如,例子用的 UserInfo
類。如果從“用戶”這個業務層面來看,UserInfo
包含的信息都屬于用戶,滿足單一職責原則。如果從更加細分的“用戶展示信息” “地址信息” “登錄認證信息”等等這些細細粒度的業務層面來看,那 UserInfo
就應該繼續拆分。
綜上所述,評價一個類的職責是否足夠單一,我們并沒有非常明確的、可以量化的標準。實際上,在真正的軟件開發中,也沒必要過于未雨綢繆,過度設計。所以,我們可以先寫一個粗粒度的類,滿足業務需求。隨著業務的發展,如果粗粒度的類越來越大,代碼越來越多,這個時候,我們就可以將這個粗粒度的類,拆分成幾個更細粒度的類。這個就是所謂的重構。
這里還有些小技巧,能夠輔助你,從側面判斷一個類的職責是否足夠單一:
- 類中的代碼行數、函數或屬性過多,會影響代碼的可讀性和可維護性,我們就需要考慮讀類進行拆分。
- 類依賴的其他類過多,或者依賴的類的其他類過多,不符合高內聚、低耦合的設計思想,就需要考慮拆分。
- 私有方法過多,就要考慮是否將私有方法獨立到新的類中,設置為 public 方法,供更多的類適用,從而提高代碼的復用性。
- 比較難給類起合適的名字,很難用一個業務名詞概括,或者只能用一些籠統的
Manager
、Context
之類的詞語來命名,這就說明類的職責定義得可能不夠清楚。 - 類中的大量方法都是集中操作類中的某幾個屬性,比如,在
UserInfo
中,如果有一辦的方法都是在操作address
信息,那就可以考慮將這幾個屬性和對應的方法拆分出來。
不過,你可能有這樣的疑問:在上面的小技巧中,提到的類的代碼行數、函數或者屬性過多,就有可能不滿足單一職責原則。那多少行代碼才算是行數過多呢?多少個函數、屬性才稱得上過多呢?
實際上,這個問題,很不好定量的回答。實際上,可以給你一個湊活能用的、比較寬泛的、可量化的標準,那就是一個類的代碼行數最好不能超過 200
行,函數及屬性的個數最好不要超過 10 個。
實際上,從另一種角度來看,當一個類的代碼,讀起來讓你頭大了,實現某個功能時不知道該用哪個函數了,想用哪個函數翻半天找不到了,只能用到一個小功能卻要引入整個類的時候,這說明類的行數、函數、屬性過多了。
類的職責是否設計地越單一越好?
為了滿足單一職責,是不是把類拆得越細就越好呢?答案是否定的。通過一個例子來解釋下。 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
類,那就會導致序列化和反序列化不匹配,程序運行出錯,拆分之后,代碼的可維護性變差了。
實際上,不管是應用設計原則還是設計模式,最終的目的還是提高代碼的可讀性、可擴展性、可維護性、復用性等。我們在考慮應用某一設計原則是否合理的時候,也可以以此作為最終的考量標準。
回顧
1.如何理解單一職責原則(SRP)?
一個類只負責完成一個職責或功能。不要設計大而全的類,要設計粒度小、功能單一的類。單一職責原則是為了提高代碼的高內聚、低耦合,提高代碼的復用性、可讀性、可維護性。
2.如何判斷類的職責是否足夠單一?
不同的應用場景、不同階段的需求、不同的業務背景,對同一個類的職責是否單一,可能會有不同的判定結果。實際上,一些側面的指標更具有指導意義和可執行性,比如,出現下面這些情況,就有可能說明這個類的設計不滿足單一職責原則:
- 類中的代碼行數、函數、屬性過多。
- 類依賴的其他類過多,或者依賴類的其他類過多。
- 私有方法過多。
- 比較難給一個類起一個合適的名字。
- 類中的大量方法都是集中操作類中的幾個屬性。
3.類的職責是否設計的越細越好?
單一職責原則通過避免設計大而全的類,避免將不相關的功能聚合在一起,來提高類的高內聚性。同時,類職責單一,類依賴和被依賴的其他類也會變少,減少了代碼的耦合性,以此來實現代碼的高內聚、低耦合。但是,如果拆分地過細,實際上會適得其反,反倒會降低代碼的內聚性,也會影響代碼的可維護性。