[译]Mixin 函数

软件构建系列

原文链接:Functional Mixins
译者注:在编程中,mixin 相似于一个固有名词,可以理解为夹杂或混入,一般不举行直译,本文也是一样。

这是“软件构建”系列教程的一部份,该系列主要从 JavaScript ES6+ 中进修函数式编程,以及软件构建手艺。敬请关注。
上一篇 | 第一篇

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 函数 > 类继承

    原文作者:DiscipleD
    原文地址: https://segmentfault.com/a/1190000009924961
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞