複雜分支從何而來
首先我們要討論的第一個問題是,為什麼遺留程式碼裡面往往有那麼多複雜分支。這些複雜分支在程式碼的首個版本中往往是不存在的,假設做設計的人還是有點經驗的話,他應該預見將來可能需要進行擴展的地方,並且預留抽象接口。
但程式碼經過若干個版本的迭代以後,尤其是經過若干次需求細節的調整以後,複雜分支就會出現了。需求的細節調整,往往不會反映在 UML 上,而會直接反映在程式碼上。例如說,原本訊息分為聊天訊息和系統訊息兩類,設計的時候自然會把這設計成訊息類的兩個子類。但接著有一天需求發生細節調整了,系統訊息裡面有一部分是重要的,它們的標題要顯示為紅色,這時候程式設計師往往會做以下修改:
在系統訊息類別上面加一個important 屬性
在對應的render 方法裡面加入一個關於important 屬性的分支,用來控制標題顏色
程式設計師為什麼會做出這樣的修改?有可能因為他沒意識到應該要抽象化。因為需求說的是「系統訊息裡面有一部分是重要的」,對於接受命令式程式語言訓練比較多的程式設計師來說,他或許首先想到的是標誌位——一個標誌位就可以區分重要跟不重要。他沒想到這個需求可以用另一種方式來解讀,「系統訊息分為重要和不重要兩個類別」。這樣子解讀,他就知道應該對系統訊息進行抽象化了。
當然也有可能,程式設計師知道可以抽象,但基於某些原因,他選擇了不這麼做。很常見的一種情況就是有人逼著程式設計師,以犧牲程式碼品質來換取專案進度速度──加入一個屬性和一個分支,遠比抽象重構要簡單得多,如果要做10個這種形式的修改,是做10個分支快還是做10個抽象快?差異顯而易見。
當然, if else 多了,就有聰明人站出來說「不如我們改成 switch case 」吧。在某些情況下,這確實能夠提升程式碼可讀性,假設每一個分支都是互斥的話。但是當 switch case 的數量也多起來以後,程式碼一樣會變得不可讀。
複雜分支有何壞處
複雜分支有什麼壞處?讓我從百度 Hi 網頁版的舊程式碼裡面截取一段出來做個範例。
switch (json.result) { case "ok": switch (json.command) { case "message": case "systemmessage": if (json.content.from == "" && json.content.content == "kicked") { /* disconnect */ } else if (json.command == "systemmessage" || json.content.type == "sysmsg") { /* render system message */ } else { /* render chat message */ } break; } break;
這段程式碼要看懂不難,因此我提一個簡單問題,以下這個JSON 命中哪個分支:
{ "result": "ok", "command": "message", "content": { "from": "CatChen", "content": "Hello!" } }
你很容易就能得到正確答案:這個JSON 命中/ * render chat message */ (顯示聊天訊息)這個分支。那我想了解一下,你是如何作出這個判斷的呢?首先,你要看它是否命中case "ok": 分支,結果是命中了;然後,你要看它是否命中case "message": 分支,結果也是命中了,所以case "systemmessage": 就不用看了;接下來,它不命中if 裡面的條件;而且,它也不命中else if 裡面的條件,所以它命中了else 這個分支。
看出問題來了嗎?為什麼你不能看著這個 else 就說出這個 JSON 命中這個分支?因為 else 本身不包含任何條件,它只隱含條件!每一個 else 的條件,都是對它之前的每一個 if 和 else if 進行先非後與運算的結果。也就是說,判斷命中這個else ,相當於判斷命中這樣一組複雜的條件:
!(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")
再套上外層的兩個switch case ,這個分支的條件就是這樣子的:
json.result == "ok" && (json.command == "message" || json.command == "systemmessage") && !(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")
這裡面有重複邏輯,省略後是這樣子的:
json.result == "ok" && json.command == "message" && !(json.content.from == "" && json.content.content == "kicked") && !(json.content.type == "sysmsg")
我們花了多大力氣才從簡簡單單的else 這四個字母中推導出這樣一長串邏輯運算表達式來?況且,不仔細看還真的看不懂這個表達式說的話。
這就是複雜分支難以閱讀和管理的地方。想像你面對一個switch case 套一個if else ,總共有3個case ,每個case 裡面有3個else ,這就夠你研究的了——每一個分支,條件中都隱含著它所有前置分支以及所有祖先分支的前置分支先非後與的結果。
如何避免複雜分支
首先,複雜邏輯運算是無法避免的。重構得到的結果應該是等價的邏輯,我們能做的只是讓程式碼變得更容易閱讀和管理。因此,我們的重點應該在於如何讓複雜邏輯運算變得易於閱讀和管理。
抽象化為類別或工廠
對於習慣做物件導向設計的人來說,可能這意味著將複雜邏輯運算打散並分散到不同的類別裡面:
switch (json.result) { case "ok": var factory = commandFactories.getFactory(json.command); var command = factory.buildCommand(json); command.execute(); break; }
这看起来不错,至少分支变短了,代码变得容易阅读了。这个 switch case 只管状态码分支,对于 "ok" 这个状态码具体怎么处理,那是其他类管的事情。 getFactory 里面可能有一组分支,专注于创建这条指令应该选择哪一个工厂的选择。同时 buildCommand 可能又有另外一些琐碎的分支,决定如何构建这条指令。
这样做的好处是,分支之间的嵌套关系解除了,每一个分支只要在自己的上下文中保持正确就可以了。举个例子来说, getFactory 现在是一个具名函数,因此这个函数内的分支只要实现 getFactory 这个名字暗示的契约就可以了,无需关注实际调用 getFactory 的上下文。
抽象为模式匹配
另外一种做法,就是把这种复杂逻辑运算转述为模式匹配:
Network.listen({ "result": "ok", "command": "message", "content": { "from": "", "content": "kicked" } }, function(json) { /* disconnect */ }); Network.listen([{ "result": "ok", "command": "message", "content": { "type": "sysmsg" } }, { "result": "ok", "command": "systemmessage" }], function(json) { /* render system message */ }); Network.listen({ "result": "ok", "command": "message", "content": { "from$ne": "", "type$ne": "sysmsg" } }, func tion(json) { /* render chat message */ });
现在这样子是不是清晰多了?第一种情况,是被踢下线,必须匹配指定的 from 和 content 值。第二种情况,是显示系统消息,由于系统消息在两个版本的协议中略有不同,所以我们要捕捉两种不同的 JSON ,匹配任意一个都算是命中。第三种情况,是显示聊天消息,由于在老版本协议中系统消息和踢下线指令都属于特殊的聊天消息,为了兼容老版本协议,这两种情况要从显示聊天消息中排除出去,所以就使用了 "$ne" (表示 not equal )这样的后缀进行匹配。
由于 listen 方法是上下文无关的,每一个 listen 都独立声明自己匹配什么样的 JSON ,因此不存在任何隐含逻辑。例如说,要捕捉聊天消息,就必须显式声明排除 from == "" 以及 type == "sysmsg" 这两种情况,这不需要由上下文的 if else 推断得出。
使用模式匹配,可以大大提高代码的可读性和可维护性。由于我们要捕捉的是 JSON ,所以我们就使用 JSON 来描述每一个分支要捕捉什么,这比一个长长的逻辑运算表达式要清晰多了。同时在这个 JSON 上的每一处修改都是独立的,修改一个条件并不影响其他条件。
最后,如何编写一个这样的模式匹配模块,这已经超出了本文的范围。
以上是談一談javascript中的各種複雜分支語句用法的詳細內容。更多資訊請關注PHP中文網其他相關文章!