06月28, 2017

【译】组合软件:7. 函数式 Mixins

原文:http://www.zcfy.cc/article/3295

Mixins(混入)是对象组合的一种形式,这里组件的特性被混合进一个复合对象中,这样每个mixin的属性就变成了该复合对象的属性。

OOP中的“mixins”一词来自混合冰淇淋店。在这种店中,并非是把一大堆不同口味的冰淇淋放在不同的预混桶中,而是用原味冰淇淋以及一堆可以混在一起的不同配料为每个客户创建定制的口味。

对象 mixins 也差不多:你可以从一个空对象开始,混入一些特性来扩展它。因为JavaScript支持动态对象扩展和没有类的对象,所以在JavaScript中使用对象 mixins 是非常简单的 - 所以它也成了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 = {}; // 创建一个空对象
    that.name = spec.name; // 给它添加一个 "name" 属性
    return that; // 返回该对象
}

// 构造一个子对象,继承自 "base"
function child(spec) {
    // 通过 "base" 构造器创建对象
    var that = base(spec); 
    that.sayHello = function() { // 放大该对象
        return 'Hello, I\'m ' + that.name;
    };
    return that; // 返回它
}

// 用法
var result = child({ name: 'a functional object' });
console.log(result.sayHello()); // "Hello, I'm a functional object"

因为child()是与base()紧耦合的,所以当你添加grandchild()greatGrandchild()等等时,也就选择了类继承中大部分常见的问题。

什么是函数式Mixin?

函数式mixins是一些可以组合的函数,它们将新属性或行为与给定对象的属性混合起来。函数式mixins不依赖于或不需要基对象工厂或构造器:只需将任意对象传递到一个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()时,我们需要把要扩展的对象传递进来。函数式mixins是专为函数组合而设计的。下面我们来创建一些要组合的东西:

const quacking = quack => o => Object.assign({}, o, {
  quack: () => quack
});

const quacker = quacking('Quack!')({});
console.log( quacker.quack() ); // 'Quack!'

组合函数式 Mixins

函数式mixins可以用简单的函数组合来组合:

const createDuck = quack => quacking(quack)(flying({}));

const duck = createDuck('Quack!');

console.log(duck.fly().quack());

不过,这看起来有点难读。调试或调整组合的顺序也可能有点棘手。

当然,这是标准的函数组合,而从前几篇文章我们已经知道用compose()pipe()来组合会更优雅一些。如果我们用pipe()来反转函数组合的顺序,那么组合读起来就会跟Object.assign({},...){... object, ... spread}一样 - 从而保持了相同的位次顺序。在属性冲突的情况下,最后一个混入的对象胜出。

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());

何时用函数式 Mixins

应该始终使用最简单的抽象来解决正在处理的问题。首先从纯函数开始。如果需要一个具有持久状态的对象,就试试工厂函数。如果需要创建更复杂的对象,就试试函数式mixins。

以下是适用函数式mixins的一些不错的场景:

  • 应用程序状态管理,例如Redux store。

  • 某些横切关注点和服务,例如集中式的日志管理器。

  • 具有生命周期钩子的UI组件。

  • 可组合的函数式数据类型,例如JavaScript Array类型实现SemigroupFunctorFoldable

一些代数结构可以用其他代数结构派生出来,这意味着某些衍生物可以被组合成一个新的数据类型而不需要定制。

注意事项

大多数问题都可以用纯函数优雅地解决。不过函数式mixins并非如此。像类继承一样,函数式mixins也有它自己的问题。事实上,用函数式mixins完全有可能忠实复制类继承的所有特性和问题。

不过,可以用以下建议来规避:

  • 使用最简单的实用实现。从左边开始,只需要根据需要转到右边:纯函数 > 工厂 > 函数式mixins > 类

  • 避免在对象、mixins或数据类型之间创建 is-a关系

  • 避免mixins之间的隐式依赖关系 - 只要有可能,函数式mixins就应该是自包含的,并且不了解其他mixins

  • “函数式mixins”并不意味着“函数式编程”

类继承从来就不是JavaScript中最好的方法,但是这种选择有时是由你不能控制的库或框架造成的。在这种情况下,使用class有时是实用的,因为这些库或者框架提供了如下功能:

  1. 不需要你扩展你自己的类(即不要求你建立多层次的类层次结构)

  2. 不需要你直接使用new关键字 - 换句话说,框架会为你处理实例化

Angular 2+和React都满足了这些需求,所以只要你不扩展自己的类,就可以安全地使用它们提供的类。如果你愿意的话,React也允许避免使用类,但是你的组件可能无法利用React基类中内置的优化,并且你的组件会看起来不像文档示例中的组件。无论如何,只要有可能就应该总是用函数形式的React组件。

类的性能

在某些浏览器中,可能会提供仅对类的JavaScript引擎优化。不过,在大多数情况下,这些优化不会对应用程序的性能产生重大影响。事实上,可能很多年都不用担心class的性能差异。不管你如何创建对象,对象创建和属性访问总是非常快(数百万次操作/秒)。

也就是说,通用目的实用程序库(比如RxJS、Lodash等)的作者应该研究用class来创建对象实例的可能的性能优势。除非你已经测量出用class带来可证明以及显著的性能降低的重大瓶颈,否则你应为干净、灵活的代码而优化,而不用担心性能。

隐式依赖

你可能会试图创建旨在一起工作的函数式mixins。想象一下,你想为你的应用程序创建一个配置管理器,当你尝试访问不存在的配置属性时,该配置管理器会记录警告。

可以像这样创建它:

// 在它自己的模块中...
const withLogging = logger => o => Object.assign({}, o, {
  log (text) {
    logger(text)
  }
});

// in a different module with no explicit mention of
// withLogging -- we just assume it's there...
const withConfig = config => (o = {
  log: (text = '') => console.log(text)
}) => Object.assign({}, o, {
  get (key) {
    return config[key] == undefined ?

      // vvv implicit dependency here... oops! vvv
      this.log(`Missing config key: ${ key }`) :
      // ^^^ implicit dependency here... oops! ^^^

      config[key]
    ;
  }
});

// in yet another module that imports withLogging and
// withConfig...
const createConfig = ({ initialConfig, logger }) =>
  pipe(
    withLogging(logger),
    withConfig(initialConfig)
  )({})
;

// elsewhere...
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'

不过,也可以像这样创建它:

// import withLogging() explicitly in withConfig module
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 compose explicit dependency in here vvv
    withLogging(logger),
    // ^^^ compose explicit dependency in here ^^^

    addConfig(initialConfig)
  )(o)
;

// The factory only needs to know about withConfig now...
const createConfig = ({ initialConfig, logger }) =>
  withConfig({ initialConfig, logger })({})
;

// elsewhere, in a different module...
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文档中明确说明API约定。

这就是为什么隐式版本在签名中o有一个默认值的原因。由于JavaScript缺少类型注解功能,我们可以通过提供默认值来伪造该功能:

const withConfig = config => (o = {
  log: (text = '') => console.log(text)
}) => Object.assign({}, o, {
  // ...

如果你在用TypeScript或Flow,那么可能为对象需求声明一个显式接口会更好一些。

函数式Mixins和函数式编程

函数式mixins语境中的“函数式”并不总是具备“函数式编程”一样的纯度含义。函数式mixins通常会被以OOP风格使用,充斥着副作用。许多函数式mixins会改变传递给它们的对象参数。请务必注意。

同样的道理,有些开发人员虽说喜欢函数式编程风格,但是不会对传入的对象维护同一性引用。在编写函数式mixins时,应该假定使用这些mixins的代码会随机混合两种风格。

就是说,如果你需要返回对象实例,那就始终返回this值,而不是对闭包中的实例对象的引用 -- 在函数式代码中,二者引用的可能不是同一对象。此外,总是假定对象实例会通过用Object.assign(){... object,... spread}语法进行赋值而复制。也就是说,如果你设置有非可枚举属性,这些属性可能不会在最终对象上出现:

const a = Object.defineProperty({}, 'a', {
  enumerable: false,
  value: 'a'
});

const b = {
  b: 'b'
};

console.log({...a, ...b}); // { b: 'b' }

同样,如果你正在用不是在你的函数式代码中创建的函数式mixins,就不要假定代码是纯的。应该假定基础对象可能被改变,可能存在副作用,且不保证引用透明,也就是说要缓存(memoize)由函数式mixins组成的工厂通常是不安全的。

总结

函数式mixins是一些可组合的工厂函数,这些函数可以像在流水线上的站点一样,给对象添加属性和行为。它们是一种从多个来源的特性(has-a,uses-a,can-do)中组合行为,而不是继承给定类的所有功能(is-a)的好方法。

请注意,“函数式mixins”并不意味着“函数式编程” - 它仅仅是指“使用函数的mixins”。函数式mixinx可以用函数式编程风格来编写,以避免副作用,保持引用透明,但是这并非是有保证的。在第三方mixins中可能存在副作用和非确定性。

  • 与简单对象mixins不同,函数式mixins支持真正的数据私有(封装),包括继承私有数据的能力。

  • 与单祖先类继承不同,函数式mixins还支持从多祖先继承的能力,类似于类装饰器、traits或多重继承。

  • 与C ++中的多重继承不同,JavaScript中的钻石问题(即菱形继承)很少出现,因为在出​​现冲突时有一个简单的规则:最后添加的mixin获胜。

  • 不同于类装饰器、traits或多重继承,它不需要基类。

从最简单的实现开始,只按需转到更复杂的实现:

纯函数>工厂函数>函数式mixins>类

英文原文:https://medium.com/javascript-scene/functional-mixins-composing-software-ffb66d5e731c

本文链接:http://www.xiaojichao.com/post/functional-mixins-javascript-scene-medium.html

-- EOF --

Comments