[譯]Mixin 函數

軟件構建系列

原文連接:Functional Mixins
譯者注:在編程中,mixin 相似於一個固有名詞,能夠理解爲混合或混入,一般不進行直譯,本文也是一樣。javascript

這是「軟件構建」系列教程的一部分,該系列主要從 JavaScript ES6+ 中學習函數式編程,以及軟件構建技術。敬請關注。
上一篇 | 第一篇java

Mixin 函數 是指可以給對象添加屬性或行爲,並能夠經過管道鏈接在一塊兒的組合工廠函數,就如同流水線上的工人。Mixin 函數不依賴或要求一個基礎工廠或構造函數:簡單地將任意一個對象傳入一個 mixin,就會獲得一個加強以後的對象。編程

Mixin 函數的特色:設計模式

  • 數據封裝數組

  • 繼承私有狀態瀏覽器

  • 多繼承安全

  • 覆蓋重複屬性數據結構

  • 無需基礎類閉包

動機

現代軟件開發的核心就是組合:咱們將一個龐大複雜的問題,分解成更小,更簡單的問題,最終將這些問題的解決辦法組合起來就變成了一個應用程序。框架

組合的最小單位就是如下二者之一:

  • 函數

  • 數據結構

他們的組合就定義了應用的結構。

一般,組合對象由類繼承實現,其中子類從父類繼承其大部分功能,並擴展或覆蓋部分。這種方法致使了 is-a 問題,好比:管理員是一名員工,這引起了許多設計問題:

  • 高耦合:因爲子類的實現依賴於父類,因此類繼承是面向對象設計中最緊密的耦合。

  • 脆弱的子類:因爲高耦合,對父類的修改可能會破壞子類。軟件做者可能在不知情的狀況下破壞了第三方管理的代碼。

  • 層次不靈活:根據單一祖先分類,隨着長時間的演變,最終全部的類都將不適用於新用例。

  • 重複問題:因爲層次不靈活,新用例一般是經過重複而不是擴展來實現的,這致使不一樣的類有着類似的類結構。而一旦重複建立,在建立其子類時,該繼承自哪一個類以及爲何繼承於這個類就不清晰了。

  • 大猩猩和香蕉問題:「...面嚮對象語言的問題是他們會得到全部與之相關的隱含環境。好比你想要一個香蕉,但你獲得的會是一隻拿着香蕉的大猩猩,以及一整片叢林。」 - Joe Armstrong(Coders at Work)

假設管理員是一名員工,你如何處理聘請外部顧問暫時行使管理員職務的狀況?(譯者:木知啊~)若是你事先知道全部的需求,類繼承可能有效,但我從沒有看到過這種狀況。隨着不斷地使用,新問題和更有效的流程將會被發現,應用程序和需求不可避免地隨着時間的推移而發展和演變。

Mixin 提供了更靈活的方法。

什麼是 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 函數是一系列將新的屬性或行爲混入特定對象的組合函數。它不依賴或須要一個基礎工廠方法或構造器,只需將任意對象傳入一個 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 函數

經過簡單的函數組合就能夠將 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 函數。

如下是一些使用 Mixin 函數很棒的例子:

  • 應用狀態管理,好比,Redux

  • 某些橫向服務,好比,集中日誌處理

  • 組件生命週期函數

  • 功能可組合的數據類型,好比,JavaScript Array 類實現了 Semigroup, Functor, Foldable

一些代數結構能夠根據其餘代數結構得出,這意味着新的數據類型能夠經過某些推導組合而成,而不須要定製。

注意事項

大部分問題均可以使用純函數優雅地解決。然而,mixin 函數同類繼承同樣,會形成一些問題。事實上,使用 mixin 函數可以徹底複製類繼承的優缺點。

你應當遵循如下的建議來避免這些問題。

  • 使用最簡單的實現。從左邊開始,根據須要移到右邊。純函數 > 工廠方法 > mixin 函數 > 類繼承

  • 避免建立對象,mixin,或數據類型之間的 is-a 關係

  • 避免 mixins 之間的隱含依賴關係,mixin 函數應當是獨立的

  • mixin 函數並不意味着函數式編程

類繼承

在 JavaScript 中,類繼承在極少狀況下(也許永遠不)會是最佳方案,但這一般是一些不禁你控制的庫或框架。在這種場景下,類有時是實用的。

  1. 無需擴展你本身的類(不須要你創建多層次的類結構)

  2. 無需使用 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 函數會改變傳入的參數對象。注意!

出於一樣的緣由,一些開發者更喜歡函數式編程風格,不修改傳入的對象。在編寫 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 函數 > 類繼承

相關文章
相關標籤/搜索