- 原文地址: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 ES6+ 學習函數語言程式設計和組合軟體技術的 “Composing Software” 系列介紹。還有更多關於這方面的內容! 下一篇 >
組合:“將部分或元素結合成整體的行為。” —— Dictionary.com
在我的高中第一堂程式設計課中,我被告知軟體開發是“把複雜問題分解成更小的問題,然後構建簡單的解決方案以得出複雜問題最終的解決方案的行為。”
我一生中最大的遺憾之一就是沒能很早認識到這堂課的重要性。我太晚才瞭解到軟體設計的本質。
我面試過數百名開發者。從這些對話中我瞭解到自己不是唯一(處於這種情況)的。極少工作軟體開發者能很好地抓住軟體開發的本質。他們不瞭解我們在使用的最重要工具,或者不知道如何充分利用它們。所有人都一直在努力回答軟體開發領域中這一個或兩個最重要的問題:
- 什麼是函式組合?
- 什麼是物件組合?
問題是你不能因為僅僅沒有意識它就躲避構建。你依然需要這樣做 —— 雖然你做的很糟糕。你編寫了帶有更多 bug 的程式碼,讓其他開發者很難理解。這是很大的問題,代價也很大。我們花費更多時間來維護軟體而不是從頭開始建立軟體,我們的這些 bug 會影響全球數十億人。
現今整個世界都執行在軟體上。每一輛新車都是一臺在車輪上的小型超級計算機,軟體設計的問題會導致真正的事故並且造成真正的生命損失。2013 年,一個陪審團發現 Toyota 的軟體團隊犯了“全然無視”的罪名,因為事故調查顯示它們有著 10,000 個全域性變數的麵條程式碼。
黑客和政府儲存漏洞為了監視人民,盜取信用卡,利用計算資源做分散式拒絕服務(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 會隱藏其中。
更少的程式碼 = 更少的錯誤覆蓋面積 = 更少的 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 上找到賣家。
類繼承只是一種複合物件結構。所有類生成複合物件,但不是所有的複合物件都是由類或者類繼承生成的。“在類繼承上支援物件組合”意味著你應該從小元件部分構建複合物件,而不是在類層次上從祖先繼承所有屬性。後者在物件導向設計中引起大量眾所周知的問題:
- 強耦合問題:因為子類依賴於父類的實現,類繼承是物件導向設計中最緊密的耦合。
- 脆弱的基類問題:由於強耦合,對基類的更改可能會破壞大量的後代類 —— 可能在第三方管理的程式碼中。作者可能會破壞掉他們不知道的程式碼。
- 不靈活的層次結構問題:對於單一的祖先分類法,給定足夠的時間和改進,所有的類分類法最終都是錯誤的新用例。
- 必要性重複問題:由於層次結構不靈活,新的用例通常是通過複製而不是擴充套件來實現,從而導致類似的類意外地的發散。一旦複製開始,就不清楚或者為什麼哪個新類應該從哪個類開始。
- 大猩猩/香蕉問題:”...面嚮物件語言的問題在於它們自身帶有所有隱含的環境。你想要的是一根香蕉,但你得到的是拿著香蕉的大猩猩和整個叢林。“ —— Joe Armstrong,"工作中的編碼員"。
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 上了解更多資訊
有關函式和物件組成的視訊課程可供 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。