設計原則之【單一職責原則】

Gopher大威發表於2022-02-26

設計原則是指導我們程式碼設計的一些經驗總結,也就是“心法”;物件導向就是我們的“武器”;設計模式就是“招式”。

以心法為基礎,以武器運用招式應對複雜的程式設計問題。

表妹旅個遊,修手機花了一半的時間

表妹:?哥啊,這次去旅遊,真是太掃興了。?

我:發生什麼事情啦?

表妹:手機攝像頭摔壞了,?光修手機就花了兩三天

我:這麼不小心,那你沒有帶相機專門用來拍照嘛?

表妹:沒有,有了智慧手機,還要帶個相機,實在太麻煩了?


你看,智慧手機雖然功能很多,給我們生活帶來了很大的便利,但是,一不小心把手機摔壞了,在修手機的過程中,就影響了你的通訊和音視訊等功能。

那如果是換成職責更加單一的單反呢?即使不小心把單反摔壞了,也不會影響你的通訊、音視訊等功能。

這不就是我們軟體開發中的,單一職責原則嘛。

就一個類而言,應該僅有一個引起它變化的原因。

字面意思是,一個類或者模組只負責完成一個職責(或者功能)。

注意:這個原則描述的物件有兩個,一個是類(class),一個是模組(module)。模組是更加抽象的概念,或者是看做比類更加粗粒度的程式碼塊,模組中包含多個類。

接下來,從“類”設計的角度來講解這個設計原則,對於模組也是相通的。

進一步解釋,如果這個類包含了兩個或多個業務不相干的功能,那麼這個類的職責就不夠單一,應該將它拆分成多個功能更加單一、粒度更細的類。

道理都懂,但是怎麼樣判斷類的職責是否足夠單一呢?

在真實的軟體開發中,對於一個類是否職責單一的判定,其實是很難拿捏的。

比如,在一個社交產品中有這麼一個類:

 1 public class UserInfo {
 2     private long userID 
 3     private String userName 
 4     private String email
 5     private String telephone
 6     private String province           // 地址資訊
 7     private String city               // 地址資訊
 8     private String region             // 地址資訊
 9     private String detailedAddress    // 地址資訊
10     // 省略其他屬性
11 }

對於這個類,是否滿足職責單一原則?有兩種不同的觀點,一種觀點認為,該類包含了跟使用者相關的資訊,滿足單一職責原則;另一種觀點認為,地址資訊在該類中佔比較重,應該拆分出來,UserInfo只保留除地址以外的資訊。

至於哪種觀點正確?實際上,應該結合實際的應用場景。如果該類中的地址資訊跟其他資訊一樣,只是用來展示,那麼UserInfo現在的設計就是合理的。但是如果現在這個社交產品發展得比較好,需要新增電商模組,那麼在電商物流中,就會用到使用者的地址資訊,為了讓電商模組更好的複用這部分資訊,並且易於後期維護,就需要將地址資訊從UserInfo中拆分處理,獨立成使用者地址資訊。

所以你看,不同的應用場景、不同階段的需求背景下,對一個類的職責是否單一的判定,可能是不一樣的。

儘管如此,我們也不應該過度設計,並不是類的職責越單一越好。

我們來看一個實現了簡單協議的序列化和反序列化功能的Serialization類。

 1 /*
 2 Protocol format:identifier-string;{key:value}
 3 For example:UEUEUE;{"name":"zhangsan"}
 4 */
 5 public class Serialization {
 6     private static final String IDENTIFIER_STRING = "UEUEUE;";
 7     private Gson gson;
 8     
 9     public Serialization {
10         this.gson = new Gson();
11     }
12     
13     public String serialize(Map<String, String> object) {
14         StringBuilder text = new StringBuilder();
15         text.append(IDEXTIFIER_STRING);
16         text.append(gson.toJson(object));
17         return text.toString();
18     }
19     
20     public Map<String, String> deserialize(String text) {
21         if (!text.startsWith(IDENTIFIER_STRING)) {
22             return Collections.emptyMap();
23         }
24         String gsonStr = text.substring(IDENTIFIER_STRING.length());
25         return gson.fromJson(gsonStr, Map.class);
26     }
27 }

 

如果我們想要讓其職責更加單一,可以將其進一步拆分,分別是隻負責序列化工作的Serializer類和只負責反序列化的Deserializer類。如下所示:

 1 public class Serializer {
 2     private static final String IDENTIFIER_STRING = "UEUEUE;";
 3     private Gson gson;
 4     
 5     public Serialization {
 6         this.gson = new Gson();
 7     }
 8     
 9     public String serialize(Map<String, String> object) {
10         StringBuilder text = new StringBuilder();
11         text.append(IDEXTIFIER_STRING);
12         text.append(gson.toJson(object));
13         return text.toString();
14     }
15 }
16 17 public class Deserializer {
18     private static final String IDENTIFIER_STRING = "UEUEUE;";
19     private Gson gson;
20     
21     public Serialization {
22         this.gson = new Gson();
23     }
24     
25     public Map<String, String> deserialize(String text) {
26         if (!text.startsWith(IDENTIFIER_STRING)) {
27             return Collections.emptyMap();
28         }
29         String gsonStr = text.substring(IDENTIFIER_STRING.length());
30         return gson.fromJson(gsonStr, Map.class);
31     }
32 }

 

雖然拆分之後,類的職責更加單一了,但也隨之帶來了新的問題。比如,修改了協議的格式,資料標識從“UEUEUE”改為“DFDFDF”;或者是序列化方式從JSON改為XML,那麼這兩個類都需要做相應的修改,程式碼的內聚性沒有拆分前的Serialization高了。如果我們只對Serializer類做了修改,而忘記修改Deserializer類的程式碼,那麼就會導致序列化和反序列化不匹配的問題。可見,拆分後,程式碼的可維護性變差了。

可見,評價一個類是否足夠職責單一,是沒有明確的,可以量化的標準的。實際上,一些側面的判斷指標更具有指導意義和可執行性,比如,出現下面這些情況,就有可能說明這個類的設計不滿足單一職責原則了:

  • 類中的程式碼行數、函式或屬性過多,會影響程式碼的可讀性和可維護性,我們就需要考慮對類進行拆分;

  • 類依賴的其他類過多,或者依賴類的其他類過多,不符合高內聚、低耦合的設計思想,我們就需要考慮對類進行拆分;

  • 私有方法過多,我們就要考慮是否將私有方法獨立到新的類中,設定public方法,供更多的類使用,從而提高程式碼的複用性;

  • 比較難給類起一個合適名字,很難用一個業務名詞概括,或者只能用一些籠統的Manager、Context之類的詞語來命名,這就說明類的職責定義得可能不夠清晰;

  • 類中大量的方法都是集中操作類中的幾個屬性,比如,在UserInfo例子中,如果一半的方法都是在操作address資訊,那就可以考慮將這幾個屬性和對應的方法拆分出來。

單一職責原則是實現高內聚、低耦合的指導方針,它是最簡單但又是最難運用的原則,需要設計人員發現類的不同職責並將其分離,而發現類的多重職責需要設計人員具有較強的分析設計能力和相關實踐經驗。

你看,單反功能職責單一,諾基亞也相對職責單一,它們就具有易維護、易擴充套件、易複用,甚至還有可讀性的特點。

總結

易維護

在不破壞原有程式碼設計、不引入新的bug 的情況下,能夠快速地修改或者新增程式碼。

比如iPhone在維修攝像頭的時候,如果一個手抖,就可能導致喇叭或麥克風被損壞,從而影響了通訊或音視訊功能,因為它們都是在一個積體電路板上的。

但是,單反在維修鏡頭的時候,就不存在這種情況,因為它的職責更單一。

易擴充套件

在不修改或少量修改原有程式碼的情況下,可以通過擴充套件的方式新增新的功能程式碼。

中秋節了,拿著iPhone想要拍個月亮發朋友圈,但是不管怎麼拍,效果都不好,看到朋友圈的月亮,又大又圓,心想:要是能換個好一點的鏡頭就好了。

這個時候,只見隔壁老鐵把單反裝在三腳架上,然後裝上長焦鏡頭,稍微對一下焦,“咔嚓”一聲,我湊過去一看,“哇,真的是又大又圓啊!”。

你看,單反可以根據不同的拍照場景,擴充套件不同的鏡頭。但是,iPhone儘管有不同的拍照模式,但還是比較有限的,你很難擴充套件鏡頭。

易複用

儘量較少重複程式碼的編寫,複用已有的程式碼。

你看,單反在換鏡頭的過程中,就是在複用機身。但是,我發現我早年買的iPhone 4s的拍照效果比較差,想要換一個拍照效果更好的。所以,花重金買了iPhone 13,結果發現,拍照效果確實好了,但是,通訊、音視訊娛樂等功能,就跟古董iPhone 4s重複了,這筆買賣好像有點不划算吧?

可讀性

通過良好的程式設計規範以及註釋,讓程式碼清晰易懂。

職責單一的程式碼,更加清晰易懂。就好比,我們都知道單反就是用來拍照的,諾基亞就是用來通訊的。但是,iPhone功能太多了,有很多功能是我們基本不會用到的,甚至不知道的。

好啦,每個設計原則是否應用得當,應該根據具體的業務場景,具體分析。

參考資料

《大話設計模式》

極客時間專欄《設計模式之美》

相關文章