命令式、宣告式、物件導向、函式式、控制反轉之華山論劍(上)
我接觸程式設計比較晚,從自學java開始,物件導向的思想就已經深入骨髓。之前那些年,我的程式碼只有這一種編碼風格。
這些年來,js發生了翻天覆地的變化,前端已經遠不是那個dom橫行,ajax呼叫介面的時代。資料驅動、函式式(宣告式程式設計)、工程化、Node、狀態管理等大量新興的技術進入眼簾。我們親眼見證前端程式碼從物件導向到函式式的轉變,從抵制到接受,從學習到驚歎,驚歎一等物件的神奇,驚歎僅僅宣告配置就可以完成功能,驚歎js居然有這樣的高玩。
當然,我也見過太多的人對函式式嗤之以鼻,覺得函數語言程式設計難以維護,在業務複雜的場景下容易形成維護噩夢(asserts hell)。今天,以我個人的立場(完全中立,不帶任何面癱色彩),就函數語言程式設計與物件導向程式設計做簡單的博弈,順便介紹下從Java中spring框架就開始興起的控制反轉思想,這種思想的兩個組成部分依賴注入和依賴收集正式大名鼎鼎的angular的mvvm的實現原理。
命令式與宣告式的區別
我們有兩種程式設計方式:命令式和宣告式。物件導向程式設計屬於命令程式設計與宣告式的結合。
我們可以像下面這樣定義它們之間的不同:
- 指令式程式設計:命令“機器”如何去做事情(how),這樣不管你想要的是什麼(what),它都會按照你的命令實現。
- 宣告式程式設計:告訴“機器”你想要的是什麼(what),讓機器想出如何去做(how)。
舉個簡單的例子,假設我們想讓一個陣列裡的數值翻倍。
命令式:
var numbers = [1,2,3,4,5]
var doubled = []
for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push (newNumber)
}
console.log (doubled) //=> [2,4,6,8,10]複製程式碼
宣告式
var numbers = [1,2,3,4,5]
var doubled = numbers.map (function (n) {
return n * 2
})
console.log (doubled) //=> [2,4,6,8,10]複製程式碼
map函式所做的事情是將直接遍歷整個陣列的過程歸納抽離出來,讓我們專注於描述我們想要的是什麼(what)。注意,我們傳入map的是一個純函式;它不具有任何副作用(不會改變外部狀態),它只是接收一個數字,返回乘以二後的值。
在一些具有函數語言程式設計特徵的語言裡,對於 list資料型別的操作,還有一些其他常用的宣告式的函式方法。例如,求一個list裡所有值的和,指令式程式設計會這樣做:
var numbers = [1,2,3,4,5]
var total = 0 for(var i = 0; i < numbers.length; i++) {
total += numbers[i]
}
console.log (total) //=> 15複製程式碼
而在宣告式程式設計方式裡,我們使用reduce函式:
var numbers = [1,2,3,4,5]
var total = numbers.reduce (function (sum, n) {
return sum + n
});
console.log (total) //=> 15複製程式碼
reduce函式利用傳入的函式把一個list運算成一個值。它以這個函式為引數,陣列裡的每個元素都要經過它的處理。每一次呼叫,第一個引數(這裡是sum)都是這個函式處理前一個值時返回的結果,而第二個引數(n)就是當前元素。這樣下來,每此處理的新元素都會合計到sum中,最終我們得到的是整個陣列的和
。
同樣,reduce函式歸納抽離了我們如何遍歷陣列和狀態管理部分的實現,提供給我們一個通用的方式來把一個list合併成一個值。我們需要做的只是指明我們想要的是什麼?
宣告式程式設計為什麼讓某些人疑惑,不屑,甚至排斥?
從宣告式程式設計誕生的那天起,對宣告式程式設計與指令式程式設計的討論就沒有停止過。作為程式設計師,我們非常習慣去命令計算機去做某些事情。遍歷列表,判斷,賦值已經是我們邏輯中最常見的程式碼。
在很多情況中,指令式程式設計確實非常直觀、簡單並且編碼執行效率最高,最重要的,維護的人也非常容易理解。加上大多數人並不理解函式的本質,只能把邏輯與資料封裝到一個個物件中,以上的種種原因,導致宣告式程式設計一直沒有成為主流的程式設計模式。甚至有人覺得宣告式程式設計是反人類思維模式的程式設計,只是為了寫一些所謂高大上的“玩具”產生的模式。
如果我們花時間去學習宣告式的可以歸納抽離的部分,它們能為我們的程式設計帶來巨大的便捷。首先,我可以少寫程式碼,這就是通往成功的捷徑。其次,我們可以抽象出非常實用的工具類,對物件或者函式進行深度加工,巢狀,運算,直到得到想要的結果。最後,每當有需求變更時候,大多數情況下,我們無需改寫框架(宣告分析)程式碼,只需要修改宣告的配置即可完成需求變更。
最重要的,它們能讓我們站在更高的層面是思考,站在雲端思考我們想要的是什麼,什麼是變化的,什麼是不變的,找到變化,配置之,找到不變,封裝之。最後你會發現,我們不關心變化,因為變化的通過配置來宣告,我們只關心不變,也就是框架,用框架(不變)來處理宣告(變化),正如道家的哲學,以不變(框架)應萬變(宣告)。而不是站在底層,思考事情該如何去做。
(通常來說,核心的架構師編寫不變的框架,低P/T編寫配置宣告,不要以為配置僅僅是json等格式,在函數語言程式設計裡,配置往往是函式/類或者任何物件)
物件導向程式設計與函數語言程式設計
物件導向
將現實世界的物體抽象成類,每個物體抽象成物件。用繼承來維護物體的關係,用封裝來描述物體的資料(屬性)與行為(方法),通過封裝技術,訊息機制可以像搭積木的一樣快速開發出一個全新的系統。既可以提高程式設計效率,又增強了程式碼的可擴充套件/維護等靈活性,是世界上運用最廣泛的程式設計方法(個人觀點:沒有之一)。
面嚮物件語言是指令式程式設計的一種抽象。抽象包括兩方面,資料抽象與過程抽象。在JS中,物件導向程式設計(也就是我們常說的基於物件,因為JS並不是物件導向的語言)把邏輯與資料封裝到函式與原型中,通過函式的原型鏈拷貝實現繼承,而程式碼的執行邏輯與資料依然封裝在函式內,但是做了屬性與方法的區分。優秀的物件導向程式設計顯然可以做到宣告式程式設計,也就是根據宣告配置生成結果(也就是說,物件導向程式設計的邏輯是預設的,我們可以根據輸入條件,判斷走不同的邏輯)。
但是絕大多數的物件導向程式設計,不會根據宣告配置去生成邏輯,邏輯的呼叫是封裝在物件中,而不是動態生成。所以並沒有做到真正的宣告式,也就是資料與邏輯完全分離。這裡所說的動態生成邏輯,是根據宣告,自動完成邏輯的生成,這樣就完全可以不用編寫業務程式碼,而僅僅靠宣告來完成邏輯的實現,而這部分處理,交給框架處理即可。
函數語言程式設計
把邏輯完全視為函式的計算。把資料與邏輯封裝到函式中,通過對函式的計算,加工,處理,來生成新的函式,最後拼裝成一個個功能獨立的函式。在運用這些函式,完成複雜邏輯的實現。
與現象物件不同的是,我們把資料和邏輯封裝到函式中而不是類與物件中。每個函式完全獨立,好的函式式設計,每個函式都是一個純函式(pure function,即輸入固定引數,即可得到相同輸入的函式)。優點是:
- 物件導向中的任何一個原型方法(prototype)都會獲得this的資料,而且可以輕易獲取閉包的資料。這樣的非純函式讓我們非常難以提煉與抽象。
- 純函式由於輸入與輸出固定,所以變得非常容易單測。好的函式式中的函式設計,不會依賴於任何其他函式或者宣告配置,只需要傳遞引數,既可以進行測試。而在面嚮物件語言中,我們往往需要啟動整個工程,或者說所有依賴的類全部要載入,才能開始測試。
- 對邏輯做抽象與提取,讓我們避免在函式內做判斷與迴圈,我們只需要把具體處理封裝到函式中,而程式執行過程中的走向、判斷與迴圈通常交給底層框架來處理。這讓我們完全有能力動態生成邏輯。比如大名鼎鼎的d3和rx,邏輯與邏輯處理的程式碼完全分離,程式碼可讀性非常高。
既然本文介紹的主要是函數語言程式設計,所以主觀評價了函式式的優點。當然物件導向的程式設計模式優點更加突出,各位客官已經非常熟悉封裝、繼承、多型給我們帶來的優點,程式碼可讀性與可維護性在所有模式中名列前茅,物件導向程式設計位列神壇已久,在此不必多言。