設計模式六大原則(一)----單一職責原則

盛開的太陽發表於2021-06-03

設計模式六大原則之【單一職則原則】

一、什麼是單一職責原則

首先, 我們來看單一職責的定義.

單一職責原則,全稱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);
    }

可以看出單一職責的好處.

五、單一職責原則的優缺點

  • 類的複雜性降低: 一個類實現什麼職責都有清晰明確的定義了, 複雜性自然就降低了

  • 可讀性提高: 複雜性降低了,可讀性自然就提高了

  • 可維護性提高: 可讀性提高了,程式碼就更容易維護了

  • 變更引起的風險降低: 變更是必不可少的,如果介面的單一職責做得好,一個介面修改只對相應的實現類有影響,對其他的介面和類無影響,這對系統的擴充套件性、維護性都有非常大的幫助

相關文章