程式碼精簡之路-責任鏈模式

chyun2011發表於2024-11-28

前言

常說c#、java是物件導向的語言,但我們平時都是在用程序導向的思維寫程式碼,實現業務邏輯像記流水賬一樣,大篇if else的判斷;對業務沒有抽象提煉、程式碼沒有分層。隨著需求變化、功能逐步擴充、業務邏輯逐漸複雜;程式碼越來越長、if else巢狀越來越多,程式碼會變成程式設計師都厭惡的"屎山"。這種程式碼後期維護成本非常高、牽一髮而動全身、改一處邏輯戰戰兢兢。假如我們完成開發任務交差,後期維護不關自己的事;但是長期做重複的CRUD、記流水賬對我們沒有好處。雖然專案不是自己的,但是時間是自己的,這樣幾年過去似乎沒有精進變化,長期下去年齡增大會逐漸喪失競爭力。

下面記錄今天開發的一個小功能,演示一步一步重構的過程。

需求

  1. 有一個智慧識別的api給使用者呼叫。角色有兩個:管理員、普通使用者。管理員不限次數呼叫,普通使用者每日限用五次。

簡單實現,只判斷如果是普通使用者就檢查次數,不滿足就返回提示:

if (service.isNormalUser() && service.freeNumUseUp()) {
    return AjaxResult.error("普通使用者免費識別次數已使用完!");
}

// todo:呼叫識別介面
  1. 功能演變:普通使用者可充值後升級為VIP使用者,VIP使用者在有效期內不限次數使用,過期以後降為普通使用者。
    增加VIP角色的檢查後:
if (service.isVipUser() && service.vipUserExpire()) {
    return AjaxResult.error("會員已到期!");
}
if (service.isNormalUser() && service.freeNumUseUp()) {
    return AjaxResult.error("普通使用者免費識別次數已使用完!");
}

// todo:呼叫識別介面

以上修改的問題:普通使用者充值以後,是增加一個VIP的角色而不是把原普通使用者角色更新為VIP角色。此時這個使用者有兩個角色,那麼上面的程式碼先判斷VIP角色是否到期是沒問題的,但是下面又判斷了是否為普通使用者就有問題了,因為他有兩個角色呀,VIP未到期時第2個條件也滿足了會給出不合理的提示。怎麼改,首先想到的是不是檢查VIP後就不檢查普通使用者了?於是修改為:

if (service.isVipUser()) {
    if (service.vipUserExpire()) {
        return AjaxResult.error("會員已到期!");
    }
} else {
    if (service.isNormalUser() && service.freeNumUseUp()) {
        return AjaxResult.error("普通使用者免費識別次數已使用完!");
    }
}

// todo:呼叫識別介面

以上仍然有問題,如果是VIP角色就不會檢查普通使用者角色了,可是按需求VIP到期以後他還具有普通使用者角色,可以在每天免費次數內使用。於是再改:

boolean dontPass = service.isVipUser() && service.vipUserExpire();
if (dontPass) {
    dontPass = service.isNormalUser() && service.freeNumUseUp();
    if (dontPass) {
        return AjaxResult.error("普通使用者免費識別次數已使用完!");
    } else {
        return AjaxResult.error("會員已到期!");
    }
}

// todo:呼叫識別介面

以上修改可以滿足VIP和普通使用者的檢查了,還差了管理員的判斷,還要再巢狀:

boolean dontPass = !service.isAdmin();
if (dontPass) {
    dontPass = service.isVipUser() && service.vipUserExpire();
    if (dontPass) {
        dontPass = service.isNormalUser() && service.freeNumUseUp();
        if (dontPass) {
            return AjaxResult.error("普通使用者免費識別次數已使用完!");
        } else {
            return AjaxResult.error("會員已到期!");
        }
    }
}

// todo:呼叫識別介面

終於滿足3個角色的檢查了,加了3層if判斷。以後再出現新的角色怎麼辦?如果功能交給同事來升級,原來的程式碼輕易不敢動只能再巢狀。

梳理以上需求,3個角色有任意一個透過就可以了。實際上檢查時可以按以下先後順序逐個過,最後一個不滿足才返回提示。

a. 是否有管理員角色,否進入下一級
b. 是否有VIP角色且未到期,否進入下一級
c. 是否有普通使用者角色且滿足免費次數條件,否進入下一級;如果沒有下一級則檢查不透過。

重新設計

  1. 審批角色介面,主要兩個功能:a. 角色判斷(當前使用者是否為本角色),b. 是否檢查(審批)透過
public interface IAudit {
    /**
     * 角色判斷:是否為我的責任
     *
     * @return
     */
    boolean isMyDuty();
    
    /**
     * 是否透過
     *
     * @return
     */
    boolean auditPass();
    
    /**
     * 檢查(審批)意見:不透過時返回空字串
     *
     * @return
     */
    String auditMessage();
}
  1. 審批角色抽象類,實現審批角色介面,並且是3個角色實現類的父類,充當審批角色介面和角色實現類的中間過度。作用是判斷檢查(審批)是否透過,這裡不大容易理解,實際3個角色的實現類分別實現介面就可以了,沒有這個中間過度也可以的。為什麼要加這個中間類?因為最終檢查是否透過要呼叫isMyDuty和auditPass兩個方法,這裡可以把這兩個方法的呼叫合併為一個方法,其實就是把判斷角色和角色的檢查條件統一在這個類而不是在3個實現類裡去分別寫了,為什麼?因為3個實現類要寫的判斷都是完全一樣的程式碼isMyDuty()&&auditPass(),作用就是本來要寫3行,現在只寫1行。看上去沒有必要?因為現在只有3個類呀,如果以後擴充套件到5個角色,5類那多了。還有,如果是功能修改呢,那就要6個類裡分別改了。每改一個類都需要針對這個類單獨測試。修改測試花時間多了,這裡只有一次修改測試。
public abstract class AbstractAudit implements IAudit {
    /**
     * 角色是否檢查透過
     *
     * @return
     */
    public boolean checkPass() {
        return isMyDuty() && auditPass();
    }
}
  1. 3個角色的實現類。
  • 管理員:
@Service
public class AdminAudit extends AbstractAudit {
    @Autowired
    private IdentifyService identifyService;
    
    @Override
    public boolean isMyDuty() {
        return identifyService.isAdmin();
    }
    
    @Override
    public boolean auditPass() {
        return true;
    }
    
    /**
     * 管理員是沒有限制的,所以沒有提示
     *
     * @return
     */
    @Override
    public String auditMessage() {
        return "";
    }
}
  • VIP使用者:
@Service
public class VipUserAudit extends AbstractAudit {
    @Autowired
    private IdentifyService identifyService;
    
    @Override
    public boolean isMyDuty() {
        return identifyService.isVipUser();
    }
    
    @Override
    public boolean auditPass() {
        return !identifyService.vipUserExpire();
    }
    
    /**
     * 這裡還需要最佳化,因為isMyDuty和auditPass可能被呼叫兩次,可以將isMyDuty、auditPass返回值存在臨時變數中
     *
     * @return
     */
    @Override
    public String auditMessage() {
        if (!isMyDuty()) {
            return "不是會員";
        } else if (!auditPass()) {
            return "會員過期";
        }
        return "";
    }
}
  • 普通使用者:
@Service
public class NormalUserAudit extends AbstractAudit {
    @Autowired
    private IdentifyService identifyService;
    
    @Override
    public boolean isMyDuty() {
        return identifyService.isNormalUser();
    }
    
    @Override
    public boolean auditPass() {
        return !identifyService.freeNumUseUp();
    }
    
    @Override
    public String auditMessage() {
        return "普通使用者免費識別次數已使用完";
    }
}
  1. 審批責任鏈類。作用為新增審批人、審批返回結果。
public class AuditChain {

    private List<AbstractAudit> chain = new ArrayList<>();

    /**
     * 新增審批人
     *
     * @param auditor
     */
    public void add(AbstractAudit auditor) {
        chain.add(auditor);
    }


    /**
     * 檢查/審批
     *
     * @return
     */
    public Result audit() {
        Result result = new Result();
        // 是否檢查透過
        boolean pass = chain.stream().anyMatch(a -> a.checkPass());
        result.setPass(pass);
        if (!pass) {
            String msg = chain.stream().map(c -> c.auditMessage()).filter(m -> Strings.isNotBlank(m)).collect(Collectors.joining(","));
            result.setMsg(msg);
        }
        return result;
    }


    @Data
    public class Result {
        private boolean pass;
        private String msg;
    }
}
  1. 實現檢查
// 審批責任鏈中加入3個角色,這裡用的Spring Boot開發,3個角色都是容器注入的,其它框架中手動建立例項

// 新增審批人角色
auditChain.add(adminAudit);
auditChain.add(vipUserAudit);
auditChain.add(normalUserAudit);

// 審批結果
AuditChain.Result auditResult = auditChain.audit();
if (!auditResult.isPass()) {
    return AjaxResult.error(auditResult.getMsg());
}

總結

最終的實現程式碼簡潔明瞭,易維護、易擴充套件升級:

  1. 核心方法只有auditChain.add和auditChain.audit,一眼看去就能明白作用是加入審批人和實現審批。
  2. 如何擴充套件功能加入其它角色?建立新的角色類並繼承AbstractAudit,並加入到責任鏈中。不需要在原來的if中巢狀了。
  3. 現在的檢查是多個角色中有任意一個透過即可,轉換到審批場景就是多角色審批,其中一個角色審批透過即可。如果要需求改成多個角色全部審批透過才行呢?其實就是責任人鏈中or的關係改為and關係。 只需要修改AuditChain類的audit方法,將chain.stream().anyMatch改為chain.stream().allMatch。anyMatch表示任意一個匹配,allMatch表示全部匹配。如果要在改造前的程式碼中要實現or到and的變化,原有程式碼幾乎要完全重寫。

學習交流:

相關文章