設計模式六大原則之【單一職則原則】
一、什麼是單一職責原則
首先, 我們來看單一職責的定義.
單一職責原則,全稱Single Responsibility Principle, 簡稱SRP.
A class should have only one reason to change
類發生更改的原因應該只有一個
就一個類而言,應該僅有一個引起它變化的原因。應該只有一個職責。如果一個類有一個以上的職責,這些職責就耦合在了一起。一個職責的變化可能會削弱或者抑制這個類完成其他職責的能力。這會導致脆弱的設計。當一個職責發生變化時,可能會影響其它的職責。另外,多個職責耦合在一起,會影響複用性。想要避免這種現象的發生,就要儘可能的遵守單一職責原則。
單一職責原則的核心就是解耦和增強內聚性。
二、為什麼要遵守單一職責原則?
通常 , 我們做事情都要知道為什麼要這麼做, 才回去做. 做的也有底氣, 那麼為什麼我們要使用單一職責原則呢?
1、提高類的可維護性和可讀寫性
一個類的職責少了,複雜度降低了,程式碼就少了,可讀性也就好了,可維護性自然就高了。
2、提高系統的可維護性
系統是由類組成的,每個類的可維護性高,相對來講整個系統的可維護性就高。當然,前提是系統的架構沒有問題。
3、降低變更的風險
一個類的職責越多,變更的可能性就越大,變更帶來的風險也就越大
如果在一個類中可能會有多個發生變化的東西,這樣的設計會帶來風險, 我們儘量保證只有一個可以變化,其他變化的就放在其他類中,這樣的好處就是 提高內聚,降低耦合。
三. 單一職責原則應用的範圍
單一職責原則適用的範圍有介面、方法、類。按大家的說法,介面和方法必須保證單一職責,類就不必保證,只要符合業務就行。
3.1 【方法層面】單一職責原則的應用
現在有一個場景, 需要修改使用者的使用者名稱和密碼. 就針對這個功能我們可以有多種實現.
第一種:
/**
* 操作的型別
*/
public enum OperateEnum {
UPDATE_USERNAME,
UPDATE_PASSWORD;
}
public interface UserOperate {
void updateUserInfo(OperateEnum type, UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void updateUserInfo(OperateEnum type, UserInfo userInfo) {
if (type == OperateEnum.UPDATE_PASSWORD) {
// 修改密碼
} else if(type == OperateEnum.UPDATE_USERNAME) {
// 修改使用者名稱
}
}
}
第二種方法:
public interface UserOperate {
void updateUserName(UserInfo userInfo);
void updateUserPassword(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate {
@Override
public void updateUserName(UserInfo userInfo) {
// 修改使用者名稱邏輯
}
@Override
public void updateUserPassword(UserInfo userInfo) {
// 修改密碼邏輯
}
}
來看看這兩種實現的區別:
第一種實現是根據操作型別進行區分, 不同型別執行不同的邏輯. 把修改使用者名稱和修改密碼這兩件事耦合在一起了. 如果客戶端在操作的時候傳錯了型別, 那麼就會發生錯誤.
第二種實現是我們推薦的實現方式. 修改使用者名稱和修改密碼邏輯分開. 各自執行各自的職責, 互不干擾. 功能清晰明瞭.
由此可見, 第二種設計是符合單一職責原則的. 這是在方法層面實現單一職責原則.
3.2 【介面層面】單一職責原則的應用
我們假設一個場景, 大家一起做家務, 張三掃地, 李四買菜. 李四買完菜回來還得做飯. 這個邏輯怎麼實現呢?
方式一
/**
* 做家務
*/
public interface HouseWork {
// 掃地
void sweepFloor();
// 購物
void shopping();
}
public class Zhangsan implements HouseWork{
@Override
public void sweepFloor() {
// 掃地
}
@Override
public void shopping() {
}
}
public class Lisi implements HouseWork{
@Override
public void sweepFloor() {
}
@Override
public void shopping() {
// 購物
}
}
首先定義了一個做家務的介面, 定義兩個方法掃地和買菜. 張三掃地, 就實現掃地介面. 李四買菜, 就實現買菜介面. 然後李四買完菜回來還要做飯, 於是就要在介面類中增加一個方法cooking. 張三和李四都重寫這個方法, 但只有李四有具體實現.
這樣設計本身就是不合理的.
首先: 張三隻掃地, 但是他需要重寫買菜方法, 李四不需要掃地, 但是李四也要重寫掃地方法.
第二: 這也不符合開閉原則. 增加一種型別做飯, 要修改3個類. 這樣當邏輯很複雜的時候, 很容易引起意外錯誤.
上面這種設計不符合單一職責原則, 修改一個地方, 影響了其他不需要修改的地方.
方法二
/**
* 做家務
*/
public interface Hoursework {
}
public interface Shopping extends Hoursework{
// 購物
void shopping();
}
public interface SweepFloor extends Hoursework{
// 掃地
void sweepFlooring();
}
public class Zhangsan implements SweepFloor{
@Override
public void sweepFlooring() {
// 張三掃地
}
}
public class Lisi implements Shopping{
@Override
public void shopping() {
// 李四購物
}
}
上面做家務不是定義成一個介面, 而是將掃地和做家務分開了. 張三掃地, 那麼張三就實現掃地的介面. 李四購物, 李四就實現購物的介面. 後面李四要增加一個功能做飯. 那麼就新增一個做飯介面, 這次只需要李四實現做飯介面就可以了.
public interface Cooking extends Hoursework{
void cooking();
}
public class Lisi implements Shopping, Cooking{
@Override
public void shopping() {
// 李四購物
}
@Override
public void cooking() {
// 李四做飯
}
}
如上, 我們看到張三沒有實現多餘的介面, 李四也沒有. 而且當新增功能的時候, 隻影響了李四, 並沒有影響張三.
這就是符合單一職責原則. 一個類只做一件事. 並且他的修改不會帶來其他的變化.
3.3 【類層面】單一職責原則的應用
從類的層面來講, 沒有辦法完全按照單一職責原來來拆分. 換種說法, 類的職責可大可小, 不想介面那樣可以很明確的按照單一職責原則拆分. 只要符合邏輯有道理即可.
比如, 我們在網站首頁可以註冊, 登入, 微信登入.註冊登入等操作. 我們通常的做法是:
public interface UserOperate {
void login(UserInfo userInfo);
void register(UserInfo userInfo);
void logout(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void login(UserInfo userInfo) {
// 使用者登入
}
@Override
public void register(UserInfo userInfo) {
// 使用者註冊
}
@Override
public void logout(UserInfo userInfo) {
// 使用者登出
}
}
那如果按照單一職責原則拆分, 也可以拆分為下面的形式
public interface Register {
void register();
}
public interface Login {
void login();
}
public interface Logout {
void logout();
}
public class RegisterImpl implements Register{
@Override
public void register() {
}
}
public class LoginImpl implements Login{
@Override
public void login() {
// 使用者登入
}
}
public class LogoutImpl implements Logout{
@Override
public void logout() {
}
}
像上面這樣寫可不可以呢? 其實也可以, 就是類很多. 如果登入、註冊、登出操作程式碼很多, 那麼可以這麼寫.
四、如何遵守單一職責原則
4.1 合理的職責分解
相同的職責放到一起,不同的職責分解到不同的介面和實現中去,這個是最容易也是最難運用的原則,關鍵還是要從業務出發,從需求出發,識別出同一種型別的職責。
例子:人的行為分析,包括了生活和工作等行為的分析,生活行為包括吃、跑、睡等行為,工作行為包括上下班,開會等行為,如下圖所示:
人類的行為分成了兩個介面:生活行為介面、工作行為介面,以及兩個實現類。如果都用一個實現類來承擔這兩個介面的職責,就會導致程式碼臃腫,不易維護,如果以後再加上其他行為,例如學習行為介面,將會產生變更風險(這裡還用到了組合模式)。
4.2 來看看簡單的程式碼實現
第一步: 定義一個行為介面
/**
* 人的行為
* 人的行為包括兩種: 生活行為, 工作行為
*/
public interface IBehavior {
}
這裡面定義了一個空的介面, 行為介面. 具體這個行為介面下面有哪些介面呢?有生活和工作兩方面的行為.
第二步: 定義生活和工作介面, 並且他們都是行為介面的子類
生活行為介面:
public interface LivingBehavior extends IBehavior{
/** 吃飯 */
void eat();
/** 跑步 */
void running();
/** 睡覺 */
void sleeping();
}
工作行為介面:
public interface WorkingBehavior extends IBehavior{
/** 上班 */
void goToWork();
/** 下班 */
void goOffWork();
/** 開會 */
void meeting();
}
第三步: 定義工作行為介面和生活行為介面的實現類
生活行為介面實現類:
public class LivingBehaviorImpl implements LivingBehavior{
@Override
public void eat() {
System.out.println("吃飯");
}
@Override
public void running() {
System.out.println("跑步");
}
@Override
public void sleeping() {
System.out.println("睡覺");
}
}
工作行為介面實現類:
public class WorkingBehaviorImpl implements WorkingBehavior{
@Override
public void goToWork() {
System.out.println("上班");
}
@Override
public void goOffWork() {
System.out.println("下班");
}
@Override
public void meeting() {
System.out.println("開會");
}
}
第四步: 行為組合呼叫.
行為介面定義好了. 接下來會定義一個行為集合. 不同的使用者擁有的行為是不一樣 , 有的使用者只用生活行為, 有的使用者既有生活行為又有工作行為.ewgni
我們並不知道具體使用者到底會有哪些行為, 所以,通常使用一個集合來接收使用者的行為. 使用者有哪些行為, 就往裡面新增哪些行為.
1. 行為組合介面BehaviorComposer
public interface BehaviorComposer {
void add(IBehavior behavior);
}
2. 行為組合介面實現類IBehaviorComposerImpl
public class IBehaviorComposerImpl implements BehaviorComposer {
private List<IBehavior> behaviors = new ArrayList<>();
@Override
public void add(IBehavior behavior) {
System.out.println("新增行為");
behaviors.add(behavior);
}
public void doSomeThing() {
behaviors.forEach(b->{
if(b instanceof LivingBehavior) {
LivingBehavior li = (LivingBehavior)b;
// 處理生活行為
} else if(b instanceof WorkingBehavior) {
WorkingBehavior wb = (WorkingBehavior) b;
// 處理工作行為
}
});
}
}
第五步: 客戶端呼叫
使用者在呼叫的時候, 根據實際情況呼叫就可以了, 比如下面的程式碼: 張三是全職媽媽, 只有生活行為, 李四是職場媽媽, 既有生活行為又有工作行為.
public static void main(String[] args) {
// 張三--全職媽媽
LivingBehavior zslivingBehavior = new LivingBehaviorImpl();
BehaviorComposer zsBehaviorComposer = new IBehaviorComposerImpl();
zsBehaviorComposer.add(zslivingBehavior);
// 李四--職場媽媽
LivingBehavior lsLivingBehavior = new LivingBehaviorImpl();
WorkingBehavior lsWorkingBehavior = new WorkingBehaviorImpl();
BehaviorComposer lsBehaviorComposer = new IBehaviorComposerImpl();
lsBehaviorComposer.add(lsLivingBehavior);
lsBehaviorComposer.add(lsWorkingBehavior);
}
可以看出單一職責的好處.
五、單一職責原則的優缺點
-
類的複雜性降低: 一個類實現什麼職責都有清晰明確的定義了, 複雜性自然就降低了
-
可讀性提高: 複雜性降低了,可讀性自然就提高了
-
可維護性提高: 可讀性提高了,程式碼就更容易維護了
-
變更引起的風險降低: 變更是必不可少的,如果介面的單一職責做得好,一個介面修改只對相應的實現類有影響,對其他的介面和類無影響,這對系統的擴充套件性、維護性都有非常大的幫助