1. 程式人生 > >讀書報告之《修改程式碼的藝術》 (II)續

讀書報告之《修改程式碼的藝術》 (II)續

這裡作為(II)的續篇,繼續距離 複雜的巢狀if else 的處理。 為了保持篇幅不會太長,所以截斷了,以一篇新的文章形式給出。

化簡複雜的if else語句,基本的手段

  1. 針對頭重腳輕的if else,使用return快速返回,從而減少巢狀層數。
  2. 合併分支。有些分支的執行內容相同,往往意味著可以合併為一個分支
  3. 扁平化。

第二個例子,比較複雜一點,給定一個日期,即年 月 日,讓你給出下一天的表述。即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日;如果是某個月的最後一天,那麼下一天就是下一個月的第一天 ; 否則只要簡單的天數+1就可以了。
  2. 第一個if (day >=28) 作者用意是這樣的:如果小於28,不論 是哪個月,都不會是該月最後一天。所以只要天數直接+1就可以了。
  3. 如果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