面条代码 vs. 馄沌代码
面條代碼 (Spaghetti Code) 指的是冗長,控制結構復雜,混亂而難以理解的代碼。
就我個人而言,曾經編寫過大量面向對象和面向過程的代碼,也曾經寫過至少數千行的函數式代碼,印象中,從來沒有編寫過冗長復雜的函數。有趣的是,我從來沒有把短小精干,低圈復雜度的函數當做一個目標(直到2007年我才第一次聽說“圈復雜度”這個名詞,直到現在也講不清楚它的計算方法),它只是消除重復,分離關注點,清晰表達等價值觀驅動下的一個結果。
在從事咨詢工作以后,我見到了大量的“面條代碼”,更準確的說,見到的大多是“面條代碼”。于是開始對自己的能力感到懷疑,并對那些有能力和“面條代碼”和平共處的工程師感到欽佩。
因為,從骨子里我就對復雜事物感到恐懼和缺乏耐心。我喜歡一目了然,邏輯清晰的事物,如果一個設計可以讓我舒服的靠在凳子上,無須看代碼,僅憑思考就能在腦海中浮現清晰的畫面,我才會感到踏實——everything is under control。
我相信,每個人都會喜歡這種踏實感。由此可以推論出,那些能夠編寫和控制“面條代碼”的工程師,一定都具備優秀的理解力和控制復雜事物的能力。
直到有一天,我在《反模式》一書中看到了對它的定義和描述,才知道原來有這么多的人痛恨它,看來像我一樣能力低下的人并不在少數。隨后看到Uncle Bob在《Clean Code》中談到方法長度和類規模時,均提到“第一條規則是短小,第二條規則是還要更短小”。至此,我徹底感到釋然——能和大師一樣愚笨,就沒什么可感到羞恥的了。
平均復雜度
按照短板理論,一個團隊所編寫的代碼,應該讓團隊中最笨的人也可以容易理解,這樣團隊的整體生產效率才能有效提升。當然你也可以把最笨的人踢出團隊,但剩下的成員里,相對于最聰明的人,依然會存在最笨的人。
不信?請看我最近在網上讀到的一篇文章,里面談到“我的一個老同事曾經說Visual?C++很臭,因為它不允許你在一個函數內擁有超過10,000行代碼” 。
謝天謝地,幸虧他不是我同事,否則,我就算再聰明100倍,也還是逃不出被踢出局的命運。并由此第一次對VC產生了好感(不過我覺得它應該把函數長度限制設為更小的值,比如100行,這樣才可能在一定程度上促進社區的代碼改善)。
所以,我們只能以大多數程序員的平均接受能力為準。而根據相關調查統計,大多數人對于7 ± 2以內規模的事物有較好的控制力。
面向對象
事實上,當使用“面向對象”范式來進行軟件設計時,如果你非常重視“消除重復”,“分離關注點”,“清晰表達”,那么很自然的,你會得到一大批短小精干的類和方法。即便你使用其它范式,比如面向過程或函數式編程,在相同的關注下,你也很難得到面條式代碼。
所以,曾經有人在演講中提到:在面向對象系統中,你不可能得到面條式代碼。
你或許開始質疑:“我們的系統都是用C++或者Java寫的,為何有那么多的 ‘面條代碼’”?
事實上,“面向對象”是一種編程范式,與具體語言關系不大。使用面向過程的語言也可以構造出面向對象系統;反之,使用面向對象語言構造的系統,卻未必是面向對象系統 。在面向對象思想的指導下,面條代碼確實很難出現。而使用“類結構”卻存在大量面條代碼的系統,并非真正的面向對象系統。這樣的系統,有一個專門的名字:肉團面(Spaghetti with meatballs)。
餛飩代碼——better or worse?
但這并非故事的全部,那位演講者的完整闡述是: 在面向對象系統中,你不可能得到面條代碼,但卻會得到餛飩代碼(Ravioli Code)。
顧名思義,餛飩代碼是指程序由許多小的,松散耦合的部分(方法,類,包等)組成。
很明顯,在那位演講者看來,餛飩代碼絕對不是一個褒義詞,把面條代碼重構為餛沌代碼只是把一個問題變成了另外一個問題 。
而這樣的看法并非個案。在咨詢的工作中,我不止一次聽到這樣的反饋,餛飩代碼相對于面條代碼,更加難以理解和跟蹤。
對于這樣的意見,最初我感到非常困惑,因為這與我的認知和經驗恰恰相反。但既然持有這種看法的人并非“一小撮不明真相的群眾”,那就有必要站在對方的角度來思考一下造成這種結果的原因。
在這些工程師看來,在面條代碼中,一個大函數盡管復雜,卻完整的描述了整個過程或算法的所有細節。但在餛沌代碼中,這些過程和細節被拆分的支零破碎,分散到不同的類和方法中,為了理解一個過程或算法,必須在類和方法間跳來跳去,如果有多態存在,你都不知道到底是哪個子類的相關方法被執行。
面對這種困惑的工程師們需要了解:面向過程和面向對象的思維方式和解決問題的方法有著很大的差異——
算法 vs. 機制
面向過程解決問題的思路是算法或流程,你會把它想像為一條流水線,或者把自己看作親力親為的CPU。而面向對象著重于機制的建立,你可以把目標系統想像成一臺由許多零件構成的機器,或者一個良好運轉的組織。
所以,對于一個面向過程的程序,你需要理解的是事物處理過程或算法步驟,每個過程都是無狀態的,只有輸入和輸出(對于全局或靜態變量的訪問是面向過程的副作用)。
而對于一個面向對象系統,你需要首先理解一個設計的結構(類,類的職責,以及類之間的關系),很多在面向過程的代碼中必須用過程來描述的“流程”,比如一些分支邏輯判斷,在面向對象的系統中,靠類結構就已經解決了。在理解了結構之后,下一步需要了解的是類之間的交互。在明白了類結構之后,對于交互的理解就不再是件困難的事情。除非你的類結構本身就混亂,晦澀,難以理解。
你不妨想像一下,當你試圖了解某個機構一個具體事務流程的時候,最高效的方法肯定是先了解它的組織架構,了解每個部門職責,以及部門之間的關系。在此基礎上,再去理解一個具體事務的流程時,就會容易理解的多。反之,在你不了解組織架構的情況下,一上來就直奔一個具體事務,你可能更加希望一個部門,甚至一個人就把所有的事情都做了;拿著一份文件在各個部門之間穿梭蓋章,肯定會讓你非常困擾和厭煩。
分離 What & How
另外,對于餛飩函數的理解也需要不同的思維方式。以面條代碼面目出現的函數,不僅僅在描述“做什么(What)”,同時還會呈現“怎么做(How)”。因此,一個函數內部必然充斥著大量的實現細節,從而導致閱讀者只能依靠注釋,或者通過對細節的歸納總結,才能最終理解“What”。而餛飩函數則是將兩個關注點進行分離,在高層通過抽象來描述“What”,在底層通過展示細節來描述“How”,最終放在一起來完整描述一個算法。需要特別強調的是,為了能夠達到描述What的目的,好名字非常重要。
這樣的方式,應該更加科學,更加符合人類認知習慣。但同時也可以理解,對于某些已經了解了What,只想了解How的人,餛飩代碼會額外增加函數間跳躍的成本。
但世上沒有免費的晚餐,既然問題的本質復雜度就在那里,為之付出一定的代價就是必然的。 除非不再選擇程序員作為職業,否則,我們只能通過評估各個方案總體上的成本收益比,來選擇合適的方式。
餛沌代碼—— 更好的成本受益比
另外,一個必須承認的事實是,“復雜性”是損害“可理解性”的。面條代碼的復雜性體現在一個函數內部細節數量和邏輯控制,而餛飩代碼的復雜性則體現在函數或類的數量和結構 。
看起來我們只是將復雜性從一種形式轉化為另外一種形式,但事實上,餛飩代碼收獲了更多,它的意義不僅僅體現在可理解性,還體現在可重用性,靈活性,更加符合“高內聚,低耦合”原則。
所以,盡管 面條代碼在現實中廣泛存在,但對其卻是壓倒性多數的批評;而餛飩代碼,盡管也并不完美,卻在面向對象陣營得到廣泛的支持,甚至被列為整潔代碼的典范。
僅僅“小”是不夠的
“短小的函數”并不意味著餛飩代碼。在“高圈復雜度”被確認為是面條代碼的特征之后,很多團隊都定義了自己的“圈復雜度紅線”;另外,一些團隊也規定了單個函數“代碼行數”的上限。但這樣的約束,只能導致“短小的函數”,而“餛飩代碼”并不僅僅“短小”,還要松散耦合,還要表達清晰。
首先,即便一個函數只有一行代碼,但也會由于包含了過多的細節而難以理解。不信,看看這個例子:
#include <stdio.h> #include <math.h>double l; main(_,o,O){ return putchar((_--+22&&_+44&&main(_,-43,_),_&&o)?(main(-43,++o,O),((l=(o+21)/sqrt(3-O*22-O*O),l*l<4&&(fabs(((time(0)-607728)%2551443)/405859.-4.7+acos(l/2))<1.57))["#"])):10); }
這個程序絕對可以通過編譯鏈接,并且功能強大——能夠用ASCII畫出當前的月亮盈虧狀況。技術上這個程序的函數主體只有一行代碼,但其所包含的信息量之大,估計沒有幾個人僅僅靠閱讀和分析就可以完全理解。
這個例子可能有些極端,那我們不妨看一個正常的例子:
return (0 < width && width <= 100 && 0 < height &&height <= 75) ?height* width : 0;
這個例子并不非常晦澀,任意一個合格的程序員花點時間就能領會它的意圖。但如果我們將其改成下面的樣子,其容易理解的程度就得到了進一步的提高。
return isValid()? calcArea() : INVALID_AREA;
盡管我們通過提取函數和定義常量,增加了新的代碼元素,但這種付出是值得的。
另外,如果一個函數的表述不具備“對稱性”,或者不符合SLAP,那它就無法達到“抽象”與“細節”,“What”與“ How”分離的目標;就算這個函數非常短小,它也是晦澀的。
所以,我們真正的目標是“消除重復”,“清晰表達”,而不是“餛飩代碼”,更不是“短小函數”。“餛飩代碼”只是一個結果,而不是“動機”,而“短小函數”則只是“餛飩代碼”的特征之一。永遠“不要把解決問題的方法當作問題本身”。
消除不必要的復雜度
另外,由于“復雜性”會影響“可理解性”,所以,我們需要控制不必要的復雜度。那些不必要的抽象,不必要的函數,均不應該在一個設計中出現。所以,Kent Beck在“簡單設計”原則中描述:
如果一個代碼元素對于滿足功能,消除重復,或者提高表達力都沒有用處,那么它就不應該存在。
在重復沒有出現的情況下,對于“預先設計”所引入的復雜性,需要特別的小心和謹慎,究竟是這個“預先設計”更有價值,還是去除其引入的復雜性以提高“可理解性”更有價值?這需要設計者根據成本收益原則進行仔細的權衡。
但“重復”一旦出現,為了消除它而引入的復雜度,就是你必須要承受的代價。即便由此降低了“可理解性”,也物有所值。因為,一般而言“重復”比“難以理解”所帶來的后果更加嚴重:“重復”往往意味著設計上的問題,以及維護上的高昂成本 。
所以,“可重用性”和“可理解性”并非“正交”的兩個概念。但它們也并非相互排斥,水火不容。在“消除重復”的前提下,我們還是可以盡量提高代碼的“可理解性”,更何況,事實上很多時候,“消除重復”的過程就是“提高可理解性”的過程。只有在少數情況下,當它們發生沖突的時候,我們才需要在二者之間做出取舍 。總結
以上是生活随笔為你收集整理的面条代码 vs. 馄沌代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图片加水印怎么加?这篇文章告诉你
- 下一篇: python 多态 知乎_Python鸭