- 原文地址:Composing Software: An Introduction
- 原文做者:[Eric Elliott](Eric Elliott)
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Sam
- 校對者:Mcskiller, CoderMing
Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)javascript
注意:這是關於從頭開始使用 JavaScript ES6+ 學習函數式編程和組合軟件技術的 「Composing Software」 系列介紹。還有更多關於這方面的內容! 下一篇 >html
組合:「將部分或元素結合成總體的行爲。」 —— Dictionary.com前端
在個人高中第一堂編程課中,我被告知軟件開發是「把複雜問題分解成更小的問題,而後構建簡單的解決方案以得出複雜問題最終的解決方案的行爲。」java
我一輩子中最大的遺憾之一就是沒能很早認識到這堂課的重要性。我太晚才瞭解到軟件設計的本質。android
我面試過數百名開發者。從這些對話中我瞭解到本身不是惟一(處於這種狀況)的。極少工做軟件開發者能很好地抓住軟件開發的本質。他們不瞭解咱們在使用的最重要工具,或者不知道如何充分利用它們。全部人都一直在努力回答軟件開發領域中這一個或兩個最重要的問題:ios
問題是你不能由於僅僅沒有意識它就躲避構建。你依然須要這樣作 —— 雖然你作的很糟糕。你編寫了帶有更多 bug 的代碼,讓其餘開發者很難理解。這是很大的問題,代價也很大。咱們花費更多時間來維護軟件而不是從頭開始建立軟件,咱們的這些 bug 會影響全球數十億人。git
現今整個世界都運行在軟件上。每一輛新車都是一臺在車輪上的小型超級計算機,軟件設計的問題會致使真正的事故而且形成真正的生命損失。2013 年,一個陪審團發現 Toyota 的軟件團隊犯了「全然無視」的罪名,由於事故調查顯示它們有着 10,000 個全局變量的麪條代碼。github
黑客和政府存儲漏洞爲了監視人民,盜取信用卡,利用計算資源作分佈式拒絕服務(DDoS)攻擊,破解密碼,甚至操縱選舉。面試
咱們必須作得更好才行。編程
若是你是一個軟件開發者,無論你知不知道,你天天都會編寫函數和數據結構。你能夠有意識地(而且更好地)作到這一點,或者你可能瘋狂的複製粘貼意外地作到這一點。
軟件開發的過程是把大問題拆分紅更小的問題,構建解決這些小問題的組件,而後把這些組件組合在一塊兒造成完整的應用程序。
函數組合是將一個函數應用於另外一個函數輸出結果的過程。在代數中,給出了兩個函數,f
和 g
,(f ∘ g)(x) = f(g(x))
。圓圈是組合運算符。它一般發音爲「複合(composed with)」或者「跟隨(after)」。你能夠像這樣大聲的念出來:「f
複合 g
等價於 f
是 g
關於 x
的函數」或者「f
跟隨 g
等價於 f
是 g
關於 x
的函數」。咱們說 f
跟隨 g
是由於先求解 g
,而後它的輸出做爲 f
的執行參數。
每次你像這樣編寫代碼時,你都在組合函數:
const g = n => n + 1;
const f = n => n * 2;
const doStuff = x => {
const afterG = g(x);
const afterF = f(afterG);
return afterF;
};
doStuff(20); // 42
複製代碼
每次你編寫一個 Promise 鏈,你都在組合函數:
const g = n => n + 1;
const f = n => n * 2;
const wait = time => new Promise(
(resolve, reject) => setTimeout(
resolve,
time
)
);
wait(300)
.then(() => 20)
.then(g)
.then(f)
.then(value => console.log(value)) // 42
;
複製代碼
一樣,每次你進行鏈式數組方法調用,lodash 庫的方法,observables(RxJS 等等)時,你在組合函數。若是你進行鏈式調用,你都在進行組合。若是你把函數返回值傳遞到另外一個函數中,你在進行組合。若是你順序的調用兩個方法,你使用 this
做爲輸入數據進行組合。
若是你在進行鏈式(調用),你即是在進行(函數)構建。
當你有意識地組合函數時,你會作得更好。
有意識地的組合使用函數,咱們能夠把 daStuff()
函數改進成簡單的一行(代碼):
const g = n => n + 1;
const f = n => n * 2;
const doStuffBetter = x => f(g(x));
doStuffBetter(20); // 42
複製代碼
這種形式的一個常見異議是調試起來比較困難。舉個例子,使用函數組合咱們該如何編寫這些內容?
const doStuff = x => {
const afterG = g(x);
console.log(`after g: ${ afterG }`);
const afterF = f(afterG);
console.log(`after f: ${ afterF }`);
return afterF;
};
doStuff(20); // =>
/*
"after g: 21"
"after f: 42"
*/
複製代碼
首先,讓咱們抽象出 「after f」 和 「after g」,定義一個名爲 trace()
的小功能:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
複製代碼
如今咱們能夠像這樣使用它:
const doStuff = x => {
const afterG = g(x);
trace('after g')(afterG);
const afterF = f(afterG);
trace('after f')(afterF);
return afterF;
};
doStuff(20); // =>
/*
"after g: 21"
"after f: 42"
*/
複製代碼
像 Lodash 和 Ramda 這些流行的函數式編程庫裏包含了更容易使用函數組合的實用程序。你能夠像這樣重寫上面的函數:
import pipe from 'lodash/fp/flow';
const doStuffBetter = pipe(
g,
trace('after g'),
f,
trace('after f')
);
doStuffBetter(20); // =>
/*
"after g: 21"
"after f: 42"
*/
複製代碼
若是你想在不導入內容的狀況下嘗試這些代碼,你能夠像這樣定義 pipe:
// pipe(...fns: [...Function]) => x => y
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
複製代碼
若是你不理解它是怎麼工做的也別擔憂。稍後咱們將會更詳盡的探索函數組合。事實上,它是如此的重要,你會在整個文檔中看到它屢次被定義和顯示。目的是幫助你熟悉它,知道它的定義和用法是自動的。讓你成爲組合家族的一份子。
pipe()
建立一個函數的管道(pepeline),把一個函數的輸出做爲另外一個函數的輸入。當你使用 pipe()
(和它的孿生方法 compose()
)時,你不須要中間變量。在不說起參數的狀況下編寫的函數成爲無值風格。爲此,你將調用一個返回新函數的函數,而不是顯示的聲明該函數。這意味着你不須要function
關鍵字或者箭頭語法(=>
)。
無值風格可能會佔用太多,但很好的一點是,那些中間變量給你的函數增長了沒必要要的複雜性。
下降複雜度有幾個好處:
在人類大腦工做記憶裏平均只有不多共享資源用於離散量子,而且每一個變量可能消耗其中一個量子。隨着你添加更多的變量,咱們準確回憶起每一個變量含義的能力會下降。工做記憶模型一般涵蓋 4-7 個離散量子。超過這些數字的話,(處理問題的)錯誤率急劇增長。
使用管道(pipe)模式,咱們消除了三個變量 —— 爲處理其餘事情釋放了將近一半可用的工做記憶。這顯著下降了咱們的認知負擔。相比於通常人,軟件開發者更傾向於將數據分塊到工做記憶中,但並非說會削弱保護的重要性。
簡潔的代碼也能夠提升你的代碼信噪比。這就像收聽收音機 —— 當收音機沒有調到正確的電臺時,會有不少干擾的噪音,而且很難聽到音樂。當你調到正確的電臺,噪音沒有了,而後你獲得更強的音樂信號。
代碼也是同樣的。更簡潔的代碼表達式能夠加強理解力。有些代碼給咱們提供有用的信息,而有些代碼只是佔用空間。若是你能夠減小使用代碼的量而不減小傳輸的含義,那麼你將使代碼更易於解析而且對於要閱讀代碼的其餘人來講也更好理解。
看看以前和以後的功能。看起來函數作了縮減而且減輕了不少代碼量。這很重要,由於額外的代碼意味着 bug 有額外的覆蓋面區域隱藏,這意味着更多的 bug 會隱藏其中。
更少的代碼 = 更少的錯誤覆蓋面積 = 更少的 bug。
「在類繼承上支持對象組合」,Gang of Four 說,「設計模式:可重用面向對象軟件的元素。」
「在計算機科學中,複合數據類型或組合數據類型是可使用編程語言原始數據類型和其餘複合類型構建的任何數據類型。[…] 構建複合類型的行爲稱爲組合。「 —— 維基百科
這些是原始值:
const firstName = 'Claude';
const lastName = 'Debussy';
複製代碼
這是一個複合值:
const fullName = {
firstName,
lastName
};
複製代碼
一樣,全部 Arrays、Sets、Maps、WeakMaps 和 TypedArrays 等都是複合數據類型。每次你構建任何非原始數據結構的時候,你都在執行某種對象組合。
請注意,Gang of Four 定義了一種稱爲複合模式的模式,它是一種特定類型的遞歸對象組合,容許你以相同的方式處理單個組件和聚合組合。有些開發者可能會感到困惑,認爲複合模式是對象組合的惟一形式。不要混淆。有不少不一樣種類的對象組合。
Gang of Four 繼續說道,「你將會看到對象組合在設計模式中一次又一次地被應用」,而後他們列出了三種對象組合關係,包括委託(在狀態,策略和觀察者模式中使用),結識(當一個對象經過引用知道另外一個對象時,一般是做爲一個參數傳遞:一個 uses-a 關係,例如,一個網絡請求處理程序可能會傳遞一個對記錄器的引用來記錄請求 —— 請求處理程序使用一個記錄器),和聚合(當子對象造成父對象一部分時:一個 has-a 關係,例如,DOM 子節點是 DOM 節點中的組件元素 —— DOM 節點擁有子節點)。
類繼承能夠用在構建複合對象,但這是一種充滿限制性和脆弱性的方法。當 Gang of Four 說「在類繼承上支持對象組合」時,他們建議你使用靈活的方式來構建複合對象,而不是使用剛性的,緊密耦合的類繼承方法。
咱們將使用「計算機科學中的分類方法:與拓撲相關的方面」(1989)中對象組成更通常的定義:
」經過將對象放在一塊兒造成複合對象,使得後者中的每個都是‘前者’的一部分。「
另外一個很好的參考是「經過複合設計可靠的軟件」,Glenford J Myers,1975年。這兩本書都已經絕版了,但若是你想在對象組成技術的主題上進行更深刻的探索,你仍然能夠在亞馬遜或者 eBay 上找到賣家。
類繼承只是一種複合對象結構。全部類生成複合對象,但不是全部的複合對象都是由類或者類繼承生成的。「在類繼承上支持對象組合」意味着你應該從小組件部分構建複合對象,而不是在類層次上從祖先繼承全部屬性。後者在面向對象設計中引發大量衆所周知的問題:
JavaScript 中最多見的對象組合形式稱爲對象連接(又稱混合組合)。它像冰淇淋同樣。你從一個對象(如香草冰淇淋)開始,而後混合你想要的功能。加入一些堅果,焦糖,巧克力漩渦,而後你會結出堅果焦糖巧克力漩渦冰淇淋。
使用類繼承構建複合:
class Foo {
constructor () {
this.a = 'a'
}
}
class Bar extends Foo {
constructor (options) {
super(options);
this.b = 'b'
}
}
const myBar = new Bar(); // {a: 'a', b: 'b'}
複製代碼
使用混合組合構建複合:
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
複製代碼
咱們稍後將更加深刻的探索其餘對象組合風格。目前,你的理解應該是:
這不是關於函數式編程(FP)和麪向對象編程(OOP)的比較,或者一種語言和另外一種語言的對比。組件能夠採用函數,數據結構,類等形式...不一樣的編程語言爲組件提供不一樣的原子元素。Java 提供類,Haskell 提供函數等等...但不管你喜歡什麼語言和範式,歸結到底,你都沒法擺脫編寫函數和數據結構。
咱們將討論不少關於函數式編程的知識,由於函數是用 JavaScript 編寫的最簡單的東西,而且函數式編程社區投入了大量時間和精力來形式化函數組合技術。
咱們不會作的是說函數式編程比面向對象編程更好,或者你必須擇其一。把 OOP 和 FP 作比較是一個錯誤的想法。就我近些年看到的每一個真正的 JavaScript 應用都普遍混合使用 FP 和 OOP。
咱們將使用對象組合來生成用於函數式編程的數據類型,以及用於爲 OOP 生成對象的函數式編程。
不管你如何編寫軟件,都應該把它寫得更好。
軟件開發的本質是組合。
不瞭解組合的軟件開發人員就像不知道螺栓和釘子的房屋建築師。在沒有組合意識的狀況下構建軟件就像一個房屋建築師把牆壁用膠帶和強力膠水捆綁在一塊兒。
是時候簡化了,簡化的最好方法就是了解本質。問題是,業內幾乎沒有人可以很好的掌握到最本質元素。就軟件行業來講,做爲一個開發者這算失敗的。但從行業的角度來看咱們有責任更好的培訓開發人員。咱們必須改進。咱們須要承擔責任。從經濟到醫療設備,今天全部的一切都運行在軟件上。在咱們星球上沒有人類生活的角落不受到軟件質量影響的。咱們須要知道咱們在作什麼。
是時候學習如何編寫軟件了。
有關函數和對象組成的視頻課程可供 EricElliottJS.com 的成員使用。若是你不是成員,請當即註冊。
Eric Elliott 是 「JavaScript 應用程序編程」(O'Reilly)和「和 Eric Elliott 一塊兒學習 JavaScript」的做者。他爲 Adobe Systems、Zumba Fitness、華爾街日報、ESPN、BBC 以及包括 Usher、Frank Ocean 和 Metallica 等在內的不少頂級錄音藝術家的軟件體驗作出了貢獻。
他與世界上最美麗的女人在任何地方遠程工做。
感謝 JS_Cheerleader。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。