六大原則之「單一職責原則(SRP)「筆記

1.單一職責原則,(Single Responsibility Principle).

定義:There should never be more than one reason for a class to change,應該有且僅有一個緣由引發類的變動。java

職責:業務邏輯,或者對象可以承擔的責任,並以某種行爲方式來執行。編程

 

2.理解

該原則提出了對對象職責的一種理想指望。對象不該該承擔太多職責,正如人不該該一心分爲二用。惟有專一,才能保證對象的高內聚;惟有單一,才能保證對象的細粒度。對象的高內聚與細粒度有利於對象的重用。架構

高內聚,低耦合( 嚴於律己,寬以待人)spa

高內聚是說模塊內部要高度聚合,低耦合是說模塊與模塊之間的藕合度要儘可能低。前者是說模塊內部的關係,後者是說模塊與模塊間的關係。設計

粗粒度:表示類別級,即僅考慮對象的類別(the   type   of   object),不考慮對象的某個特定實例。好比,用戶管理中,建立、刪除,對全部的用戶都一視同仁,並不區分操做的具體對象實例。  rest

細粒度:表示實例級,即須要考慮具體對象的實例(the   instance   of   object),固然,細粒度是在考慮粗粒度的對象類別以後纔再考慮特定實例。好比,合同管理中,列表、刪除,須要區分該合同實例是否爲當前用戶所建立。code

通常權限的設計是解決了粗粒度的問題,由於這部分具備通用性,而細粒度能夠當作業務部分,由於其具備不肯定性。對象

一個龐大的對象承擔了太多的職責,當客戶端須要該對象的某一個職責時,就不得不將全部的職責都包含進來,從而形成冗餘代碼或代碼的浪費。這實際上保證了DRY原則,即"不要重複你本身(Don't Repeat Yourself)",確保系統中的每項知識或功能都只在一個地方描述或實現。 接口

單一職責原則還有利於對象的穩定。所謂"職責",就是對象可以承擔的責任,並以某種行爲方式來執行。對象的職責老是要提供給其餘對象調用,從而造成對象與對象的協做,由此產生對象之間的依賴關係。對象的職責越少,則對象之間的依賴關係就越少,耦合度減弱,受其餘對象的約束與牽制就越少,從而保證了系統的可擴展性。ip

單一職責原則並非極端地要求咱們只能爲對象定義一個職責,而是利用極端的表述方式重點強調,在定義對象職責時,必須考慮職責與對象之間的所屬關係。職責必須恰如其分地表現對象的行爲,而不至於破壞和諧與平衡的美感,甚至格格不入。換言之,該原則描述的單一職責指的是公開在外的與該對象緊密相關的一組職責。

例如,在媒體播放器中,能夠在MediaPlayer類中定義一組與媒體播放相關的方法,如Open()、Play()、Stop()等。這些方法從職責的角度來說,是內聚的,徹底符合單一職責原則中"專一於作一件事"的要求。若是需求發生擴充,須要咱們提供上傳、下載媒體文件的功能。那麼在設計時,就應該定義一個新類如MediaTransfer,由它來承擔這一職責;而不是爲了方便,草率地將其添加到MediaPlayer類中。

單一職責適用於接口、類、同時也適用於方法。方法的粒度也不宜過粗。

 

3.問題由來

類T負責兩個不事的職責:職責P1、職責P2。當因爲職責P1需求發生改變而須要修改類T時,有可能會致使原來運行的職責P2功能發生故障。解決方法:分別創建兩個類完成對應的功能。

 

4.好處:

  1. 類的複雜性下降,實現什麼職責都有清晰明確的定義;
  2. 可讀性提升,複雜性下降,那固然可讀性提升了;
  3. 可維護性提升,那固然了,可讀性提升,那固然更容易維護了;
  4. 變動引發的風險下降,變動是必不可少的,接口的單一職責作的好的話,一個接口修改只對相應的實現類有影響,與其餘的接口無影響,這個是對項目有很是大的幫助。

5.難點

5.1 職責劃分無量化標準:學究理論仍是工程應用?後者時,要考慮可變因素與不可變因素,以及相關的收益成本比率等。

5.2 單一職責妥協:項目中單一職責原則不多得以體現,或者很是難(囿[yòu]於國內技術人員的地位、話語權、項目中的環境、工做量、人員的技術水平、硬件資源等,最終的結果就是經常違背單一職責原則)。

 

6.實踐建議

6.1 接口必定要作到SRP,類的設計儘可能作到只有一個緣由引發變化。

6.2 妥協原則:

A.只有邏輯足夠簡單,才能夠在代碼級別上違背SRP;
B.只有類中方法數量足夠少,才能夠在方法級別上違背SRP;
C.實際應用中的類都要複雜的多,一旦發生職責擴散而須要修改類時,除非這個類自己很是簡單,不然仍是要遵循SRP。

 

7.範例

7.1 職責分明示例(Role-Based Access Control, 基於角色的訪問控制)(屬性與行爲分離) 

項目中經常使用到 用戶、機構、角色管理這些模塊,基本上使用的都是RBAC模型(經過分配和取消角色來完成用戶權限的授予與取消,使動做主體(用戶)與資源的行爲(權限)分離)。 但上述接口設計得有問題,用戶的屬性與用戶的行爲沒有分開。

RBAC概念

摘自百度百科:

    基於角色的權限訪問控制(Role-Based Access Control)做爲傳統訪問控制(自主訪問,強制訪問)的有前景的代替受到普遍的關注。在RBAC中,權限與角色相關聯,用戶經過成爲適當角色的成員而獲得這些角色的權限。這就極大地簡化了權限的管理。在一個組織中,角色是爲了完成各類工做而創造,用戶則依據它的責任和資格來被指派相應的角色,用戶能夠很容易地從一個角色被指派到另外一個角色。角色可依新的需求和系統的合併而賦予新的權限,而權限也可根據須要而從某角色中回收。角色與角色的關係能夠創建起來以囊括更普遍的客觀狀況。

應將其拆分爲兩個接口,IUserBO負責用戶的屬性,也即收集和反饋用戶的屬性信息;IUserBiz負責用戶的行爲,完成用戶信息的維護與變動。

代碼清單1-1 分清職責後的代碼示例

.......  
IUserBiz userInfo = new UserInfo();  
//我要賦值了,我就認爲它是一個純粹的BO  
IUserBO userBO = (IUserBO)userInfo;  
userBO.setPassword("abc");  
//我要執行動做了,我就認爲是一個業務邏輯類  
IUserBiz userBiz = (IUserBiz)userInfo;  
userBiz.deleteUser();  
.......

實際上,在項目中,更傾向於使用以下的結構圖:

7.2 職責分明的電話類圖(行爲與行爲相分離)

舉個電話的例子,電話通話的時候有4個過程發生:拔號、通話、迴應、掛機,寫一個接口,其類圖以下:

public interface IPhone {  
    //撥通電話  
    public void dial(String phoneNumber);  
    //通話  
    public void chat(Object o);  
    //迴應,只有本身說話而沒有迴應,那算啥?!  
    public void answer(Object o);  
    //通話完畢,掛電話  
    public void huangup();  
}

IPhone這個接口包含了兩個職責:一個是協議管理,由dial()與 hangup()兩個方法實現;一個是數據傳輸,由chat()與 answer()實現。

考慮:

A.協議接通的變化會引發這個接口或實現類的變化嗎? 會的!  數據傳輸(電話不只僅通話,還能夠上網)的變化也會引發其變化!這兩個緣由都引發了類的變化。B.電話拔通還用管使用什麼協議嗎?電話鏈接後還須要關心傳遞什麼數據嗎?

都不,即這兩個職責的變化不相互影響,那就考慮拆分紅兩個接口,類圖以下:

這個類圖略有些複雜,一個手機類須要把兩個對象組合在一塊兒才能用。組合是一種強耦合關係,還不如使用接口實現的方式呢。修改以下:

7.3 職責分明到接口

修改用戶信息方法 ----〉 職責分明的方法

上述寫法很糟(職責不清晰,不單一),改了吧。。。

7.4 分層架構模式(一個較大的單一職責示例)

分層架構模式實際上也體現了這一原則,它將整個系統按照職責的內聚性分爲不一樣的層,層內的模塊與類具備宏觀的內聚性,它們關注的事情應該是一致的。例如,領域邏輯層就主要關注系統的業務邏輯與業務流程,而數據的持久化與訪問則交由數據訪問層來負責。以訂單的管理爲例,咱們在領域邏輯層中定義以下的類OrderManager:

public class OrderManager    
{    
    private IOrderRepository repository = RepositoryFactory.createOrderRepository();    
    public void place(Order order)    
    {    
        if (order.IsValid())    
        {    
            repository.add(order);    
        }    
        else    
        {    
      throw new InvalidOperationException("Order can't be placed. ");    
        }    
    }    
    public void cancel(Order order)    
    {    
        if (order.isValid() && order.canCancel(DateTime.now()))    
        {    
            repository.remove(order);    
        }    
        else    
        {    
            throw new InvalidOperationException("Order can't be canceled. ");    
        }    
    }    
}    
public static class RepositoryFactory    
{    
     public static IOrderRepository createOrderRepository()     
     {    
          return new OrderRepository();    
     }    
}

OrderManager類的實現體現了單一職責原則的思想。

  • 首先,OrderManager類中的place()和cancel()方法均屬於訂單管理的業務邏輯,與領域邏輯層關注的事情是一致的。
  • 在這兩個方法的實現中,咱們須要檢驗訂單的正確性(檢驗訂單是否包含了必要的信息,如聯繫人、聯繫地址與聯繫電話),以及判斷當前時間是否在容許取消訂單的時間範圍內。雖然它們仍然屬於訂單處理的業務邏輯,但擁有這些檢查信息的是Order對象,而不是OrderManager,即Order對象是檢查訂單的信息專家 。所以,isValid()和canCancel()方法應該被定義在Order類中。
  • 至於添加和移除訂單的操做,雖然保證了下訂單和取消訂單的業務邏輯實現,但其實現卻屬於數據訪問層的範疇,於是該職責被委派給了OrderRepository類 。
  • 至於RepositoryFactory類,則是負責建立OrderRepository對象的工廠類。

這些類的職責以及協做關係如圖2-4所示。

將數據訪問的邏輯從領域對象中分離出去是有道理的,由於數據訪問邏輯的變化方向與訂單業務邏輯的變化方向是不一致的,引發職責發生變化的緣由也不相同。這也是單一職責原則的核心思想。遵循該原則,就可以有效地分離對象的變與不變,將變化的職責以抽象的方式獨立於原對象以外,原對象就更加穩定。Martin Fowler認爲,設計一個模型時應使該模型中最頻繁修改的部分所影響的類型數量達到最少。咱們對訪問Order數據表的邏輯進行了封裝與抽象,以隔離數據訪問邏輯的變化,即便數據訪問邏輯發生變化,它影響到的只是OrderRepository類而已。

7.5  妥協示例(項目中常見的單一職責違背可接受示例)

所謂職責擴散,就是由於某種緣由,職責P被分化爲粒度更細的職責P1和P2.

比職:類T只負責一個職責P,這樣設計是符合SRP的。後來因爲某種緣由,須要將職責P細分爲粒度更細的P1與P2,這時若是要遵循SRP,須要將類T也分解爲兩個類T1和T2,分別負責P1、P2這兩個職責。可是在程序已經寫好的狀況下,這樣作簡直太費時間了。因此,簡單的修改類T,用它來負責兩個職責是一個比較不錯的選擇,雖然這樣作有悖於SRP。

如,用一個類描述動物呼吸這個場景:

class Animal{  
    public void breathe(String animal){  
        System.out.println(animal+"呼吸空氣");  
    }  
}  
public class Client{  
    public static void main(String[] args){  
        Animal animal = new Animal();  
        animal.breathe("羊");  
    }  
}  
運行結果:  
羊呼吸空氣

程序上線後,發現問題了,並不全部動物都呼吸空氣的,如魚是呼吸水的。 修改時如若遵循SRP,則需將Animal類細分爲陸生動物類Terrestrial ,水生動物 Aquatic,代碼以下:[修改方式一]

class Animal{  
    public void breathe(String animal){  
        if("魚".equals(animal)){  
            System.out.println(animal+"呼吸水");  
        }else{  
            System.out.println(animal+"呼吸空氣");  
        }  
    }  
}  
public class Client{  
    public static void main(String[] args){  
        Animal animal = new Animal();  
        animal.breathe("羊");  
        animal.breathe("魚");  
  
    }  
}  
運行結果:  
羊呼吸空氣  
魚呼吸水

能夠看到,這種修改方式要簡單得多。可是卻存在着隱患:有一天須要將魚分爲呼吸淡水和海水的魚,則又要修改Animal類的breathe方法,而對原有的代碼修改會對調用「羊」等相關功能帶來風險,也許某一天,你會發現程序運行的結果變爲「羊呼吸水」了。這種修改方式直接在代碼級別上違背了SRP,雖然修改起來最簡單,但隱患卻最大。還有一種修改方式:[修改方式三]

class Animal{  
    public void breathe(String animal){  
        System.out.println(animal+"呼吸水");  
    }  
    public void breathe2(String animal){  
        System.out.println(animal+"呼吸空氣");  
    }  
}  
   
public class Client{  
    public static void main(String[] args){  
        Animal animal = new Animal();  
        animal.breathe("羊");  
        animal.breathe2("魚");  
    }  
}  
   
運行結果:  
    羊呼吸空氣  
    魚呼吸水

能夠看出,這種修改沒有改動原來的方法,而是在類中添加了一個方法,這樣雖然也違背了SRP,但在方法級別上倒是符合SRP的,由於它並無改動原來方法的代碼。這三種方式各有優缺點,那麼在實際編程中,採用哪種呢?這須要根據實際狀況而定:建議:
A.只有邏輯足夠簡單,才能夠在代碼級別上違背SRP;
B.只有類中方法數量足夠少,才能夠在方法級別上違背SRP;

例如本文所舉的這個例子,它太簡單,它只有一個方法,因此,不管在代碼仍是在方法級別違背SRP,都不會形成太大的影響。實際應用中的類都要複雜的多,一旦發生職責擴散而須要修改類時,除非這個類自己很是簡單,不然仍是要遵循SRP。

相關文章
相關標籤/搜索