在程式設計中思考,簡化你的判斷邏輯
之前看 Linus Toward 在去年的某次採訪中說到的好程式碼壞程式碼,當中提到了邏輯的精簡,能用更通用的邏輯減少 if else 的判斷在某種程度上可以使你的程式碼變得更好。最近一段時間重構了部分老程式碼,也 Review 了不少程式碼,對此觀點深有感觸。
很多時候,程式設計師接到的需求,產品巴不得你立刻就能搞定,有時候會給非常緊迫的時間點。這種情況下會帶來的最直接問題,就是“設計壞味”。有整體的架構設計的不合理,也有程式碼邏輯的問題。尤其是對於邊界條件的處理,因為需求的急,很多時候大家就會按照業務語言去寫。
比如,下面這個例子:
某一報警系統產生了報警郵件,現在需要按照型別顯示不同的內容。
型別 | 報警選擇物件 | 郵件中展示內容 |
---|---|---|
Web事務 | 單條Web事務 | Tier,Web事務,節點 |
Web事務 | Tier | Tier,節點 |
節點 | 某一個節點 | 節點 |
節點 | Tier | Tier,節點 |
如果按照業務的語言,我們可能寫出如下的虛擬碼:
if (Web事務 && 單條Web事務) {
return Tier,Web事務,節點;
}
if (Web事務 && Tier) {
return Tier,節點;
}
if (節點 && 某一個節點) {
return 節點;
}
if (節點 && Tier) {
return Tier,節點;
}
可能你看到這裡會立刻笑出來,哪有隻寫 if
不寫 else
的。那好,也許你會寫出這樣的程式碼。
if (Web事務) {
if (單條Web事務) {
return Tier,Web事務,節點;
} else if (Tier) {
return Tier,節點;
}
} else if (節點) {
if (某一個節點) {
return 節點;
} else if (Tier) {
return Tier,節點;
}
}
我相信,大部分人都能將判斷邏輯寫到這一層,但是,這就完了麼?也許你會說完了,邏輯也正確,看起來也很清晰。然而,這遠遠不夠。
其實我們可以發現,在每種情況下,都會返回節點
資訊。只返回節點資訊的只有一種情況,其他的情況下,基本都返回Tier
資訊。只有Web事務 && 單條Web事務
的情況需要返回Web事務
資訊。所以,最後我們可以精簡為兩個判斷。
result = "節點";
if (Web事務 && 單條Web事務) {
result = "Web事務" + result;
}
if (Web事務 || !某一個節點) {
result = "Tier" + result;
}
return result;
回到最初的命題,為何不要用業務的語言來編寫判斷邏輯呢?因為業務語言是給使用者和產品看的,他們在描述上本身就不夠精簡。其次,業務的描述,很多時候,是定義邊界,說明問題,而不是告訴你判斷邏輯。
所以,在寫程式碼的時候,更多的時候要細化邏輯。這樣,在維護修改時,才更為方便。下面我舉另一個更具體的例子。
這是我 Review Scala 程式碼的時候遇到的一個問題,首先我先用業務語言描述一下需求。需要判斷某個規則的開閉狀態,在一天的幾點到幾點間啟用,且還可以額外設定是週一到週日的哪幾天啟用。
於是我看到當時的同事,寫一個方法isSuppressTime
,會給兩個引數,第一個引數為一個時間戳timestamp
,第二個引數為一個 List[Map[String, String]]
。
Map[String, String]
裡面有4個值,分別是:
- startTime,從零點到某個具體開始時間的秒數,0~86399。
- endTime,從零點到某個具體結束時間的秒數,0~86399。
- isDaily,當時間戳在 startTime endTime 之間時,如果此項為 true,則返回 true。
- weekdays,週一到週日,1~7,以
,
間隔組成的字串,如1,4,5
。表示週一、週四、週五且時間戳在 startTime endTime 之間時返回 true
最後他寫出瞭如下的程式碼(Scala):
def isSuppressTime(now: Long = System.currentTimeMillis(),
suppressTimes: java.util.List[java.util.Map[String, String]] = rule.getSuppressTime): Boolean = {
if(suppressTimes == null)
return false
import scala.collection.JavaConversions._
suppressTimes.foreach(params => {
if (params != null && params.size > 0) {
val startTime = params.get("startTime")
val endTime = params.get("endTime")
val isDaily = params.get("isDaily")
val weekdays = params.get("weekday").split(",").toList
val zero = zeroTimestamp()
//今天零點零分零秒的毫秒數
val start = zero + startTime.toLong * 1000
val end = zero + endTime.toLong * 1000
if (start <= now && end >= now) {
if (isDaily.toBoolean)
return true
val cal = Calendar.getInstance()
cal.setTimeInMillis(now)
var day = cal.get(Calendar.DAY_OF_WEEK)
if (day == 1)
day = 7
else
day = day - 1
val weekday = day.toString
if (weekdays.contains(weekday))
return true
}
}
})
return false
}
private def zeroTimestamp(): Long = {
val cal = Calendar.getInstance()
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.MILLISECOND, 1)
cal.getTimeInMillis()
}
這個程式碼看著很長,判斷很多,而且還用了超級多陳舊的 API,使得它的效能也不好。而且這是一段 Scala 的程式碼,卻用了較多的return
和var
,這兩個都是 Scala 不提倡的。最關鍵的,明明是函式式的程式碼,他卻寫出了過程式的感覺。給後面的維護人員(我)帶來了不少困擾。
下面,我們來一點點優化這段程式碼(需要一點點 Java 功底),首先對於方法zeroTimestamp()
,我第一眼看到的時候,是驚訝的,原作者用了一個比較舊的Calendar
。對它最深刻的印象就是當年寫SimpleDataFormat
的時候,因為Calendar
這貨導致執行緒不安全。而每次建立SimpleDataFormat
的開銷比較大,最後不得不寫了個ThreadLocal<SimpleDataFormat>
。
而Java8以後,我們可以直接用java.time
下面的類來改寫zeroTimestamp()
,這裡只需要一行程式碼,如下:
private def zeroTimestamp(): Long = {
Timestamp.valueOf(LocalDate.now().atStartOfDay()).getTime
}
回到isSuppressTime
方法上來,我姑且不說他的設計多麼麻煩,因為這是一個已經被廣泛使用的方法,我能做到的就是在不改變簽名的情況寫來優化實現。
首先,開頭我們就看到
if(suppressTimes == null)
return false
這個空的判斷和強制return
,在 Java 裡面,null
是一個很痛苦的事情,Scala 因為基於 JVM 也不例外,但是 Scala 和 Java 8 都分別有一個 Option
類(Java8是 Optional
),來做空值處理。
所以,我們這裡第一時間可以幹掉這個判斷,寫成如下方式
Option[util.List[util.Map[String, String]]](suppressTimes).map(times => {
// ba la ba la
}).getOrElse(false)
程式碼裡面的times
為方法中非空的suppressTimes
,註釋部分為核心的處理邏輯,這樣我們避免了一個if
判斷,減少了一個return
。
而其實, Option([A]).map([B] => Boolean).getOrElse(false)
等價於Option
的exists
方法,最後完整的程式碼應該如下:
def isSuppressTime(now: Long = System.currentTimeMillis(),
suppressTimes: java.util.List[java.util.Map[String, String]]): Boolean = {
Option[util.List[util.Map[String, String]]](suppressTimes).exists(times => {
times.foreach(params => {
if (params != null && params.size > 0) {
val startTime = params.get("startTime")
val endTime = params.get("endTime")
val isDaily = params.get("isDaily")
val weekdays = params.get("weekday").split(",").toList
val zero = zeroTimestamp()
//今天零點零分零秒的毫秒數
val start = zero + startTime.toLong * 1000
val end = zero + endTime.toLong * 1000
if (start <= now && end >= now) {
if (isDaily.toBoolean)
return true
val cal = Calendar.getInstance()
cal.setTimeInMillis(now)
var day = cal.get(Calendar.DAY_OF_WEEK)
if (day == 1)
day = 7
else
day = day - 1
val weekday = day.toString
if (weekdays.contains(weekday))
return true
}
}
})
false
})
}
在上面的例子裡面,我們已經幹掉了兩個return
和一個if
,讓它有一點 Functional 的感覺了。
上面重構後程式碼中,times
的型別是util.List[util.Map[String, String]]
。times.foreach(params => {})
裡的params
對應的是util.Map[String, String]
型別。所以,我們看到判斷if (params != null && params.size > 0)
時應該會發現,這就是一個list filter的過程嘛。.filter()
之後不就是一個.map()
,於是我們可以這麼改:
Option[util.List[util.Map[String, String]]](suppressTimes).exists(times => {
times.filter(params => params != null && !params.isEmpty).map(params => {
// ba la ba la
}).contains(true)
})
當然.map(xxx => Boolean).contains(true)
等價於.exists()
,於是我們重構的方法如下:
def isSuppressTime(now: Long = System.currentTimeMillis(),
suppressTimes: java.util.List[java.util.Map[String, String]]): Boolean = {
Option[util.List[util.Map[String, String]]](suppressTimes).exists(times => {
times.filter(params => params != null && !params.isEmpty).exists(params => {
val startTime = params.get("startTime")
val endTime = params.get("endTime")
val isDaily = params.get("isDaily")
val weekdays = params.get("weekday").split(",").toList
val zero = zeroTimestamp()
//今天零點零分零秒的毫秒數
val start = zero + startTime.toLong * 1000
val end = zero + endTime.toLong * 1000
if (start <= now && end >= now) {
if (isDaily.toBoolean)
return true
val cal = Calendar.getInstance()
cal.setTimeInMillis(now)
var day = cal.get(Calendar.DAY_OF_WEEK)
if (day == 1)
day = 7
else
day = day - 1
val weekday = day.toString
if (weekdays.contains(weekday))
return true
}
false
})
})
}
這次重構,我們又去掉了一層 if 判斷,改為 filter 實現,減少了一次返回。到了這一步,我們會發現,下面需要實現的就是一個方法,對util.Map[String, String]
去做處理,返回一個布林值,於是我們精簡方法的實現程式碼為兩部分:
def isSuppressTime(now: Long = System.currentTimeMillis(),
suppressTimes: java.util.List[java.util.Map[String, String]] = rule.getSuppressTime): Boolean = {
Option[util.List[util.Map[String, String]]](suppressTimes)
.exists(times => times.filter(params => params != null && !params.isEmpty).exists(isValidateTimes(_, now)))
}
這個為邊界過濾的方法。
private def isValidateTimes(params: java.util.Map[String, String], now: Long): Boolean = {
val startTime = params.get("startTime")
val endTime = params.get("endTime")
val isDaily = params.get("isDaily")
val weekdays = params.get("weekday").split(",").toList
val zero = zeroTimestamp()
//今天零點零分零秒的毫秒數
val start = zero + startTime.toLong * 1000
val end = zero + endTime.toLong * 1000
if (start <= now && end >= now) {
if (isDaily.toBoolean)
return true
val cal = Calendar.getInstance()
cal.setTimeInMillis(now)
var day = cal.get(Calendar.DAY_OF_WEEK)
if (day == 1)
day = 7
else
day = day - 1
val weekday = day.toString
if (weekdays.contains(weekday))
return true
}
false
}
這個為我們要重構的核心處理邏輯。我們優化整理它的判斷條件,最後可以實現如下的完整程式碼:
def isSuppressTime(now: Long = System.currentTimeMillis(),
suppressTimes: java.util.List[java.util.Map[String, String]] = rule.getSuppressTime): Boolean = {
Option[util.List[util.Map[String, String]]](suppressTimes)
.exists(times => times.filter(params => params != null && !params.isEmpty).exists(isValidateTimes(_, now)))
}
private def isValidateTimes(params: java.util.Map[String, String], now: Long): Boolean = {
val zero = zeroTimestamp()
val start = zero + params.get("startTime").toLong * 1000
val end = zero + params.get("endTime").toLong * 1000
val isDaily = params.get("isDaily").toBoolean
val weekdays = params.get("weekday").split(",").toList
val weekday = LocalDate.now().getDayOfWeek.getValue.toString
(start <= now && end >= now) && (isDaily || weekdays.contains(weekday))
}
這樣,我們就實現了一個if
都沒有,一個return
都沒有的純函式式寫法。
總的來說,程式碼誰都能寫出來,但是把需求從文字或者是流程描述換成編碼實現時就有了對程式設計師抽象邏輯能力的需求。如何組織抽象,就像是如何寫作文,或者是 Kata(空手道里面的招數、套路),不要按照業務描述寫 if else,而要儘可能簡化找到一致性的簡單邏輯描述。
如果能將所有的特殊情況變為通用情況,簡化邏輯判斷,那麼程式碼在後面的迭代中也會比較易於維護。
相關文章
- 前端業務程式碼配置化處理條件判斷邏輯前端
- ruby邏輯判斷符號符號
- JavaScript(ES6)邏輯判斷條件優化JavaScript優化
- c#學習----邏輯判斷C#
- shell程式設計中的控制判斷語句程式設計
- 幾道經典邏輯推理題,提高你的邏輯思考能力
- 程式設計師,你的邏輯思維有多強?程式設計師
- 遊戲機制設計:生活邏輯轉化為遊戲邏輯的設計形式遊戲
- 計算機程式的思維邏輯 (69) - 執行緒的中斷計算機執行緒
- 邏輯程式設計與函式程式設計的介紹程式設計函式
- Optional簡化空值判斷,減少程式碼中的if-else程式碼塊
- 當邏輯程式設計遭遇CQRS時程式設計
- 程式設計師面試邏輯題解析程式設計師面試
- [BUG反饋]模型編輯模板存在條件邏輯判斷錯誤模型
- shell程式設計(五)條件判斷程式設計
- 02 . Shell變數和邏輯判斷及迴圈使用變數
- vue router+ vuex+ 首頁登入判斷邏輯Vue
- 小小邏輯判斷符的錯誤使用,資損幾萬塊
- TRIZ在軟體設計中的思考
- [02] 多執行緒邏輯程式設計執行緒程式設計
- JS 寫邏輯判斷,不要只知道用 if-else 和 switchJS
- 程式設計師需要了解的邏輯學思想程式設計師
- 提高程式設計邏輯的7種方法 - DEV程式設計dev
- PTA 程式設計 判斷題-期末複習程式設計
- 如何判斷程式設計師在做什麼?程式設計師
- AI「王道」邏輯程式設計的復興?清華提出神經邏輯機,已入選ICLRAI程式設計ICLR
- [譯] 如何簡化你的設計
- 能啟發你不斷思考進步的最佳5條程式設計語錄程式設計
- 機器學習中的邏輯迴歸模型簡介機器學習邏輯迴歸模型
- 經典示例-在快樂中鍛鍊程式邏輯
- 小程式分包的一些思考及Uiniapp 分包優化邏輯的驗證UIAPP優化
- 邏輯迴歸:使用Python的簡化方法邏輯迴歸Python
- 程式設計是最好的邏輯能力訓練方法! - thoughtbot程式設計
- 《Java程式設計邏輯》第3章 類的基礎Java程式設計
- 軍事思維者的思考邏輯
- 關於前端資料&邏輯的思考前端
- 併發程式設計喚醒判斷用while程式設計While
- 用超程式設計來判斷STL型別程式設計型別