Single Responsibility Principle (SRP) - OO設計的單一職責原則
概要
There should never be more than one reason for a class to change.
永遠不要讓一個類存在多個改變的理由。
換句話說,如果一個類需要改變,改變它的理由永遠隻有一個。如果存在多個改變它的理由,就需要重新設計該類。
SRP(Single Responsibility Principle)原則的核心含意是:隻能讓一個類有且僅有一個職責。這也是單一職責原則的命名含義。
為什麼一個類不能有多于一個以上的職責呢?
如果一個類具有一個以上的職責,那麼就會有多個不同的原因引起該類變化,而這種變化将影響到該類不同職責的使用者(不同使用者):
1,一方面,如果一個職責使用了外部類庫,則使用另外一個職責的使用者卻也不得不包含這個未被使用的外部類庫。
2,另一方面,某個使用者由于某個原因需要修改其中一個職責,另外一個職責的使用者也将受到影響,他将不得不重新編譯和配置。
這違反了設計的開閉原則,也不是我們所期望的。
職責的劃分
既然一個類不能有多個職責,那麼怎麼劃分職責呢?
Robert.C Martin給出了一個著名的定義:所謂一個類的一個職責是指引起該類變化的一個原因。
If you can think of more than one motive for changing a class, then that class has more than one responsibility.
如果你能想到一個類存在多個使其改變的原因,那麼這個類就存在多個職責。
Single Responsibility Principle (SRP)的原文裡舉了一個Modem的例子來說明怎麼樣進行職責的劃分,這裡我們也沿用這個例子來說明一下:
SRP違反例:
Modem.java
interface Modem {
public void dial(String pno); //撥号
public void hangup(); //挂斷
public void send(char c); //發送資料
public char recv(); //接收資料
}
咋一看,這是一個沒有任何問題的接口設計。但事實上,這個接口包含了2個職責:第一個是連接配接管理(dial, hangup);另一個是資料通信(send, recv)。很多情況下,這2個職責沒有任何共通的部分,它們因為不同的理由而改變,被不同部分的程式調用。
是以它違反了SRP原則。
下面的類圖将它的2個不同職責分成2個不同的接口,這樣至少可以讓用戶端應用程式使用具有單一職責的接口:
讓ModemImplementation實作這兩個接口。我們注意到,ModemImplementation又組合了2個職責,這不是我們希望的,但有時這又是必須的。通常由于某些原因,迫使我們不得不綁定多個職責到一個類中,但我們至少可以通過接口的分割來分離應用程式關心的概念。
事實上,這個例子一個更好的設計應該是這樣的,如圖:
小結
面向對象設計五大原則的了解,他們分别是:SRP——單一職責原則;OCP——開放封閉原則;LSP——Liskov替換原則;DIP——依賴倒置原則;ISP——接口隔離原則。
1. 單一職責原則
在《靈活軟體開發》中,把“職責”定義為“變化的原因”,也就是說,就一個類而言,應該隻有一個引起它變化的原因。
在《UML與模式應用》一書中又提到,“職責”可以定義為“一個類或者類型的契約或者義務”,并把職責分成“知道”型職責和“做”型職責。
其中“做”型職責指的是一個對象自己完成某種動作或者和其他對象協同完成某個動作;“知道”型職責指的是一個對象需要了解哪些資訊。如果按照這種方式來定義“職責”的話,就與《敏》中對單一職責原則的定義不太相符了,是以還是了解為“變化的原因”比較恰當。
這個原則很好了解,但是既然談到了職責,就不妨再來看看GRASP——通用職責配置設定軟體模式(選自《UML與模式應用》)。按照我自己的看法來講,在下面這些職責配置設定模式中所涉及到的設計問題,是建立在現實世界抽象層次上的設計,從這個層次上進一步細化,才到了設計模式所說的針對接口程式設計和優先使用組合的兩大原則。
在這個層次上的抽象,一定要按照現實生活中的思維方法來進行,從我們人類考慮問題的角度出發,把解決現實問題的思維方式逐漸轉化成程式能夠了解的思維方式,絕不允許在這一步考慮程式代碼如何實作,那樣子的架構就是基于程式實作邏輯,而不是從解決問題的角度出發來實作業務邏輯(參考“面向對象的思維方法”)。
1) 專家模式。
在一個系統中可能存在成千上萬個職責,在面向對象的設計中,定義對象的互動時,就要做出如何将職責配置設定給類的設計選擇。
專家模式的解決方案就是:把一個職責配置設定給資訊專家——掌握了為履行職責所必需的資訊的類。
按照專家模式可以得到:一個對象所執行的操作通常是這個對象在現實世界中所代表的事物所執行的操作——這恰恰印證了我上面中的說法。
不過使用專家模式的時候,一定要仔細判斷什麼樣的職責是應該隻由一個類完成,什麼樣的職責應該由不同的類協作完成。舉一個小小的反例吧,在“思維方法”一文中,提供了一個收發郵件的例子用以說明作者的觀點,源碼如下所示:
public class JunkMail {
private String head;
private String body;
private String address;
public JunkMain() { // 預設的類構造器
this.head=...;
this.body=...;
public static boolean sendMail(String address) {
// 調用qmail,發送email
public static Collection listAllMail() {
// 通路資料庫,傳回一個郵件位址集合
作者在這裡就犯了一個職責配置設定的錯誤:上面的head、body和address都是屬于郵件自身的屬性,但是這個類卻有一個叫做sendMail的方法,錯誤就在這個方法這裡。在現實生活中,我們發送郵件的時候,是通過郵差來進行的,絕對沒有一封信會長上翅膀自己飛到收信人的手中,在程式中也是一樣,一封郵件絕不可能自己把自己發送出去,應該通過某個MailController之類的類來完成這個功能(之是以不命名為MailSender,是因為後面可能還要添加receiveMail等功能)。
2) 建立者模式
如果下列條件滿足的話,就把建立類A的執行個體的職責配置設定給類B的執行個體:
a) B聚集了A對象
b) B包含了A對象
c) B記錄了A對象的執行個體
d) B要經常使用A對象
e) 當A的執行個體被建立時,B具有要傳遞給A的初始化資料(也就是說B是建立A的資訊專家)
如果以上條件中不止一條滿足的話,那麼最好讓B聚集或者包含A
建立者模式用于指導對象執行個體建立任務的配置設定,基本目的就是找到一個與被建立對象有關聯關系的建立者。
3) 低耦合度
4) 高聚合度