設計原則之單一職責原則(SRP)

簡介

單一職責原則是最重要的設計原則,也是最抽象的設計原則。小到函數,大到平臺的設計,均可以使用單一職責原則來指導。也正由於它的抽象性,沒有一個統一的規則,不一樣的人即便是設計同一個功能,所劃分的函數、類也都是不相同的。java

定義

單一職責原則,英文名稱 Single Responsibility Principle,意爲每個模塊、類、函數應當只具有一個職責,也即只有一個功能。按照馬丁大叔的說法:「一個類的改變只有一個理由」。編程

這個原則只給了咱們一個方向,就跟「聽過不少道理依然過很差這一輩子」中的道理同樣,爲何依然過很差?由於道理僅僅是一個道理而不具有可操做性,沒有辦法按照步驟一二三來獲得想要的結果。session

單一不須要解釋,關鍵是職責,一個函數、接口、類、模塊要幹多少活纔算是職責單一?多大的粒度是合適的呢?函數

按照我現階段的知識水平,單一職責原則背後隱去的關鍵概念是抽象,函數、接口、類須要符合本身所在的抽象層次,在其自身所在的層面上內聚成領域,這就是本身的職責。ui

實踐

需求:作一個登陸功能,要求有過濾黑名單,登陸成功後發送短信、郵件等功能。編碼

注:僅示意spa

public class LoginManager {
    public String login(String userId, String password) {
        List<String> blacklist = blacklistService.findByUserId(userId);
        if(CollectionUtils.isNotEmpty) {
            return "user blocked";
        }
        
        User user = userService.findByUserId(userId);
        if (user == null) {
            return "user not exists";
        }
        
        String passwordMd5 = Md5Utils.md5(password);
        if (!passwordMd5.equals(user.getPassword()) {
            return "user login failed";
        }
        
        String uuid = UUIDUtils.getUUID();
        cacheService.set(uuid, userId);
        setCookie("sessionId", uuid);
        
        // mail related logic
        String mailContent = user.getUserName + "! Welcome back. From mail."
        mailService.send(user.getMail(), mailContent);
        
        // msg related logic
        String smsContent = user.getUserName + "! Welcome back. From sms."
        smsService.send(user.getPhone(), smsContent);
        
        return "success";
    }
}

函數

這個功能從函數名來看,並無違反單一職責的原則,登陸就是須要作這麼多的事。可是從編碼實現來講,已經違反了SRP。登陸包含的職責有過濾、校驗,可是過濾、校驗的具體細節並不在登陸函數的職責範圍內,據此重構登陸函數.net

public class LoginManager {
    public String login(String userId, String password) {
        Pair<Boolean, String> check = loginCheck(userId, password);
        if (!check.left()){
            return check.right();
        }
        
        saveUserSesssion(userId);
        afterLogined(userId);
        
        return "success";
    }
    
    private Pair<Boolean, String> loginCheck(String userId, String password) {
        
        Pair<Boolean. String> beforeCheck = loginBeforeCheck(userId);
        if(!before.left()){
            return beforeCheck;
        }
        
        return userCheck(userId, password);
    }
    
    private Pair<Boolean, String> loginBeforeCheck(String userId){
        List<String> blacklist = blacklistService.findByUserId(userId);
        if(CollectionUtils.isNotEmpty) {
            return Pair.of(false, "user blocked");
        }
        
        return Pair.of(true, "");
    }
    
    
    private Pair<Boolean, String> userCheck(String userId, String password){
        User user = userService.findByUserId(userId);
        if (user == null) {
            return Pair.of(false, "user not exists");
        }
        
        String passwordMd5 = Md5Utils.md5(password);
        if (!passwordMd5.equals(user.getPassword()) {
            return Pair.of(false, "user login failed");
        }
        
        return Pair.of(true, "");
    }
    
    private void saveUserSesssion(String userId){
        String uuid = UUIDUtils.getUUID();
        cacheService.set(uuid, userId);
        setCookie("sessionId", uuid);
    }
    
    private void afterLogined(User user) {
        User user = userService.findByUserId(userId);
        sendMail(user);
        sendSms(user);
    }
    
    private void sendMail(User user) {
        // mail related logic
        String mailContent = user.getUserName + "! Welcome back. From mail."
        mailService.send(user.getMail(), mailContent);
    }
    
    private void sendSms(User user) {
        // msg related logic
        String smsContent = user.getUserName + "! Welcome back. From sms."
        smsService.send(user.getPhone(), smsContent);
    }
}

重構完成後,若是須要增長過濾條件,則只須要修改loginBeforeCheck 函數,若是須要增長登陸後功能,則只須要修改 afterLogined 函數,每一個函數都只有一個修改的理由,也即符合 SRP 原則。設計

類與接口

當咱們將功能從函數的粒度重構以後,每一個函數只負責了本身的部分,已經符合了 SRP 原則,可是從類的角度來看,登陸類承擔了太多的功能。增長校驗規則須要修改登陸類,增長登陸後的功能也須要修改登陸類,所以類也須要按照 SRP 的原則來進行重構。code

在思考函數重構的過程當中,咱們已經對如何劃分類有了思考。校驗能夠抽出來,登陸後發短信、郵件也能夠抽出來,這樣登陸類就符合了本身的名稱:僅關心登陸的細節。

public interface LoginCheckService {
    public Pair<Boolean, String> check(String userId, String password);
}

public interface LoginListener{
    public void afterLogin(LoginEvent event);
}

光有這兩個類多是不夠的,咱們還須要定義一個登陸事件LoginEvent, 事件註冊中心 Registry, 事件分發Dispatcher, LoginCheckService 是有前後順序的要求的,能夠實現一個 Order 接口,也能夠拆成兩個接口,同一個接口的實現沒有順序要求。這徹底取決於咱們系統功能的規模,和咱們對職責的認識。

模塊

雖然登陸功能通常不會作成模塊,但咱們能夠站在模塊的角度來思考。模塊是你們共用的依賴,對於可擴展性、可維護性要求會比一個功能要求更高。在 類和接口 小節的描述中,事件、註冊中心等在功能層面上可能不是必須的,在模塊層面上,這些是必須的。沒有事件,使用方就不知道如何響應;沒有註冊中心,使用方就不知道如何定製化;沒有事件分發,模塊就沒法將事件通知到使用方。

缺點

SRP 能夠很好的將咱們的功能、應用解耦,可是應該看到 SRP 存在的缺點,才能夠更好的權衡本身的設計。

  • 不明確。職責的含義沒有明確界定,如何界定是門藝術。
  • 無評判標準。界定出來的職責是好是壞?沒有標準,只有經驗。
  • 易濫用。職責劃分到最後可能就是一個接口一個方法,看似符合 SRP,實則是 SRP 的濫用。
  • 函數、接口、類爆炸。
  • 知識比較支離。信息分佈在各個類中,不如放在一塊兒集中。

後記

要作一個符合SRP 原則的設計是很困難的,須要咱們在實踐中總結經驗。對一個領域有了充分的瞭解,咱們才能更加遊刃有餘的應用SRP 原則。同時不要濫用 SRP原則,編程是門藝術,設計更是一門藝術。

我的

我的公衆號

個人博客即將同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1nov2ydr8zly

相關文章
相關標籤/搜索