文章目錄
- 定義
- 案例分析
- 重復的假象
- 代碼合并
- 解決方案
- 小結
定義
SRP是SOLID五大設計原則中最容易被誤解的一個。也許是名字的原因,很多程序員根據SRP這個名字想當然地認為這個原則就是指:每個模塊都應該只做一件事。
在歷史上,我們曾經這樣描述SRP這一設計原則:
任何一個軟件模塊都應該有且僅有一個被修改的原因。
在現實環境中,軟件系統為了滿足用戶和所有者的要求,必然要經常做出這樣那樣的修改。而該系統的用戶或者所有者就是該設計原則中所指的“被修改的原因”。所以,我們也可以這樣描述SRP:
任何一個軟件模塊都應該只對一個用戶(User)或系統利益相關者(Stakeholder)負責。
不過,這里的“用戶”和“系統利益相關者”在用詞上也并不完全準確,它們很有可能指的是一個或多個用戶和利益相關者,只要這些人希望對系統進行的變更是相似的,就可以歸為一類——一個或多個有共同需求的人。在這里,我們將其稱為行為者(actor)。
所以,對于SRP的最終描述就變成了:
任何一個軟件模塊都應該只對某一類行為者負責。
案例分析
重復的假象
某個工資管理程序中的Employee類有三個函數calculatePay()、reportHours()和save()
如上圖所示這個類的三個函數分別對應的是三類非常不同的行為者,違反了SRP設計原則。
- calculatePay()函數是由財務部門制定的,他們負責向CFO匯報。
- reportHours()函數是由人力資源部門制定并使用的,他們負責向COO匯報。
- save()函數是由DBA制定的,他們負責向CTO匯報。
這三個函數被放在同一個源代碼文件,即同一個Employee類中,程序員這樣做實際上就等于使三類行為者的行為耦合在了一起,這有可能會導致CFO團隊的命令影響到COO團隊所依賴的功能。
例如,calculatePay()函數和reportHours()函數使用同樣的邏輯來計算正常工作時數。程序員為了避免重復編碼,通常會將該算法單獨實現為一個名為regularHours()的函數:
接下來,假設CFO團隊需要修改正常工作時數的計算方法,而COO帶領的HR團隊不需要這個修改,因為他們對數據的用法是不同的。這時候,負責這項修改的程序員會注意到calculatePay()函數調用了regularHours()函數,但可能不會注意到該函數會同時被reportHours()調用。
這類問題發生的根源就是因為我們將不同行為者所依賴的代碼強湊到了一起。對此,SRP強調這類代碼一定要被分開。
代碼合并
一個擁有很多函數的源代碼文件必然會經歷很多次代碼合并,該文件中的這些函數分別服務不同行為者的情況就更常見了。
例如,CTO團隊的DBA決定要對Employee數據庫表結構進行簡單修改。與此同時,COO團隊的HR需要修改工作時數報表的格式。
這樣一來,就很可能出現兩個來自不同團隊的程序員分別對Employee類進行修改的情況。不出意外的話,他們各自的修改一定會互相沖突,這就必須要進行代碼合并。
在這個例子中,這次代碼合并不僅有可能讓CTO和COO要求的功能出錯,甚至連CFO原本正常的功能也可能受到影響。
多人為了不同的目的修改了同一份源代碼,這很容易造成問題的產生。
解決方案
最簡單直接的辦法是將數據與函數分離,設計三個類共同使用一個不包括函數的、十分簡單的EmployeeData類,每個類只包含與之相關的函數代碼,互相不可見,這樣就不存在互相依賴的情況了。
這種解決方案的壞處在于:程序員現在需要在程序里處理三個類。另一種解決辦法是使用Facade設計模式:
這樣一來,EmployeeFacade類所需要的代碼量就很少了,它僅僅包含了初始化和調用三個具體實現類的函數。
我們也可以選擇將最重要的函數保留在Employee類中,同時用這個類來調用其他沒那么重要的函數:
總而言之,上面的每一個類都分別容納了一組作用于相同作用域的函數,而在該作用域之外,它們各自的私有函數是互相不可見的。
小結
單一職責原則主要討論的是函數和類之間的關系—但是它在兩個討論層面上會以不同的形式出現。在組件層面,我們可以將其稱為共同閉包原則(Common Closure Principle),在軟件架構層面,它則是用于奠定架構邊界的變更軸心(Axis ofChange)。
參考內容來源于:《架構整潔之道》