阿里Java開發手冊思考(二)

史培培發表於2019-01-29

阿里Java開發手冊思考(二)

上期我們分享了關於Java中equals與hashCode的理解

本期我們將分享Java中if/else複雜邏輯的處理

在github上曾看到一些issue,國外的程式設計師比較忌諱寫else,看到了很多這樣的評論else is horrible,那麼對於邏輯很複雜的程式碼段,如果用太多的if/else的話,那麼會導致程式碼的閱讀難度變大,同時會增加程式碼的圈複雜度,理論上,如果一個函式的圈複雜度超過8,那麼這個函式就還有可優化的地方,那麼如何優化這種多分支的複雜邏輯的函式呢?手冊中給出了三種方法:衛語句策略模式狀態模式,通過閱讀《重構:改善既有程式碼的設計》發現,解決這個問題其實有很多種,下面我們就一一道來。

第一大類:重新組織函式

1、Extract Method(提煉函式)

這種方法應該是最常用的方法之一,當函式過長或者分支太多的話,就可以考慮將其中的一段程式碼提煉成一個獨立的函式。

  • 原始程式碼:
public void today() {
	if (isBusy()) {
		System.out.println("change time.");
	} else if (isFree()) {
		System.out.println("go to travel.");
	} else {
		System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
	}
}
複製程式碼
  • 改後程式碼:
public void today() {
	if (isBusy()) {
		changeTime();
	} else if (isFree()) {
		goToTravel();
	} else {
		stayAtHomeToLearn();
	}
}

private void changeTime() {
		System.out.println("change time.");
}

private void goToTravel() {
	System.out.println("go to travel.");
}

private void stayAtHomeToLearn() {
	System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
}
複製程式碼

用法:

  • 提煉新函式,根據這個函式的意圖來命名,以它做什麼來命名,而不是以他怎麼做來命名
  • 仔細檢查提煉出的程式碼是否引用了作用域限於源函式的變數,包括區域性變數和源函式引數
  • 適用場景:函式過長或者需要註釋才能讓人理解用途的程式碼

2、Substitute Algorithm(替換演算法)

把某個演算法替換成另一個更清晰的演算法,或將函式本體替換為另一個演算法。

  • 原始程式碼:
public String foundPerson(String[] people) {
	for (int i = 0; i < people.length; i++) {
		if (people[i].equals("Don")) {
			return "Don";
		}
		if (people[i].equals("John")) {
			return "John";
		}
		if (people[i].equals("Kent")) {
			return "Kent";
		}
	}
	return "";
}
複製程式碼
  • 改後程式碼:
public String foundPerson(String[] people) {
	List<String> candidates = Arrays.asList(new String[] { "Don", "John", "Kent" });
	for (int i = 0; i < people.length; i++) {
		if (candidates.contains(people[i])) {
			return people[i];
		}
	}
	return "";
}
複製程式碼

用法:

  • 準備好另一個替換用的演算法
  • 新演算法,要與原本的演算法結果相同
  • 適用場景:把某個演算法替換為更清晰的演算法,或者把函式替換為一個演算法

第二大類:簡化條件表示式

1、Eecompose Conditional(分解條件表示式)

如果有複雜的條件(if-then-else)語句,從if、then、else三個段落中分別提煉出獨立函式。

  • 原始程式碼:
public void today() {
	if (isBusy() || isNotWeekend()) {
	   System.out.println("change time.");
	return;
	} else {
		System.out.println("go to travel.");
	}
}
複製程式碼
  • 改後程式碼:
public void today() {
	if (notFree()) {
		changeTime();
	} else {
		goToTravel();
	}
}

private boolean notFree() {
	return isBusy() || isNotWeekend();
}

private void changeTime() {
	System.out.println("change time.");
}

private void goToTravel() {
	System.out.println("go to travel.");
}
複製程式碼

用法:

  • 將if段落提煉出來,構成一個獨立函式
  • 將then段落和else段落都提煉出來,各自構成一個獨立函式
  • 適用場景:複雜的條件語句。如果發現巢狀的條件邏輯,先觀察是否可以使用衛語句,如果不行,再開始分解其中的每個條件

2、Consolidate Conditioinal Expression(合併條件表示式)

如果有一系列條件測試,都得到相同結果,將這些測試合併為一個條件表示式,並將這個條件表示式提煉成為一個獨立函式。

  • 原始程式碼:
public void today() {
	if (isWeekend()) {
		System.out.println("go to travel.");
	}
	if (isHoliday()) {
		System.out.println("go to travel.");
	}
	if (noWork()) {
		System.out.println("go to travel.");
	}
}
複製程式碼
  • 改後程式碼:
public void today() {
	if (isFree()) {
		System.out.println("go to travel.");
	}
}

private boolean isFree() {
	return isWeekend() || isHoliday() || noWork();
}
複製程式碼

用法:

  • 確定這些條件語句都沒有副作用
  • 使用適當的邏輯操作符,將一系列相關條件表示式合併為一個,並對合並後的表示式提煉函式
  • 適用場景:一系列條件測試,都得到相同的結果

3、Consolidate Duplicate Conditional Clauses(合併重複的條件判斷)

在條件表示式的每個分支上有相同的一段程式碼,將這段程式碼搬移到條件表示式之外。

  • 原始程式碼:
public void today() {
	if (isBusy()) {
		System.out.println("change time.");
		sleep();
	} else if (isFree()) {
		System.out.println("go to travel.");
		sleep();
	} else {
		System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
		sleep();
	}
}
複製程式碼
  • 改後程式碼:
public void today() {
	if (isBusy()) {
		System.out.println("change time.");
	} else if (isFree()) {
		System.out.println("go to travel.");
	} else {
		System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
	}
	sleep();
}
複製程式碼

用法:

  • 鑑別出執行方式不隨條件變化而變化的的程式碼
  • 如果這些共同程式碼位於條件表示式起始處,就將它移到條件表示式之前,如果在尾端,移到條件表示式之後
  • 適用場景:在條件表示式的每個分支上有相同的一段程式碼

4、Remove Control Flag(移除控制標記)

在一系列布林表示式中,某個變數帶有“控制標記(Flag)”的作用,以break語句或者return語句取代控制標記。

5、Replace Nested Confitional with Guard Clauses(以衛語句取代巢狀條件表示式)

如果多個分支都屬於正常行為,就應該使用if...else...的條件表示式,如果某個條件極其罕見,就應該單獨檢查該條件,並在該條件為真時立刻從函式中返回,這樣的單獨檢查常常被稱為衛語句。

  • 原始程式碼:
public void today() {
	if (isBusy()) {
		System.out.println("change time.");
	} else if (isFree()) {
		System.out.println("go to travel.");
	} else {
		System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
	}
}
複製程式碼
  • 改後程式碼:
public void today() {
	if (isBusy()) {
		System.out.println("change time.");
		return;
	}
	if (isFree()) {
		System.out.println("go to travel.");
		return;
	}
	System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
	return;
}
複製程式碼

用法:

  • 對於每個檢查,放進一個衛語句,衛語句要麼就從函式中返回,要麼就丟擲一個異常
  • 每次將條件檢查替換成衛語句後,編譯並測試:如果所有的衛語句都導致同樣的結果,請使用合併條件表示式
  • 適用場景:使用衛語句返回所有特殊情況

6、Replace Conditional with Polymorphism(以多型取代條件表示式)

條件表示式,根據物件的型別選擇不同的行為,將這個條件表示式的每個分支放進一個子內的覆寫函式中,然後將原始函式宣告為抽象函式,這一項就是手冊中說的策略模式以及狀態模式。 正因為有了多型,所以“型別碼的switch語句”以及“基於型別名稱的if-then-else”語句在物件導向程式中很少出現。

第三大類:簡化函式呼叫

1、Separate Query from Modifier(將查詢函式和修改函式分離)

某個函式既返回物件狀態值,又修改了狀態,建立兩個不同的函式,其中一個負責查詢,另一個負責修改。

  • 併發的情況:需要保留第三個函式來同時做這兩件事

2、Parameterize Method(令函式攜帶引數)

若干函式做了類似的工作,但在函式本體中卻包含了不同的值,建立單一函式,以參數列達那些不同的值。

  • 原始程式碼:
public void tenPercentRaise() {
	salary *= 1.1;
}

public void fivePercentRaise() {
	salary *= 1.05;
}
複製程式碼
  • 改後程式碼:
public void raise(double factor) {
	salary *= (1 + factor);
}
複製程式碼
  • 要點在於:以可將少量數值視為引數為依據,找出帶有重複性的程式碼

3、Replace Parameter with Explicit Methods(以明確函式取代引數)

有一個函式,其中完全取決於引數值而採取不同行為,針對該引數的每一個可能值,建立一個獨立的函式。

  • 原始程式碼:
public void setValue(String name, int value) {
	if (name.equals("height")) {
		height = value;
	}

	if (name.equals("width")) {
		width = value;
	}
}
複製程式碼
  • 改後程式碼:
public void setHeight(int arg) {
	height = arg;
}

public void setWidth(int arg) {
	width = arg;
}
複製程式碼

用法:

  • 針對引數的每一種可能值,新建一個明確的函式
  • 修改條件表示式的每個分支,使其呼叫合適的新函式
  • 適用場景:函式完全取決於引數值而採取不同行為

阿里Java開發手冊思考(二)

微信公眾號:碼上論劍
請關注我的個人技術微信公眾號,訂閱更多內容

相關文章