模型描述的关系模式_你的项目该用哪种编程模式?
哎,一個1970年的問題,爭論了快50年了,還有那么多引戰的。
客觀一點講,對于玩過不少語言、大體上幾種模式都上過項目的我來講,幾種編程模式的本質問題都是管理問題。?
01面向過程,本質是“順序,循環,分支”
拉個小學生過來,十分鐘講明白,速度出項目,速度出東西。簡單明了,大家都不亂。但這么耿直的狀況,現在基本上已經不可能見到了。任何事物分解,用最簡單的面向過程方式分解,都會讓后期復雜度提升到一個爆炸的狀態。
面向過程開發,就像是總有人問你要后續的計劃一樣,下一步做什么,再下一步做什么。意外、事物中斷、突發事件,當今一切事物的復雜度,都不是“順序循環分支”幾句話能說清楚的。
來來來,幾十件事兒并發,多線程工作,你覺得用“順序循環分支”描述一個人、一個模塊,單線工作還好,稍微大一點兒,如果用這種最簡單的描述方式,要么幾乎無法使用,缺失細節太多,要么事無巨細,用最簡單的描述,描述到量級超出人類的掌控范圍。
這是個要么寫個小工具,要么寫個小玩具的代碼結構,玩玩就行了,上項目還是算了。有些穩定性要求極高,掌控度要求極高的特殊環境還可能會使用。
02面向對象,本質是“繼承,封裝,多態”
可以理解為將一切事物模塊化,不要問我你想做什么,怎么去做。而是計算手里有什么資源,有多少人,每個人會什么,每個人能做到什么。基于現狀,我們能完成什么。至于具體要干嘛,組合放這兒了,事物來了,就按照計劃,各自負責各自的。
上層回避下層細節,讓每一層中間層接觸理解的事物控制在很小的范圍內。很長一段時間里,大量的項目幾乎只有面向對象這一種模式。人腦同時能接收的信息是有限的,同時接收的信息過多,不是忘事兒漏事兒,就是漏洞百出。
面向對象的代碼結構,有效做到了層層分級、層層封裝,每一層只理解需要對接的部分,其他被封裝的細節不去考慮,有效控制了小范圍內信息量的爆炸。每個模塊,每個人做好自己的事兒就行了,自然運轉,來事兒了干事兒,沒事兒了閑置。然而當項目的復雜度超過一定程度的時候,會演變成一個完全無法管理的失控狀態。一個為了簡化思維出現的模式,嚴重增加了整個系統的復雜度。
第一個問題:模塊間對接的代價遠遠高于實體業務干活的代價。
類似金字塔管理模式,模塊數超出一個極限之后,最底層實現業務的部分寫不了幾十行。但是該模塊的上層對接,上層的上層對接,各種框架對接代碼作為中間層遠遠超出業務的執行。到了最后,所有逗逼程序猿都在研究結構怎么設計,框架怎么設計,應該使用哪種抽象,怎么抽象能簡化框架,簡化結構。
中間層、對接層的復雜度遠遠高于實體業務,已經沒有任何一個人,任何一個再牛的架構師知道整個項目的細節,已經沒有人能掌握,甚至因為復雜,因為不了解,已經幾乎無法改動了。
這也是亞馬遜的工程師描述的:“我的工作,就是每天進到一大片屎山里,然后進到屎山的中心,去找到底是哪里出了問題,為什么唯獨這一塊兒這么臭。。。”
第二個問題:代碼膨脹讓原本寫不了兩行的實體代碼膨脹到數十倍。
原本是有什么事兒,完成什么事兒。因為面向對象概念的層級劃分,要實現的業務需要封裝,封裝好跟父類對接。多繼承是萬惡之源,讓整個系統結構變成了網狀、環狀,最后變成一坨亂麻。
而單繼承,原本直接找業務代碼實現的事情,需要層層對接,層層調度。每一層對接代碼,都是要人寫的。雖然基本都是沒有任何技術難度的純敲,但是代碼量的提升,無論再簡單的業務,再簡單的結構,代碼量龐大本身就已經是一個重大的隱患性bug了。畢竟出問題了,讀下來、找問題也是要浪費大量時間的。
層級劃分的代碼量提升同時也造成了第三個問題。
第三個問題:過多的間接過程才能訪問到實體業務,嚴重影響性能,速度極慢。
要知道,《UNIX編程藝術》,第一原則就是KISS原則,整本書都貫徹了KISS原則。(keep it simple, stupid!)
寫項目、寫代碼,目的都是為了解決問題,寫出解決問題的產品。但是龐大的面向對象模型,讓你花費或者說浪費了過多的時間在考慮與要解決的問題完全無關,而且非常復雜的管理調度問題上。但是,面向對象模式雖然是個最容易出屎山的模式,但是在很大程度上,無論任何一種架構的引入,我們現代編程,項目里對接使用最多的,仍然是面向對象的工作模式。
大家都很痛苦,大家都知道問題在哪兒,大家都在罵面向對象,但畢竟沒有更好的解決方案,不是么?
03函數式編程本質是“函數映射”,通俗一點講叫規則制定
這個函數不是面向過程編程的函數,這個函數是數學概念里的函數。定義是這樣的:兩個非空集合A與B存在著對應關系f,而且對于A中的每一個元素x,B中總有唯一的一個元素y與它對應,就這種對應為從A到B的映射,記作函數f(A)。。。
這個東西理解起來比較麻煩,通俗易懂一點講,在我們嘗試設計了無數的語言模型,試圖用編程模式來描述事物的運轉,描述要做的復雜業務的執行模式,甚至描述這個世界本身的時候,從來都沒有人真正解決過,最后都倒在了人腦跟不上這一點。事物變得越龐大復雜,對事物復雜度的描述總是會越來越失控。
終于,1956年開始出現質疑,1960年lisp出現,1970年lisp混合使用函數式模型,在當時還只是理念。John Backus在他1977年的圖靈獎頒獎演講中正式提出函數式編程概念。而函數式編程所做的事情很簡單,就是放棄這些愚昧無知的人類創造的語言和概念,去使用“上帝”創世的時候一直在使用的語言“數學”。這個世界的復雜度遠遠高于任何一個計算機軟件工程。開始有人試圖理解并模仿世界最初的構造,去寫代碼,用這種方式構建項目。
抽象的東西不說那么多,說白了,模仿的方式就是用“數學”描述“規則制定”。有人會感覺,計算機編程不也是在用數學寫嗎,跟函數式編程非要單獨提出來的“數學”區別在哪兒?函數描述和語句描述最直觀的界限,我這里舉一個簡單而又熟悉的例子,就清楚了。
從前寫數學題的時候最經典的一個寫法。設x1=3,x2=9,y=x1+x2,得出y=12。如果類比到編程語言里,我們會感覺這種寫法很冗余,會變成let x1 = 3; return x1 = x1 + 9;這事兒看習慣代碼了覺得沒毛病,還原回數學模型,細思極恐。x1=3;x1 = x1 + 9???3= 3 + 9???計算機概念最初就引入了一個反數學模型的概念:變量。而變量的存在,是可以保存數據的。這個數據保存已經不是純粹的數學描述了。
函數式模型描述的是事物是怎么運行的,而不是事物運行本身。所謂怎么運行的,就像是寫一個數學公式,傳入參數,傳出參數。其本體是這個函數,而不是傳入的什么參數。而運行本身,最大的區別,在常規編程概念里叫“變量”,函數式編程里叫“副作用”。也就是是否將傳入的數據本身作為函數。
在常規編程模型里,變量是函數的一部分,但是在數學概念里,數學公式本身就是全部,不包含使用數學公式傳入的參數。這些是更本質的函數式編程的描述,單看這一部分,比前面的幾個模型概念已經復雜很多了。這些本身只是表象,后面說具體怎么解決問題的,舍棄變量的好處和代價又是什么呢?
計算機程序模塊因為有變量這個概念,模塊切分之后往往并不總是正確的。某些初始化沒執行,后面的模塊自己跑,因為數據沒構造、沒有數據結構等等問題,單獨截出來的部分直接是錯誤的。
而數學公式,無論從任何一個位置截取出來,公式的任何一個子公式都始終保持永遠是正確的。子公式帶入更復雜的公式,最終得到更龐大的復雜公式。這些過程無論是組合還是截取,永遠都保持著公式的正確性。
那么我們如果不出現這種類似,設x1 = 3;x1 = x1 + 9這種變量式代碼。保持高中寫題的模式,設x1=3;x2=x1+9;將第一個公式帶入第二個公式,x2=3+9這種思維模式,得到x2=12。所有代碼模塊,始終保持傳入參數,參數按照公式運行得出結果的這唯一的lambda表達式模型。
整個程序就變成了無數的,絕對正確的函數(公式),以及函數之間的互相帶入。系統的運轉就是傳入參數,得到結果。不存在變量狀態、變量管理等等無數的問題。相當于永遠設定各種各樣的規則,永遠不出現某規則里在什么具體的情況應該怎么怎么,而是讓該規則保持正確,剩余的一切事物只需要在該規則下自然運轉,不需要所謂的中間層管理層去做任何的程序運轉的干涉。
采用這種代碼設計模式,第一個特點,就是腦殼疼。最經典的一個例子,看下面這段代碼,函數返回從x加到y,x
function test (x, y) {
?????? let? res = 0;
?????? for(let i = x; i <= y; i++) {
????????????? res=res+i;
?????? }
?????? return? res;
}
這里已經違反了函數式編程模型,變量res是個副作用參數,i是個副作用參數。開頭設res=0了,res=res+x這數學模型已經被顛覆了。如果從中間截取任何一個流程,已經是錯的了。不引入變量,常規編程模型里,循環這個概念已經不可能實現了。我們的做法是:
function test_run (x, y) {
?????? return? test2 (x, y, x);
}
function? test2 (x, y, res) {
?????? if(y ===x) {
????????????? return? x;
?????? }else {
????????????? console.log(x, y-1, res+y);
????????????? return? test2 (x, y - 1, res+y);
?????? }
}
test_run (1, 100);
這個函數,除了x和y,沒有任何變量因素存在。這就是所謂“規則制定”最應該存在的狀態。無論在什么狀態下,無論在整個系統運行的哪一個步驟被截取出來,這個公式都是永遠正確的,所謂模仿數學模型的函數式模型。
上面這個已經不是循環了,畢竟循環這個概念存在最根本的基石就是變量。變量作為計數,才能從x加到y。這是個尾遞歸函數。
注意要認清楚,尾遞歸和遞歸本質上是兩個東西,是可以替代迭代循環的,而遞歸則不行。內存膨脹太嚴重,雖然尾遞歸看起來很像遞歸。
采用函數式編程極大減少了系統的復雜度,而且減少了運維成本。因為整個系統里沒有所謂的運行期,也就沒有運行期錯誤。中間層,對階層,管理模塊全部都可以刪除了。只要按照設定好的規則,只要不超出規則,就不需要太多管理。公式規則的設定不應該涉及實際運行過程中的細節變化問題,不應該出現在什么情況下要怎么樣,什么情況下怎么怎么的寫法。有且僅只有輸入和輸出以及公式的運轉邏輯,可以出現參數,但不允許出現副作用參數。只能以類似設x2=x1+9;x3=x1*x2這種寫法。
僅制定合理有效正確的規范,而不糾結規則規范下的具體運行。規則本身已經是最好的管理了。
函數式編程的第二個特點是:只要確保函數正確、函數運行正確、函數組合的復雜函數正確,你不需要關注函數在干什么,你只需要知道函數傳入什么,傳出什么,而且可以用任何多線程、多調用之類的方式造任何模式的函數。因為本身不存在變量概念,也就不存在多線程編程里最惡心的臨界區問題了。
函數式編程的第三個特點是:快。沒有中間商賺差價,沒有中間層瞎折騰,資源可以完全利用在該利用的地方,運行快。
函數式編程的第四個特點是:快。這個快,是寫代碼快。我們寫項目,其實根本就不是在寫業務。業務代碼兩三天敲完了,然后結構設計,框架設計,對接設計,數據管理,內存管理,調度管理,資源管理。這些都還好說,運行起來出bug,10%時間寫bug,90%時間調bug。(你是我們公司雇來專職寫bug的么?)
而函數式編程任何一個無副作用參數的模塊取出來,都是與數據本身無關的公式。只要你寫的最小的那個模塊沒錯,只要你沒寫錯邏輯,就不會出錯。
經常有說法,一個項目開發,C++和java要寫一年,C語言要寫5年,而lisp可能只需要三個月。這個對比有點夸大的成分,但單從代碼量上講,這個量級也差不多。
說著說著是不是感覺函數式編程就是未來,就是以后的趨勢了?是趨勢沒錯,肯定該學的都還是要學的。我要開始打臉了。
如果真的函數式編程這么好用,效果也這么好,真要是沒點兒致命的缺點,為什么50多年了,都還沒有那么普及,現在普及的還是面向對象呢。
缺點一:門檻太高,也就意味著你招不到人。
面向對象模式講究的是群體開發,每個人只需要關注你所在位置,你的眼前需要關注的問題,用封裝屏蔽底層運行細節的方式,簡化一次性接收的信息量。在寫好架構的情況下,分發架構文檔,多人同時開發。你優秀,把你水準拉低,封裝到代碼層內。你水平低,過不了TDD測試,只要能過,也出不了什么大問題,這是一種可以量產,可以找大量廉價勞動力堆出來一個能用的屎山的工作模式。同時不需要能力過高的人,不需要會很多,覆蓋面很廣的人。
有面向對象架構師一個人把握全局就可以了,其他人只用看著文檔、無腦堆代碼、過測試就行了。重復工作量幾百倍幾千倍的堆,跑不起來,浪費資源,那就拿錢砸。
要知道,拿錢砸硬件,是看得見的,而且是誠實的。一倍的錢,砸一倍的性能,不需要寫的太好。而人存在著無數不確定性,用錢很難直觀體現出來能力大小,增加算法復雜度,讓水平低的看不懂,導致各種問題,還不如用錢砸硬件。
水平拉低的另一個好處,招人好招,都可以互相替代,難度都不大。找個能加班的,比找個能力強的有用。
面向對象開發模式是非常適合商業運作的開發模式。而函數式編程的門檻,我就兩個字,“數學?”,呵呵。。。
function test_run (x, y) {
?????? return? test2 (x, y, x);
}
function? test2 (x, y, res) {
?????? if(y === x) {
?????? ?????? return? x;
?????? }else {
????????????? console.log(x, y-1, res+y);
????????????? return? test2 (x, y - 1, res+y);
?????? }
}
test_run (1, 100);
就上面這個東西,一個簡單的從1加到100,能看出來這是個循環的,估計都已經不好招了。
缺點二:慢。。。
上面說快,這邊開始打臉。我說的慢,真的就是運行起來,運行資源占用導致的慢。舉個例子,現在神經網絡算法非常火,有幾個真的知道神經網絡算法是什么的?這個名詞其實根本沒那么玄幻,簡單一點說,神經網絡算法的本質就是多項式分解。
最常見的多項式分解,f(x)=an·x^n+an-1·x^(n-1)+…+a2·x^2+a1·x+a0,這個東西還有三角多項式分解什么的,都學過沒什么好介紹的。
多項式分解就是使用n次冪的多項式,可以描述任何一個函數,可以將任何一個函數分解為無限次冪的多項式函數。神經網絡就是能用神經網絡加權算法描述任何一個函數,可以將任何一個函數分解為無限級神經網絡加權的多項式函數。但是為什么不用初中還是高中就學過的多項式分解,一定要引入一個這么復雜的神經網絡,還這么火呢?
說白了,計算機說起來很強大,我們好像都覺得CPU很強大。學計算機的都知道,計算機CPU這個寄存器設計能做的事情很有限,根本就不強大。多項式分解這種東西,你教一個初中生算,現在大體上都會,貌似是不是小學都開始教這個了?但是你讓計算機算多項式分解,用了無數的套路才抽象出一個近似的多級的冪運算,或者多級的sin,cos運算。
神經網絡算法,每一個神經元節點的加權運算,看起來輸入輸出權值運算比多項式分解復雜多了,但實際上每個神經元的權值運算,計算機是算的動的,而冪運算,計算機代價無法估量了。完全以數學思維寫代碼,帶入的某些數學概念,在計算機上運行時,因為不擅長,會多出來大量額外的工作,而且可能是近似,永遠都算不對。
畢竟某種意義上來說,計算機概念使用的“浮點數”這個概念,已經是算個近似,注定永遠都不可能像數學一樣準確運算了。而如果用其他代碼回避浮點數概念,增加其他機制,復雜度和資源占用又要開始爆表了。再說2^32,2^64這幾個寄存器限制,大數運算,僅僅是加減法,可能都已經是一個很復雜的運算了。這些問題如果放在真正的數學運算里原本是不需要考慮的。
再舉個簡單的例子,計算機根本就不會算除法。
我們當今世界的網絡安全基礎,就是RSA算法,誰都解不開。肯定不是真的解不開,只是因為計算機不會算除法,兩個大質數相乘的乘積,除開這件事情,是一個世界難題。有些事情其實是細思極恐,你會算除法么?除法是什么?你會的除法運算是不是只是背了個乘法表,然后反過來背著乘法表,撞除法運算。
如果真的拿數學理念上,讓計算機這種低級CPU的一個字節8位去算,來上幾次方,幾個大數,幾十行代碼,能給你把超級服務器算崩潰。但凡你覺得很簡單的概念上到計算機上,算法的膨脹,資源的消耗很可能在你沒注意的,莫名其妙的梗上卡死。
缺點三:慢。。。?
這個慢,對應上面的開發效率慢、寫代碼慢。
說白了,本質上還是門檻太高。你招一批水平一般的面向對象程序員,設計好結構,呼呼啦啦都開始敲,敲了幾千上萬行代碼,事兒搞定了。從代碼量上說很龐大,解決一個很小的事兒,效率很低。實際上呢?你找了個函數式開發的高手,蹲在哪兒開始思考,寫寫刪刪,測測改改。幾十行,百十來行寫完項目。這個開發效率高?而且相對還很有意思,動腦程度很高?
如果他沒考慮清楚呢?如果這哥們兒家里有事兒呢?如果這哥們兒生病了呢?如果這哥們兒技術其實沒那么屌,腦子都快想炸了,憋不出來呢?
如果團隊里水平都很高,都有很深厚的底子。項目開發效率會高,但真實情況是,這種條件你根本找不來。而哪怕你找來了,也沒什么用,這就是第四個缺點了。
缺點四:lisp詛咒(The Lisp Curse)?
關于這個名詞,有很多文章都有講,也是幾十年來無數引戰帖討論過的問題。這個又是個描述起來比較麻煩的概念,同時也是我確信,哪怕以后程序員水準普遍提高,函數式編程再優秀,都永遠都是小眾,而且不建議鋪開了上項目使用的最重要的原因。
對于lisp詛咒最準確的描述,也是大眾最容易理解的描述,是這樣的。Lisp是如此強大,以至于在其他編程語言中的技術問題,到了Lisp中變成了社會問題。
是的,你沒有看錯,不是技術問題,是社會問題。社會問題跟技術問題最大的區分點,是“人”。技術沒問題,技術很強大,但是強大的技術模式,必然會導致人的分裂,社區的分裂,以至于函數式開發模型最后一定會死在技術無關的“管理問題”上。其中lisp詛咒導致的最嚴重的問題就是,“孤狼式開發”。
我舉個例子:面向對象只是一種設計思路,是一種概念,并沒有說什么C++是面向對象的語言,java是面向對象的語言。
C語言一樣可以是面向對象的語言,Linux內核就是面向對象的原生GNU C89編寫的,但是為了支持面向對象的開發模式,Linux內核編寫了大量概念維護modules,維護struct的函數指針,內核驅動裝載等等機制。而C++和java為了增加面向對象的寫法,直接給編譯器加了一堆語法糖。
而lisp、Haskell,在純粹的函數式編程模型下,寫出來一整套支持面向對象的概念和框架,需要多少代碼呢?幾百行。。。
這種開發優勢最終導致了社區里有無數套隨手寫的、低質量的、無人維護的、bug無數的垃圾面向對象框架。也正因為如此,你只能自己寫一套面向對象的框架,只要你理論知識扎實,估計寫不了幾百行,也就是一天兩天的事兒。
每個人都在使用自己寫的面向對象,每一個人都在使用自己寫的庫,而且上下是斷層的。層次高的,寫幾套自己項目用的依賴沒什么難度,卻沒有別人用。時間久了,很容易消失。項目交接的時候,大量無人維護的,個人開發的依賴,代碼量很小,功能很強大,但是難度太大。項目交接幾乎無法進行,除非來一個大牛接手,但同樣的,如果不是這種方式,一旦技術斷層,項目再也沒人能看得懂了,最好的辦法不是撞大運的招人,而是把看不懂,又不得不改的部分刪了重寫。
最終導致,邏輯過于復雜的函數式編程,總是會出現技術人員斷層,畢竟能力參差不齊,一次性技術門檻提的過高了。還能維護的項目,幾乎都是一個人寫出來維護的項目,哪怕第二個人的存在,可能都只是暫時的。“孤狼式開發”,這個名字起的是非常到位的。
最后,總結一下,也是軟件項目管理經典教材《人月神話》上講的,“沒有銀彈”。
在什么情況使用什么樣的模式。不要太自信,不要太相信自己習慣使用的開發模式和框架。你只會一種模式,怎么對比?你只會一種語言,怎么對比?
別爭論了,讓他們最后各歸各位
在驅動開發、嵌入式底層開發這些地方,面向過程開發模式,干凈,利索,直觀,資源掌控度高。在這些環境,面向過程開發幾乎是無可替代的。
在工作量大,難度較低、細節過多、用簡單的規范規則無法面面俱到的環境下,用面向對象開發模式,用低質量人力砸出來產業化項目。
在復雜度過高、層級過高的架構頂層、面向對象模式增加的對接復雜度已經開始接近失控的環境下,把上層的中間層代碼都刪了,讓少數精英使用制定規范的函數式模型,函數式編程的引入則能大大簡化項目復雜度和開發成本。
要學的東西還很多,學無止境吧~
1.這才是大學教育該有的樣子!
2.“玄鐵”之后是“無劍”:阿里平頭哥發布AIoT芯片平臺,欲引領芯片開發新模式
3.傳統的嵌入式C語言程序員快要滅絕了?
4.華為釋放信號:若ARM受限,考慮用RISC-V架構
5.一位嵌入式工程師的硬核單片機編程思想!
6.讓嵌入式開發事半功倍,這些工具你備齊了嗎?
總結
以上是生活随笔為你收集整理的模型描述的关系模式_你的项目该用哪种编程模式?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用服务器控件在后台调用前台客户端JS方法
- 下一篇: g++编译c++11 thread报错问