讀書報告之《修改程式碼的藝術》 (II)續
阿新 • • 發佈:2018-10-31
這裡作為(II)的續篇,繼續距離 複雜的巢狀if else 的處理。 為了保持篇幅不會太長,所以截斷了,以一篇新的文章形式給出。
化簡複雜的if else語句,基本的手段
- 針對頭重腳輕的if else,使用return快速返回,從而減少巢狀層數。
- 合併分支。有些分支的執行內容相同,往往意味著可以合併為一個分支
- 扁平化。
第二個例子,比較複雜一點,給定一個日期,即年 月 日,讓你給出下一天的表述。即2000-1-1 ==》 2000-1-2, 2000-1-31==》2000-2-1 2000-12-31==》2001-1-1
這裡簡化了一些,給各位一些看程式碼的耐心。只給出月 日, 然後二月固定為28天。
int nextdate(int month, int day) { if (day >= 28) // 如果是一個月的最後一天 { if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) { if (day >= 31) { // 這個月的最後一天, 需要同時調整 月 日 day = 1; month += 1; if (month > 12) // 一年的最後一天,需要同時調整年 月 日 { month = 1; } } else { day += 1; } } else if (month == 2) { // 非閏年28號是2月的最後一天 day = 1; month += 1; } else if (month == 4 || month == 6 || month == 9 || month == 11) { if (day >= 30) // 是30號 { day = 1; month += 1; } else { day += 1; } } } else // 如果不是一個月的最後一天,則day直接加1 day += 1; return month * 100 + day; // 計算出明天的日期 }
每次看這樣的程式碼,不得不說要減壽半年。所以在正式化簡之前,為了不致各位看官的手臂男,我先大概聊一聊這個程式:
- 基本的演算法邏輯:計算下一天時,如果是這一年的最後一天,那麼就是下一年的1月1日;如果是某個月的最後一天,那麼下一天就是下一個月的第一天 ; 否則只要簡單的天數+1就可以了。
- 第一個if (day >=28) 作者用意是這樣的:如果小於28,不論 是哪個月,都不會是該月最後一天。所以只要天數直接+1就可以了。
- 如果day >=28, 那麼對不同的月份,就可能是月末最後一天,也可能不是,具體再用if else處理。
程式的設計聊完了,先做個簡單的重構。
- if (day >= 28)這個語句實在是太頭重腳輕了,所以使用return快速返回的手法。順便去掉一層巢狀
int nextdate(int month, int day) {
if (day < 28) {// 如果不是一個月的最後一天,則day直接加1
day += 1;
return month * 100 + day; // 計算出明天的日期
}
if (month == 1 || month == 3 || month == 5 || month == 7
|| month == 8 || month == 10 || month == 12) {
if (day >= 31) { // 這個月的最後一天, 需要同時調整 月 日
day = 1;
month += 1;
if (month > 12) // 一年的最後一天,需要同時調整年 月 日
{
month = 1;
}
} else {
day += 1;
}
} else if (month == 2) {
// 非閏年28號是2月的最後一天
day = 1;
month += 1;
} else if (month == 4 || month == 6 || month == 9 || month == 11) {
if (day >= 30) // 是30號
{
day = 1;
month += 1;
} else {
day += 1;
}
}
return month * 100 + day; // 計算出明天的日期
}
然後觀察剩下的一大串if else語句,有很多重複的程式碼片段,這個現象說明可能比較適合應用合併分支的手法。
- 重複程式碼段day =1; month +=1; // 表示如果是月末最後一天時,next date 就是下一個月的1號
- 重複程式碼段 day += 1; // 表示如果不是月末最後一天時,next date就是 本月天數 + 1
所以我們合併分支條件,將所有判定是月末最後一天的條件合併在一起。整個程式的邏輯就簡化為是不是月末最後一天,如果是 do somethin 如果不是 do something。
當然,到這裡的時候,已經完全可以重新寫一套程式碼,相對來說速度上比改這個程式碼肯定還要快一些,現實工作中 這種情形也不少遇到。不過這裡為了扣住這個blog主題——“修改程式碼的藝術”,所以下面還是以程式碼重構的方式進行
要合併分支,首先需要將這些分支集中到同一層。 千萬不要直接上下層合併,只要稍微複雜一些,就連怎麼死的都不知道。因此先做一次扁平化操作。具體怎麼做,之前已經詳細講述了,這裡就直接快進
int nextdate(int year, int month, int day) {
if (day < 28) {// 如果不是一個月的最後一天,則day直接加1
day += 1;
return month * 100 + day; // 計算出明天的日期
}
if ((month == 1 || month == 3 || month == 5 || month == 7
|| month == 8 || month == 10 || month == 12) && (day >= 31)) {
// 這個月的最後一天, 需要同時調整 月 日
day = 1;
month += 1;
if (month > 12) // 一年的最後一天,需要同時調整年 月 日
{
year += 1;
month = 1;
}
} else if ((month == 1 || month == 3 || month == 5 || month == 7
|| month == 8 || month == 10 || month == 12) && !(day >= 31)) {
day += 1;
} else if (month == 2) {
// 非閏年28號是2月的最後一天
day = 1;
month += 1;
} else if ((month == 4 || month == 6 || month == 9 || month == 11) && (day >= 30)) {
// 30號是這些月份的最後一天
day = 1;
month += 1;
} else if ((month == 4 || month == 6 || month == 9 || month == 11) && !(day >= 30)) {
day += 1;
}
return month * 100 + day; // 計算出明天的日期
}
在合併所有判定是月末最後一天的分支 之前,需要先將這些分支移動到相鄰的位置。一般而言,if 。。。 else if 。。。 else 是不可以隨便上下移動位置的
if (x < 5) { }
else if (x < 10) {}
如果將後面的else if (x<10) 與 if (x<5) 交換位置,一看就知道出問題了。後面的分支已經永遠不可能被執行到了。
if (x < 10) { }
else if (x < 5) {}
出現這個情況的原因就是 else if (x < 10) ,這既是面子又是個裡子。 if x<10面子, else 裡子,其實暗含著 x>=5。所以如果寫成這樣就可以了
if (5 <= x && x < 10) { }
else if (x < 5) {}
您開竅了嗎? 口才有限,只能點到為止。
移動和合並分支
int nextdate(int month, int day) {
if (day < 28) {// 如果不是一個月的最後一天,則day直接加1
day += 1;
return month * 100 + day; // 計算出明天的日期
}
if ((month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && (day >= 31)
|| (month == 2)
|| (month == 4 || month == 6 || month == 9 || month == 11) && (day >= 30)) {
// 這個月的最後一天, 需要同時調整 月 日
day = 1;
month += 1;
if (month > 12) // 一年的最後一天,需要同時調整年 月 日
{
month = 1;
}
} else if (((month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && !(day >= 31))
|| ((month == 4 || month == 6 || month == 9 || month == 11) && day < 30)) {
day += 1;
}
return month * 100 + day; // 計算出明天的日期
}
接下來就可以直接應用各種重構手法,將邏輯條件直接提煉為單獨的方法,提高程式碼可讀性。類似的技術用到if (month > 12)這個分支,還是直接快進到最終的程式碼
int nextdate(int month, int day) {
if (isLastDayofaYear(day, month)) {
// 一年中的最後一天,下一天就是1月1日
day = 1;
month = 1;
} else if (isLastDayOfaMonth(day, month)) {
// 這個月的最後一天,下一天就是下一個月的1號
day = 1;
month += 1;
} else { // 不是月末最後一天,直接天數+1
day += 1;
}
return month * 100 + day; // 計算出明天的日期
}
private boolean isLastDayOfaMonth( int day, int month) {
if (month == 1 || month == 3 || month == 5 || month == 7
|| month == 8 || month == 10 || month == 12) {
return day == 31;
} else if (month == 4 || month == 6 || month == 9 || month == 11) {
return day == 30;
} else if (month == 2) {
return day == 28;
} else {
throw new RuntimeException("unknow month");
}
}
private boolean isLastDayofaYear(int day, int month) {
return month==12 && day == 31;
}
最後再談優化,程式到這裡,要優化時就容易的多了,直接用個靜態map或者乾脆陣列,可以對isLastDayOfaMonth再次簡化以及效能優化。所以不要進行不成熟的優化。程式碼清晰度上來了,往往會有更好的優化方案。
over