原文連接:Functional Mixins
譯者注:在編程中,mixin 相似於一個固有名詞,能夠理解爲混合或混入,一般不進行直譯,本文也是一樣。javascript這是「軟件構建」系列教程的一部分,該系列主要從 JavaScript ES6+ 中學習函數式編程,以及軟件構建技術。敬請關注。
上一篇 | 第一篇java
Mixin 函數 是指可以給對象添加屬性或行爲,並能夠經過管道鏈接在一塊兒的組合工廠函數,就如同流水線上的工人。Mixin 函數不依賴或要求一個基礎工廠或構造函數:簡單地將任意一個對象傳入一個 mixin,就會獲得一個加強以後的對象。編程
Mixin 函數的特色:設計模式
數據封裝數組
繼承私有狀態瀏覽器
多繼承安全
覆蓋重複屬性數據結構
無需基礎類閉包
現代軟件開發的核心就是組合:咱們將一個龐大複雜的問題,分解成更小,更簡單的問題,最終將這些問題的解決辦法組合起來就變成了一個應用程序。框架
組合的最小單位就是如下二者之一:
函數
數據結構
他們的組合就定義了應用的結構。
一般,組合對象由類繼承實現,其中子類從父類繼承其大部分功能,並擴展或覆蓋部分。這種方法致使了 is-a 問題,好比:管理員是一名員工,這引起了許多設計問題:
高耦合:因爲子類的實現依賴於父類,因此類繼承是面向對象設計中最緊密的耦合。
脆弱的子類:因爲高耦合,對父類的修改可能會破壞子類。軟件做者可能在不知情的狀況下破壞了第三方管理的代碼。
層次不靈活:根據單一祖先分類,隨着長時間的演變,最終全部的類都將不適用於新用例。
重複問題:因爲層次不靈活,新用例一般是經過重複而不是擴展來實現的,這致使不一樣的類有着類似的類結構。而一旦重複建立,在建立其子類時,該繼承自哪一個類以及爲何繼承於這個類就不清晰了。
大猩猩和香蕉問題:「...面嚮對象語言的問題是他們會得到全部與之相關的隱含環境。好比你想要一個香蕉,但你獲得的會是一隻拿着香蕉的大猩猩,以及一整片叢林。」 - Joe Armstrong(Coders at Work)
假設管理員是一名員工,你如何處理聘請外部顧問暫時行使管理員職務的狀況?(譯者:木知啊~)若是你事先知道全部的需求,類繼承可能有效,但我從沒有看到過這種狀況。隨着不斷地使用,新問題和更有效的流程將會被發現,應用程序和需求不可避免地隨着時間的推移而發展和演變。
Mixin 提供了更靈活的方法。
「組合優於繼承。」 - 設計模式:可重用面向對象軟件的元素
Mixin 是對象組合的一種,它將部分特性混入複合對象中,使得這些屬性成爲複合對象的屬性。
面向對象編程中的 "mixin" 一詞來源於冰激凌店。不一樣於將不一樣口味的冰激凌預先混合,每一個顧客能夠自由混合各類口味的冰激凌,從而創造出屬於本身的冰激凌口味。
對象 mixin 與之相似:從一個空對象開始,而後一步步擴展它。因爲 JavaScript 支持動態對象擴展,因此在 JavaScript 中使用對象 mixin 是很是簡單的。它也是 JavaScript 中最多見的繼承形式,來看一個例子:
const chocolate = { hasChocolate: () => true }; const caramelSwirl = { hasCaramelSwirl: () => true }; const pecans = { hasPecans: () => true }; const iceCream = Object.assign({}, chocolate, caramelSwirl, pecans); /* // 支持對象擴展符的話也能夠寫成這樣... const iceCream = {...chocolate, ...caramelSwirl, ...pecans}; */ console.log(` hasChocolate: ${ iceCream.hasChocolate() } hasCaramelSwirl: ${ iceCream.hasCaramelSwirl() } hasPecans: ${ iceCream.hasPecans() } `); /* 輸出 hasChocolate: true hasCaramelSwirl: true hasPecans: true */
函數繼承是指經過函數來加強對象實例實現特性繼承的過程。該函數創建一個閉包使得部分數據是私有的,並經過動態對象擴展使得對象實例擁有新的屬性和方法。
來看一下這個詞的創造者 Douglas Crockford 所給出的例子。
// 父類 function base(spec) { var that = {}; // Create an empty object that.name = spec.name; // Add it a "name" property return that; // Return the object } // 子類 function child(spec) { // 調用父類構造函數 var that = base(spec); that.sayHello = function() { // Augment that object return 'Hello, I\'m ' + that.name; }; return that; // Return it } // Usage var result = child({ name: 'a functional object' }); console.log(result.sayHello()); // "Hello, I'm a functional object"
因爲 child()
同 base()
緊密耦合在一塊兒,當你想添加 grandchild()
, greatGrandchild()
等時,你將面對類繼承中許多常見的問題。
Mixin 函數是一系列將新的屬性或行爲混入特定對象的組合函數。它不依賴或須要一個基礎工廠方法或構造器,只需將任意對象傳入一個 mixin 方法,它就會被擴展。
來看下面的例子。
const flying = o => { let isFlying = false; return Object.assign({}, o, { fly () { isFlying = true; return this; }, isFlying: () => isFlying, land () { isFlying = false; return this; } }); }; const bird = flying({}); console.log( bird.isFlying() ); // false console.log( bird.fly().isFlying() ); // true
這裏須要注意,當調用 flying()
時須要傳遞一個被擴展的對象。Mixin 函數被設計用來實現函數組合,繼續看下去。
const quacking = quack => o => Object.assign({}, o, { quack: () => quack }); const quacker = quacking('Quack!')({}); console.log( quacker.quack() ); // 'Quack!'
經過簡單的函數組合就能夠將 mixin 函數組合起來。
const createDuck = quack => quacking(quack)(flying({})); const duck = createDuck('Quack!'); console.log(duck.fly().quack());
可是,這看上去有點醜陋,調試或從新排列組合順序也有點困難。
固然,這只是標準的函數組合,而咱們能夠經過一些好的辦法來將它們組合起來,好比 compose()
或 pipe()
。若是,使用 pipe()
就需反轉函數的調用順序,才能保持相同的執行順序。當屬性衝突時,最後的屬性生效。
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x); // OR... // import pipe from `lodash/fp/flow`; const createDuck = quack => pipe( flying, quacking(quack) )({}); const duck = createDuck('Quack!'); console.log(duck.fly().quack());
你應當老是使用最簡單的抽象來解決問題。從純函數開始。若是須要一個持久化狀態的對象,就試試工廠方法。若是你須要構建更復雜的對象,那就試試 Mixin 函數。
如下是一些使用 Mixin 函數很棒的例子:
應用狀態管理,好比,Redux
某些橫向服務,好比,集中日誌處理
組件生命週期函數
功能可組合的數據類型,好比,JavaScript Array
類實現了 Semigroup
, Functor
, Foldable
一些代數結構能夠根據其餘代數結構得出,這意味着新的數據類型能夠經過某些推導組合而成,而不須要定製。
大部分問題均可以使用純函數優雅地解決。然而,mixin 函數同類繼承同樣,會形成一些問題。事實上,使用 mixin 函數可以徹底複製類繼承的優缺點。
你應當遵循如下的建議來避免這些問題。
使用最簡單的實現。從左邊開始,根據須要移到右邊。純函數 > 工廠方法 > mixin 函數 > 類繼承
避免建立對象,mixin,或數據類型之間的 is-a 關係
避免 mixins 之間的隱含依賴關係,mixin 函數應當是獨立的
mixin 函數並不意味着函數式編程
在 JavaScript 中,類繼承在極少狀況下(也許永遠不)會是最佳方案,但這一般是一些不禁你控制的庫或框架。在這種場景下,類有時是實用的。
無需擴展你本身的類(不須要你創建多層次的類結構)
無需使用 new
關鍵字,也就是說,框架會替你實例化
Angular 2+ 和 React 知足這些需求,因此你無需擴展你本身的類,而是放心地使用它們的類。在 React 中,你能夠不使用類,不過這樣你的組件將不會得到 React 的優化,而且你的組件也會同文檔中的例子不一樣。但不管如何,使用函數構建 React 組件老是你的首選。
在一些瀏覽器中,類會得到 JavaScript 引擎的優化,其餘的則沒法直接使用。在幾乎全部狀況下,這些優化都不會對程序產生決定性的影響。事實上,在接下去的幾年中,你都無需關心類在性能上的不一樣。不管你如何構建對象,對象建立和屬性訪問老是很是快的(每秒百萬次)。
也就是說,相似 RxJS,Lodash 等公共庫的做者應該研究使用 class
建立對象實例可能的性能優點。除非你可以證實經過類可以解決性能瓶頸,不然,你就應當使你的代碼保持乾淨、靈活,而沒必要擔憂性能。
你可能打算建立一些計劃用於一同工做的 mixin 函數。試想一下,你想要爲你的應用添加一個配置管理器,當你訪問不存在的配置屬性時,它會提示警告,像這樣:
// log 模塊 const withLogging = logger => o => Object.assign({}, o, { log (text) { logger(text) } }); // 確認配置項存在模塊,同 log 模塊無關,這裏只是確保 log 存在 const withConfig = config => (o = { log: (text = '') => console.log(text) }) => Object.assign({}, o, { get (key) { return config[key] == undefined ? // vvv 隱式依賴! vvv this.log(`Missing config key: ${ key }`) : // ^^^ 隱式依賴! ^^^ config[key] ; } }); // 模塊封裝 const createConfig = ({ initialConfig, logger }) => pipe( withLogging(logger), withConfig(initialConfig) )({}) ; // 調用 const initialConfig = { host: 'localhost' }; const logger = console.log.bind(console); const config = createConfig({initialConfig, logger}); console.log(config.get('host')); // 'localhost' config.get('notThere'); // 'Missing config key: notThere'
也能夠是這樣,
// 引入 log 模塊 import withLogging from './with-logging'; const addConfig = config => o => Object.assign({}, o, { get (key) { return config[key] == undefined ? this.log(`Missing config key: ${ key }`) : config[key] ; } }); const withConfig = ({ initialConfig, logger }) => o => pipe( // vvv 明確的依賴! vvv withLogging(logger), // ^^^ 明確的依賴! ^^^ addConfig(initialConfig) )(o) ; // 工廠方法 const createConfig = ({ initialConfig, logger }) => withConfig({ initialConfig, logger })({}) ; // 另外一模塊 const initialConfig = { host: 'localhost' }; const logger = console.log.bind(console); const config = createConfig({initialConfig, logger}); console.log(config.get('host')); // 'localhost' config.get('notThere'); // 'Missing config key: notThere'
選擇隱式仍是顯式取決於不少因素。Mixin 函數做用的數據類型必須是有效的,這就須要 API 文檔中的函數簽名很是清晰。
這就是隱式依賴版本中爲 o
添加默認值的緣由。因爲 JavaScript 缺乏類型註釋功能,但咱們能夠經過默認值來代替它。
const withConfig = config => (o = { log: (text = '') => console.log(text) }) => Object.assign({}, o, { // ...
若是你使用 TypeScript 或 Flow,最好爲你的對象參數定義一個明確的接口。
Mixin 函數並不像函數式編程那樣純。Mixin 函數一般是面向對象編程風格,具備反作用。許多 Mixin 函數會改變傳入的參數對象。注意!
出於一樣的緣由,一些開發者更喜歡函數式編程風格,不修改傳入的對象。在編寫 mixin 時,你應當適當地使用這兩種編碼風格。
這意味着,若是你要返回對象的實例,則始終返回 this
,而不是閉包中對象實例的引用。由於在函數式編程中,頗有可能這些引用指向的並非同一個對象。另外,老是使用 Object.assign()
或 {...object, ...spread}
語法進行復制。但須要注意的是,非枚舉的屬性將不會存在於最終的對象上。
const a = Object.defineProperty({}, 'a', { enumerable: false, value: 'a' }); const b = { b: 'b' }; console.log({...a, ...b}); // { b: 'b' }
出於一樣的緣由,若是你使用的 mixin 函數不是本身構建的,就不要認爲它就是純的。假設基礎對象會被改變,假設它可能會產生反作用,不保證參數不會改變,即由 mixin 函數組合而成的記錄工廠一般是不安全的。
Mixin 函數是可組合的工廠方法,它可以爲對象添加屬性和行爲,就如同裝配線上的站。它是將多個來源的功能(has-a, uses-a, can-do)組合成行爲的好方法,而不是從一個類上繼承全部功能(is-a)。
記住,「mixin 函數」 並不意味着「函數式編程」。Mixin 函數能夠用函數式編程風格編寫,避免反作用並不修改參數,但這並不保證。第三方 mixin 可能存在反作用和不肯定性。
不一樣於對象 mixin,mixin 函數支持正真的私有數據(封裝),包括繼承私有數據的能力。
不一樣於單繼承,mixin 函數還支持繼承多個祖先的能力,相似於類裝飾器或多繼承。
不一樣於 C++ 中的多繼承,JavaScript 中不多出現屬性衝突問題,當屬性衝突發生時,老是最後添加的 mixin 有效。
不一樣於類裝飾器或多繼承,不須要基類
老是從最簡單的實現方式開始,只根據須要使用更復雜的實現方式:
純函數 > 工廠方法 > mixin 函數 > 類繼承