参考资料:《重构改善既有代码的设计》第九章 简化条件表达式
9.1 分解条件表达式
程序中,复杂的条件逻辑是最常导致代码复杂度上升的地点之一。检查不同的条件分支,根据不同分支做不同的事情,很快就会得到一个相当长的函数。很长的函数使得可读性下降,带有复杂条件逻辑的代码,包括条件逻辑代码和执行功能的代码会告诉你实现了什么功能,但往往要仔细的阅读整段代码才能正确理解。这就表明代码的可读性是不高的。好的代码应该带有自解释性!!
大块头的代码可以将其分解为多个独立的函数,根据函数实现的功能为它们命名,再用这些函数重新实现功能。对于条件逻辑,将分支条件分解成新函数可以让条件逻辑更加清晰。
具体做法:将if 、else分支段落提炼出来,构成一个独立的函数
范例:假设我要计算购买某样商品的总价(总价=数量*单价),而这个商品在冬季和夏季的单价是不同的:
if (date.before (SUMMER_START) || date.after(SUMMER_END))
charge = quantity * _winterRate + _winterServiceCharge;
else charge = quantity * _summerRate;
我把每个分支的判断条件都提炼到一个独立函数中,如下所示:
if (notSummer(date))
charge = winterCharge(quantity);
else charge = summerCharge (quantity);
private boolean notSummer(Date date) {
return date.before (SUMMER_START) || date.after(SUMMER_END);
}
private double summerCharge(int quantity) {
return quantity * _summerRate;
}
private double winterCharge(int quantity) {
return quantity * _winterRate + _winterServiceCharge;
}
通过这段代码你可以看出,整个重构带来的清晰性。实际工作中,我会逐步进行每一次提炼,并在每次提炼之后编译并测试。
像这样的情况下,许多程序员都不会去提炼分支条件。因为这些分支条件往往非常短,看上去似乎没有提炼的必要。但是,尽管这些条件往往很短,在代码意图和代 码自身之间往往存在不小的差距。哪怕在上面这样一个小小例子中,notSummer(date) 这个语句也能够比原本的代码更好地表达自己的用途。对于原来的代码,我必须看着它,想一想,才能说出其作用。当然,在我们这个简单的例子中,这并不困难。不过,即使如此,提炼出来的函数可读性也更高一些——它看上去就像一段注释那样清楚而明白。(这段话堪称经典了!)
Ps: 自己的写法如下。如前面说的,好的代码应该带有自解释性,看到代码的变量名或者参数名就应该知道这个变量代表什么意思,或者这个函数实现了什么功能。如果notSummer这个函数不是经常使用的话,没有必要特地写成函数,更推荐引入“解释性变量”,例如bSummer代表是否是夏天。解释性函数或者解释性变量都会比 if (date.before (SUMMER_START) || date.after(SUMMER_END)) 可读性来的好。
boolean bSummer = date.before (SUMMER_START) || date.after(SUMMER_END);
if (bSummer)
charge = winterCharge(quantity);
else charge = summerCharge (quantity);
private double summerCharge(int quantity) {
return quantity * _summerRate;
}
private double winterCharge(int quantity) {
return quantity * _winterRate + _winterServiceCharge;
}
范例2: 游戏中人物满足一定等级,并且尚未获得该奖励,则在界面上开放奖励的按钮。错误写法:
uiReward.gameObject.SetActive(requireLevel <= FRole.getLevel(roleId) && FWelfare.getWelfareInfo(roleId).getRewardStatus())
正确写法:
boolean bSatisfiedLevel = requireLevel <= FRole.getLevel(roleId);
boolean bAlreadyGotReward = FWelfare.getWelfareInfo(roleId).getRewardStatus();
boolean bOpen = bSatisfiedLevel && (!bAlreadyGotReward);
uiReward.gameObject.SetActive(bOpen)
9.4 Remove Control Flag(移除控制标记)
在一系列条件表达式中,你常常会看到「用以判断何时停止条件检查」的控制标记(control flag):
set done to false
while not done
if (condition)
do something
set done to true
next step of loop
这样的控制标记带来的麻烦超过了它所带来的便利。人们之所以会使用这样的控制标记,因为结构化编程原则告诉他们:每个子程序(routines)只能有一个入口(entry) 和一个出口(exit)。我赞同「单一入口」原则(而且现代编程语言也强迫我们这样做),但是「单一出口」原则会让你在代码中加入讨厌的控制标记,大大降低条件表达式的可读性。这就是编程语言提供break 语句和continue 语句的原因:你可以用它们跳出复杂的条件语句。去掉控制标记所产生的效果往往让你大吃一惊:条件语句真正的用途会清晰得多。
作法(Mechanics)
对控制标记(control flags)的处理,最显而易见的办法就是使用Java 提供的break 语句或continue 语句。
在未能提供break 和continue 语句的编程语言中,我们可以使用另一种办法:
即使在支持break 和continue 语句的编程语言中,我通常也优先考虑上述第二方案。因为return 语句可以非常清楚地表示:不再执行该函数中的其他任何代码。 如果还有这一类代码,你早晚需要将这段代码提炼出来。
9.5 以卫语句取代嵌套条件式
以卫语句取代嵌套条件式的精髓是给某一条分支以特别的重视。如果使用if-then-else 结构,你对if 分支和else 分支的重视是同等的。 这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句(guard clauses)就不同了,它告诉阅读者:『这种情况很罕见,如果它真的发生了,请做 一些必要的整理工作,然后退出。』
范例:
想像一个薪资系统,其中以特殊规则处理死亡员工、驻外员工、退休员工的薪资。这些情况不常有,但的确偶而会出现。
假设我在这个系统中看到下列代码:
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
};
}
return result;
};
在这段代码中,非正常情况的检查掩盖了正常情况的检查,所以我应该使用『卫语句」来取代这些检查,以提高程序清晰度。我可以逐一引入卫语句。
修改:
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
};
9.6 以多态取代条件表达式
9.7 引入Null对象(本质是多态的使用,特殊类)
9.8 断言
3.1 重复代码
function Dialog:OnShow(params)
local approveRecords = self.m_data:GetApproveRecords()
local today = approveRecords:GetApproveRecordsToday()
local earlier = approveRecords:GetApproveRecordsEarlier()
local bShowTodayRecord = self:DrawTableView(self.m_Fields.UITableView_Today, self.m_Fields.Image_TodayBG, today)
local bShowEarlierRecord = self:DrawTableView(self.m_Fields.UITableView_Earlier, self.m_Fields.Image_EarlierBG, earlier)
local bShowUIGroupRecord = bShowTodayRecord or bShowEarlierRecord
self.m_Fields.UIGroup_Record.gameObject:SetActive(bShowUIGroupRecord)
self.m_Fields.UIGroup_NoInfo.gameObject:SetActive(not bShowUIGroupRecord)
end
function Dialog:DrawTableView(uiTableView,uiBG,dataRecord)
local bShow = #dataRecord ~= 0
uiBG.gameObject:SetActive(bShow)
uiTableView.gameObject:SetActive(bShow)
if bShow then
uiTableView:DrawList(#dataRecord,function(index,item)
local record = dataRecord[index + 1]
item.data = record
if not record then return end
item.m_Fields.Text_Record.text = self:ComposeEntry(record.time, record.otherrolename)
end)
end
return bShow
end