高級程序員必須要會的五種編程范式
今天咱們來聊聊一個聽起來挺高大上的話題——編程范式。這詞兒聽起來可能有點唬人,但其實它就是描述編程時組織代碼的不同風格和方法。
我會盡量用簡單的話給大家解釋清楚,每種范式到底是怎么一回事。這樣,當別人說起“面向?qū)ο蟆?、“函?shù)式”或者“聲明式”這些詞兒時,你就能心領(lǐng)神會了。
這篇文章主要是個簡單的理論介紹,當然啦,咱們也會看一些偽代碼和實際的代碼示例。
咱們開始吧!
什么是編程范式?
所謂的編程范式,其實就是我們組織程序或者編程語言的不同方式和風格。每種范式都有自己的一套結(jié)構(gòu)、特性,以及解決常見編程問題的方法和觀點。
為啥會有這么多不同的編程范式呢?這問題其實和為啥有這么多編程語言差不多。不同的范式適合解決不同類型的問題,所以針對不同項目使用不同的范式是有意義的。
隨著時間的推移,軟件和硬件的進步也推動了不同方法的發(fā)展。再加上我們?nèi)祟惖膭?chuàng)造力,我們總喜歡創(chuàng)造新東西,改進前人的成果,把工具調(diào)整成我們喜歡的樣子,或者看起來更高效的方式。
所以,今天我們在編寫和組織程序時,有很多選擇。
編程范式不是什么
要明確一點,編程范式不是編程語言,也不是工具。你不能用范式來“構(gòu)建”任何東西。它們更像是一套理念和指導原則,是很多人達成共識、遵循并不斷發(fā)展的東西。
編程語言并不總是和某個特定的范式綁定在一起。有些語言在設(shè)計時就考慮了特定的范式,并且提供了更多促進該范式編程的特性(比如Haskell和函數(shù)式編程)。
但也有很多“多范式”的語言,意味著你可以根據(jù)自己的需要,讓代碼適應不同的范式(比如JavaScript和Python)。
同時,不同的編程范式并不是互斥的,你完全可以在同一個項目中同時使用來自不同范式的實踐。
我為啥要關(guān)心這個?
簡單來說,就是為了增加你的知識面。
詳細點說,我覺得了解編程的多種方法很有趣。探索這些話題可以幫助你開闊思維,跳出你已經(jīng)熟悉的工具和框架。
而且,這些術(shù)語在編程界經(jīng)常被提及,所以有一個基本的了解,將有助于你更好地理解其他相關(guān)的主題。
好了,既然我們已經(jīng)介紹了編程范式是什么和不是什么,接下來就讓我們一起來看看最流行的幾種范式,了解它們的主要特點,并進行比較。
要記住,這個列表并不全面。還有一些其他的編程范式?jīng)]有在這里涵蓋到,但我會介紹最流行和最廣泛使用的幾種。
命令式編程
命令式編程由一系列詳細的指令組成,這些指令按給定的順序提供給計算機執(zhí)行。它之所以被稱為“命令式”,是因為作為程序員,我們會非常具體地告訴計算機必須做什么。
命令式編程關(guān)注的是描述程序是如何一步步運作的。
假如你想烤一個蛋糕,你的命令式程序可能看起來像這樣(我可不是個厲害的廚師,所以別太苛刻哦 ??):
1. 在一個碗里倒入面粉
2. 往同一個碗里打入幾個雞蛋
3. 往同一個碗里倒些牛奶
4. 混合這些食材
5. 將混合物倒入模具
6. 烤上35分鐘
7. 讓它冷卻下來
用實際的代碼示例來說,假設(shè)我們要過濾一個數(shù)字數(shù)組,只保留大于5的元素。我們的命令式代碼可能長這樣:
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
const result = [];
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 5) result.push(nums[i]);
}
console.log(result); // 輸出:[6, 7, 8, 9]
我們告訴程序遍歷數(shù)組中的每個元素,將元素的值與5進行比較,如果元素大于5,就把它加入到新數(shù)組中。
我們的指令非常詳細具體,這就是命令式編程的核心。
過程式編程
過程式編程是命令式編程的一個延伸,它增加了函數(shù)(也稱為“過程”或“子程序”)的特性。
在過程式編程中,鼓勵用戶將程序執(zhí)行細分為函數(shù),作為提高模塊化和組織性的一種方式。
繼續(xù)我們的蛋糕例子,過程式編程可能是這樣的:
function pourIngredients() {
// 在一個碗里倒入面粉
// 往同一個碗里打入幾個雞蛋
// 往同一個碗里倒些牛奶
}
function mixAndTransferToMold() {
// 混合食材
// 將混合物倒入模具
}
function cookAndLetChill() {
// 烤上35分鐘
// 讓它冷卻下來
}
pourIngredients();
mixAndTransferToMold();
cookAndLetChill();
你可以看到,通過實現(xiàn)函數(shù),我們可以直接看文件末尾的三個函數(shù)調(diào)用,對我們的程序做什么就有了一個清晰的了解。
這種簡化和抽象是過程式編程的好處之一。但在函數(shù)內(nèi)部,我們?nèi)匀皇褂玫氖敲钍酱a。
函數(shù)式編程
函數(shù)式編程將函數(shù)的概念提升到了一個新的層次。
在函數(shù)式編程中,函數(shù)被視為一級公民,這意味著它們可以被賦值給變量,作為參數(shù)傳遞,也可以作為其他函數(shù)的返回值。
另一個關(guān)鍵概念是純函數(shù)。一個純函數(shù)只依賴于它的輸入來生成結(jié)果。給定相同的輸入,它總是產(chǎn)生相同的結(jié)果。此外,它不會產(chǎn)生任何副作用(即不會對函數(shù)外部的環(huán)境產(chǎn)生任何改變)。
有了這些概念,函數(shù)式編程鼓勵我們用函數(shù)來編寫大部分程序(驚訝吧??)。它還主張代碼的模塊化和無副作用,這使得在代碼庫中更容易識別和分離責任,從而提高了代碼的可維護性。
回到數(shù)組過濾的例子,我們可以看到,在命令式范式中,我們可能會使用一個外部變量來存儲函數(shù)的結(jié)果,這可以被視為一個副作用。
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
const result = []; // 外部變量
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 5) result.push(nums[i]);
}
console.log(result); // 輸出:[6, 7, 8, 9]
要將其轉(zhuǎn)換為函數(shù)式編程,我們可以這樣做:
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
function filterNums() {
const result = []; // 內(nèi)部變量
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 5) result.push(nums[i]);
}
return result;
}
console.log(filterNums()); // 輸出:[6, 7, 8, 9]
代碼幾乎一樣,但我們把迭代包裝在了一個函數(shù)里,并且在函數(shù)內(nèi)部也存儲了結(jié)果數(shù)組。這樣,我們可以確保函數(shù)不會修改它作用域之外的任何東西。它只創(chuàng)建了一個變量來處理它自己的信息,一旦執(zhí)行完成,那個變量也就不存在了。
聲明式編程
聲明式編程的核心是隱藏復雜性,讓編程語言更接近人類的語言和思維方式。它與命令式編程正好相反,因為程序員不需要給出關(guān)于計算機應該如何執(zhí)行任務(wù)的指令,而是關(guān)于需要什么結(jié)果。
舉個例子,用數(shù)組過濾的故事來說,聲明式的方法可能是這樣的:
const nums = [1, 4, 3, 6, 7, 8, 9, 2];
console.log(nums.filter(num => num > 5)); // 輸出:[6, 7, 8, 9]
看到?jīng)],用filter函數(shù)時,我們并沒有明確告訴計算機要遍歷數(shù)組或者把值存儲到另一個數(shù)組里。我們只是說出了我們想要什么("filter")以及滿足的條件("num > 5")。
這樣的好處是,代碼更容易閱讀和理解,通常也更簡短。JavaScript中的filter、map、reduce和sort函數(shù)就是聲明式代碼的很好例子。
另一個好例子是現(xiàn)代的JS框架/庫,比如React。看看這段代碼:
<button onClick={() => console.log('你點擊了我!')}>點擊我</button>
這里我們有一個按鈕元素,帶有一個事件監(jiān)聽器,當按鈕被點擊時,會觸發(fā)console.log函數(shù)。
React使用的JSX語法將HTML和JS混合在一起,這讓編寫應用程序變得更加簡單快捷。但實際上,瀏覽器并不會直接讀取和執(zhí)行這樣的代碼。React代碼最終會被轉(zhuǎn)譯成常規(guī)的HTML和JS,這才是瀏覽器真正運行的東西。
JSX是聲明式的,因為它的目的是為開發(fā)者提供一個更友好、更高效的工作接口。
關(guān)于聲明式編程,一個重要的事情是,計算機在背后實際上是將這些信息作為命令式代碼來處理的。
以數(shù)組為例,計算機仍然會像在for循環(huán)中那樣遍歷數(shù)組,但作為程序員,我們不需要直接編寫這些代碼。聲明式編程所做的,就是將那些復雜性從程序員的直接視野中隱藏起來。
面向?qū)ο缶幊?/span>
面向?qū)ο缶幊蹋∣OP)是最流行的編程范式之一。
OOP的核心概念是將關(guān)注點分離到被編碼為對象的實體中。每個實體都會組合一組特定的信息(屬性)和可以由實體執(zhí)行的操作(方法)。
OOP大量使用類(這是從程序員設(shè)置的藍圖或樣板開始創(chuàng)建新對象的一種方式)。從類創(chuàng)建的對象稱為實例。
繼續(xù)我們的偽代碼烹飪示例,假設(shè)在我們的面包店中,我們有一個主廚(叫Frank)和一個助理廚師(叫Anthony),他們每個人在烘焙過程中都有特定的責任。如果我們使用OOP,我們的程序可能看起來像這樣:
// 創(chuàng)建對應每個實體的兩個類
class Cook {
constructor(name) {
this.name = name;
}
mixAndBake() {
// 混合食材
// 將混合物倒入模具
// 烤35分鐘
}
}
class AssistantCook {
constructor(name) {
this.name = name;
}
pourIngredients() {
// 在一個碗里倒入面粉
// 在同一個碗里打入幾個雞蛋
// 在同一個碗里倒些牛奶
}
chillTheCake() {
// 讓其冷卻下來
}
}
// 從每個類實例化一個對象
const Frank = new Cook('Frank');
const Anthony = new AssistantCook('Anthony');
// 調(diào)用每個實例對應的方法
Anthony.pourIngredients();
Frank.mixAndBake();
Anthony.chillTheCake();
OOP的好處是,它通過明確的責任和關(guān)注點分離,促進了對程序的理解。
總結(jié)
正如我們所看到的,編程范式是我們面對編程問題的不同方式,以及組織我們代碼的方式。
命令式、過程式、函數(shù)式、聲明式和面向?qū)ο蠓妒绞墙裉熳钍軞g迎和廣泛使用的范式之一。了解它們的基礎(chǔ)知識對于一般知識和更好地理解編碼世界的其他主題都有好處。