前言
常說c#、java是物件導向的語言,但我們平時都是在用程序導向的思維寫程式碼,實現業務邏輯像記流水賬一樣,大篇if else的判斷;對業務沒有抽象提煉、程式碼沒有分層。隨著需求變化、功能逐步擴充、業務邏輯逐漸複雜;程式碼越來越長、if else巢狀越來越多,程式碼會變成程式設計師都厭惡的"屎山"。這種程式碼後期維護成本非常高、牽一髮而動全身、改一處邏輯戰戰兢兢。假如我們完成開發任務交差,後期維護不關自己的事;但是長期做重複的CRUD、記流水賬對我們沒有好處。雖然專案不是自己的,但是時間是自己的,這樣幾年過去似乎沒有精進變化,長期下去年齡增大會逐漸喪失競爭力。
下面記錄今天開發的一個小功能,演示一步一步重構的過程。
需求
- 有一個智慧識別的api給使用者呼叫。角色有兩個:管理員、普通使用者。管理員不限次數呼叫,普通使用者每日限用五次。
簡單實現,只判斷如果是普通使用者就檢查次數,不滿足就返回提示:
if (service.isNormalUser() && service.freeNumUseUp()) {
return AjaxResult.error("普通使用者免費識別次數已使用完!");
}
// todo:呼叫識別介面
- 功能演變:普通使用者可充值後升級為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. 是否有普通使用者角色且滿足免費次數條件,否進入下一級;如果沒有下一級則檢查不透過。
重新設計
- 審批角色介面,主要兩個功能:a. 角色判斷(當前使用者是否為本角色),b. 是否檢查(審批)透過
public interface IAudit {
/**
* 角色判斷:是否為我的責任
*
* @return
*/
boolean isMyDuty();
/**
* 是否透過
*
* @return
*/
boolean auditPass();
/**
* 檢查(審批)意見:不透過時返回空字串
*
* @return
*/
String auditMessage();
}
- 審批角色抽象類,實現審批角色介面,並且是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();
}
}
- 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 "普通使用者免費識別次數已使用完";
}
}
- 審批責任鏈類。作用為新增審批人、審批返回結果。
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;
}
}
- 實現檢查
// 審批責任鏈中加入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());
}
總結
最終的實現程式碼簡潔明瞭,易維護、易擴充套件升級:
- 核心方法只有auditChain.add和auditChain.audit,一眼看去就能明白作用是加入審批人和實現審批。
- 如何擴充套件功能加入其它角色?建立新的角色類並繼承AbstractAudit,並加入到責任鏈中。不需要在原來的if中巢狀了。
- 現在的檢查是多個角色中有任意一個透過即可,轉換到審批場景就是多角色審批,其中一個角色審批透過即可。如果要需求改成多個角色全部審批透過才行呢?其實就是責任人鏈中or的關係改為and關係。 只需要修改AuditChain類的audit方法,將
chain.stream().anyMatch
改為chain.stream().allMatch
。anyMatch表示任意一個匹配,allMatch表示全部匹配。如果要在改造前的程式碼中要實現or到and的變化,原有程式碼幾乎要完全重寫。
學習交流: