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

相关文章
相关标签/搜索