摘要:函數(shù)通常是面向對象編程風格,具有副作用。因為在函數(shù)式編程中,很有可能這些引用指向的并不是同一個對象。記住,函數(shù)并不意味著函數(shù)式編程。函數(shù)可以用函數(shù)式編程風格編寫,避免副作用并不修改參數(shù),但這并不保證。
軟件構建系列
原文鏈接:Functional Mixins
譯者注:在編程中,mixin 類似于一個固有名詞,可以理解為混合或混入,通常不進行直譯,本文也是同樣。這是“軟件構建”系列教程的一部分,該系列主要從 JavaScript ES6+ 中學習函數(shù)式編程,以及軟件構建技術。敬請關注。
上一篇 | 第一篇
Mixin 函數(shù) 是指能夠給對象添加屬性或行為,并可以通過管道連接在一起的組合工廠函數(shù),就如同流水線上的工人。Mixin 函數(shù)不依賴或要求一個基礎工廠或構造函數(shù):簡單地將任意一個對象傳入一個 mixin,就會得到一個增強之后的對象。
Mixin 函數(shù)的特點:
數(shù)據(jù)封裝
繼承私有狀態(tài)
多繼承
覆蓋重復屬性
無需基礎類
動機現(xiàn)代軟件開發(fā)的核心就是組合:我們將一個龐大復雜的問題,分解成更小,更簡單的問題,最終將這些問題的解決辦法組合起來就變成了一個應用程序。
組合的最小單位就是以下兩者之一:
函數(shù)
數(shù)據(jù)結構
他們的組合就定義了應用的結構。
通常,組合對象由類繼承實現(xiàn),其中子類從父類繼承其大部分功能,并擴展或覆蓋部分。這種方法導致了 is-a 問題,比如:管理員是一名員工,這引發(fā)了許多設計問題:
高耦合:由于子類的實現(xiàn)依賴于父類,所以類繼承是面向對象設計中最緊密的耦合。
脆弱的子類:由于高耦合,對父類的修改可能會破壞子類。軟件作者可能在不知情的情況下破壞了第三方管理的代碼。
層次不靈活:根據(jù)單一祖先分類,隨著長時間的演變,最終所有的類都將不適用于新用例。
重復問題:由于層次不靈活,新用例通常是通過重復而不是擴展來實現(xiàn)的,這導致不同的類有著相似的類結構。而一旦重復創(chuàng)建,在創(chuàng)建其子類時,該繼承自哪個類以及為什么繼承于這個類就不清晰了。
大猩猩和香蕉問題:“...面向對象語言的問題是他們會獲得所有與之相關的隱含環(huán)境。比如你想要一個香蕉,但你得到的會是一只拿著香蕉的大猩猩,以及一整片叢林。” - Joe Armstrong(Coders at Work)
假設管理員是一名員工,你如何處理聘請外部顧問暫時行使管理員職務的情況?(譯者:木知啊~)如果你事先知道所有的需求,類繼承可能有效,但我從沒有看到過這種情況。隨著不斷地使用,新問題和更有效的流程將會被發(fā)現(xiàn),應用程序和需求不可避免地隨著時間的推移而發(fā)展和演變。
Mixin 提供了更靈活的方法。
什么是 Mixin?“組合優(yōu)于繼承。” - 設計模式:可重用面向對象軟件的元素
Mixin 是對象組合的一種,它將部分特性混入復合對象中,使得這些屬性成為復合對象的屬性。
面向對象編程中的 "mixin" 一詞來源于冰激凌店。不同于將不同口味的冰激凌預先混合,每個顧客可以自由混合各種口味的冰激凌,從而創(chuàng)造出屬于自己的冰激凌口味。
對象 mixin 與之類似:從一個空對象開始,然后一步步擴展它。由于 JavaScript 支持動態(tài)對象擴展,所以在 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 */什么是函數(shù)繼承?
函數(shù)繼承是指通過函數(shù)來增強對象實例實現(xiàn)特性繼承的過程。該函數(shù)建立一個閉包使得部分數(shù)據(jù)是私有的,并通過動態(tài)對象擴展使得對象實例擁有新的屬性和方法。
來看一下這個詞的創(chuàng)造者 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) { // 調用父類構造函數(shù) 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 函數(shù)?Mixin 函數(shù)是一系列將新的屬性或行為混入特定對象的組合函數(shù)。它不依賴或需要一個基礎工廠方法或構造器,只需將任意對象傳入一個 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 函數(shù)被設計用來實現(xiàn)函數(shù)組合,繼續(xù)看下去。
const quacking = quack => o => Object.assign({}, o, { quack: () => quack }); const quacker = quacking("Quack!")({}); console.log( quacker.quack() ); // "Quack!"組合 Mixin 函數(shù)
通過簡單的函數(shù)組合就可以將 mixin 函數(shù)組合起來。
const createDuck = quack => quacking(quack)(flying({})); const duck = createDuck("Quack!"); console.log(duck.fly().quack());
但是,這看上去有點丑陋,調試或重新排列組合順序也有點困難。
當然,這只是標準的函數(shù)組合,而我們可以通過一些好的辦法來將它們組合起來,比如 compose() 或 pipe()。如果,使用 pipe() 就需反轉函數(shù)的調用順序,才能保持相同的執(zhí)行順序。當屬性沖突時,最后的屬性生效。
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 函數(shù)的使用場景
你應當總是使用最簡單的抽象來解決問題。從純函數(shù)開始。如果需要一個持久化狀態(tài)的對象,就試試工廠方法。如果你需要構建更復雜的對象,那就試試 Mixin 函數(shù)。
以下是一些使用 Mixin 函數(shù)很棒的例子:
應用狀態(tài)管理,比如,Redux
某些橫向服務,比如,集中日志處理
組件生命周期函數(shù)
功能可組合的數(shù)據(jù)類型,比如,JavaScript Array 類實現(xiàn)了 Semigroup, Functor, Foldable
一些代數(shù)結構可以根據(jù)其他代數(shù)結構得出,這意味著新的數(shù)據(jù)類型可以通過某些推導組合而成,而不需要定制。
注意事項大部分問題都可以使用純函數(shù)優(yōu)雅地解決。然而,mixin 函數(shù)同類繼承一樣,會造成一些問題。事實上,使用 mixin 函數(shù)能夠完全復制類繼承的優(yōu)缺點。
你應當遵循以下的建議來避免這些問題。
使用最簡單的實現(xiàn)。從左邊開始,根據(jù)需要移到右邊。純函數(shù) > 工廠方法 > mixin 函數(shù) > 類繼承
避免創(chuàng)建對象,mixin,或數(shù)據(jù)類型之間的 is-a 關系
避免 mixins 之間的隱含依賴關系,mixin 函數(shù)應當是獨立的
mixin 函數(shù)并不意味著函數(shù)式編程
類繼承在 JavaScript 中,類繼承在極少情況下(也許永遠不)會是最佳方案,但這通常是一些不由你控制的庫或框架。在這種場景下,類有時是實用的。
無需擴展你自己的類(不需要你建立多層次的類結構)
無需使用 new 關鍵字,也就是說,框架會替你實例化
Angular 2+ 和 React 滿足這些需求,所以你無需擴展你自己的類,而是放心地使用它們的類。在 React 中,你可以不使用類,不過這樣你的組件將不會獲得 React 的優(yōu)化,并且你的組件也會同文檔中的例子不同。但無論如何,使用函數(shù)構建 React 組件總是你的首選。
性能在一些瀏覽器中,類會獲得 JavaScript 引擎的優(yōu)化,其他的則無法直接使用。在幾乎所有情況下,這些優(yōu)化都不會對程序產生決定性的影響。事實上,在接下去的幾年中,你都無需關心類在性能上的不同。無論你如何構建對象,對象創(chuàng)建和屬性訪問總是非常快的(每秒百萬次)。
也就是說,類似 RxJS,Lodash 等公共庫的作者應該研究使用 class 創(chuàng)建對象實例可能的性能優(yōu)勢。除非你能夠證明通過類能夠解決性能瓶頸,否則,你就應當使你的代碼保持干凈、靈活,而不必擔心性能。
隱式依賴你可能打算創(chuàng)建一些計劃用于一同工作的 mixin 函數(shù)。試想一下,你想要為你的應用添加一個配置管理器,當你訪問不存在的配置屬性時,它會提示警告,像這樣:
// 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 函數(shù)作用的數(shù)據(jù)類型必須是有效的,這就需要 API 文檔中的函數(shù)簽名非常清晰。
這就是隱式依賴版本中為 o 添加默認值的原因。由于 JavaScript 缺少類型注釋功能,但我們可以通過默認值來代替它。
const withConfig = config => (o = { log: (text = "") => console.log(text) }) => Object.assign({}, o, { // ...
如果你使用 TypeScript 或 Flow,最好為你的對象參數(shù)定義一個明確的接口。
Mixin 函數(shù)與函數(shù)式編程Mixin 函數(shù)并不像函數(shù)式編程那樣純。Mixin 函數(shù)通常是面向對象編程風格,具有副作用。許多 Mixin 函數(shù)會改變傳入的參數(shù)對象。注意!
出于同樣的原因,一些開發(fā)者更喜歡函數(shù)式編程風格,不修改傳入的對象。在編寫 mixin 時,你應當適當?shù)厥褂眠@兩種編碼風格。
這意味著,如果你要返回對象的實例,則始終返回 this,而不是閉包中對象實例的引用。因為在函數(shù)式編程中,很有可能這些引用指向的并不是同一個對象。另外,總是使用 Object.assign() 或 {...object, ...spread} 語法進行復制。但需要注意的是,非枚舉的屬性將不會存在于最終的對象上。
const a = Object.defineProperty({}, "a", { enumerable: false, value: "a" }); const b = { b: "b" }; console.log({...a, ...b}); // { b: "b" }
出于同樣的原因,如果你使用的 mixin 函數(shù)不是自己構建的,就不要認為它就是純的。假設基礎對象會被改變,假設它可能會產生副作用,不保證參數(shù)不會改變,即由 mixin 函數(shù)組合而成的記錄工廠通常是不安全的。
結論Mixin 函數(shù)是可組合的工廠方法,它能夠為對象添加屬性和行為,就如同裝配線上的站。它是將多個來源的功能(has-a, uses-a, can-do)組合成行為的好方法,而不是從一個類上繼承所有功能(is-a)。
記住,“mixin 函數(shù)” 并不意味著“函數(shù)式編程”。Mixin 函數(shù)可以用函數(shù)式編程風格編寫,避免副作用并不修改參數(shù),但這并不保證。第三方 mixin 可能存在副作用和不確定性。
不同于對象 mixin,mixin 函數(shù)支持正真的私有數(shù)據(jù)(封裝),包括繼承私有數(shù)據(jù)的能力。
不同于單繼承,mixin 函數(shù)還支持繼承多個祖先的能力,類似于類裝飾器或多繼承。
不同于 C++ 中的多繼承,JavaScript 中很少出現(xiàn)屬性沖突問題,當屬性沖突發(fā)生時,總是最后添加的 mixin 有效。
不同于類裝飾器或多繼承,不需要基類
總是從最簡單的實現(xiàn)方式開始,只根據(jù)需要使用更復雜的實現(xiàn)方式:
純函數(shù) > 工廠方法 > mixin 函數(shù) > 類繼承
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/83670.html
摘要:可以說,相比繼承而已,更喜歡這種組合的方式。需要指出的是,是可以包含在其他的中的程序會在控制臺打印出。包含多個我們的要包裹在數(shù)組當中,提醒了我們可以在組件中包含多個注意事項這里有幾件事需要引起我們的注意,當我們使用的時候。 update: Mixin 只適用于 ES5。如果你的項目里面用的是 ES6 ,可以采用高階組件來實現(xiàn) Mixin 的功能。 我使用 React.js 構建大型項目...
摘要:使用構造函數(shù)的原型繼承相比使用原型的原型繼承更加復雜,我們先看看使用原型的原型繼承上面的代碼很容易理解。相反的,使用構造函數(shù)的原型繼承像下面這樣當然,構造函數(shù)的方式更簡單。 五天之前我寫了一個關于ES6標準中Class的文章。在里面我介紹了如何用現(xiàn)有的Javascript來模擬類并且介紹了ES6中類的用法,其實它只是一個語法糖。感謝Om Shakar以及Javascript Room中...
摘要:譯立即執(zhí)行函數(shù)表達式處理支持瀏覽器環(huán)境微信小程序。學習整體架構,利于打造屬于自己的函數(shù)式編程類庫。下一篇文章可能是學習的源碼整體架構。也可以加微信,注明來源,拉您進前端視野交流群。 前言 上一篇文章寫了jQuery整體架構,學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫 雖然看過挺多underscore.js分析類的文章,但總感覺少點什么。這也許就是紙上得來終覺淺,絕知此...
摘要:在吸取了的一些特性基礎上,有了大幅改進,也就是現(xiàn)在的。嵌套極大程度上降低了選擇器名稱和屬性的重復書寫。選擇器嵌套選擇器嵌套是指從一個選擇器中嵌套子選擇器,來實現(xiàn)選擇器的繼承關系。也已經成為的一個標配組件。 SASS是Syntactically Awesome Stylesheete Sass的縮寫,它是css的一個開發(fā)工具,提供了很多便利和簡單的語法,讓css看起來更像是一門...
摘要:基于的語言比特幣開發(fā)教程用機器人接受和發(fā)送比特幣在上一篇教程中我們創(chuàng)建了自動回復消息的機器人當用戶發(fā)送消息時,機器人會自動回復同一條消息按本篇教程后學習后完成后,你的機器人將會接受用戶發(fā)送過來的加密貨幣,然后立即轉回用戶。 基于Mixin Network的Go語言比特幣開發(fā)教程 : 用 Mixin Messenger 機器人接受和發(fā)送比特幣 showImg(https://segmen...
閱讀 2914·2021-10-19 10:09
閱讀 3134·2021-10-09 09:41
閱讀 3380·2021-09-26 09:47
閱讀 2696·2019-08-30 15:56
閱讀 599·2019-08-29 17:04
閱讀 986·2019-08-26 11:58
閱讀 2510·2019-08-26 11:51
閱讀 3361·2019-08-26 11:29