原文连接:Functional Mixins
译者注:在编程中,mixin 相似于一个固有名词,能够理解为混合或混入,一般不进行直译,本文也是一样。javascript这是“软件构建”系列教程的一部分,该系列主要从 JavaScript ES6+ 中学习函数式编程,以及软件构建技术。敬请关注。
上一篇 | 第一篇java
Mixin 函数 是指可以给对象添加属性或行为,并能够经过管道链接在一块儿的组合工厂函数,就如同流水线上的工人。Mixin 函数不依赖或要求一个基础工厂或构造函数:简单地将任意一个对象传入一个 mixin,就会获得一个加强以后的对象。编程
Mixin 函数的特色:设计模式
数据封装数组
继承私有状态浏览器
多继承安全
覆盖重复属性数据结构
无需基础类闭包
现代软件开发的核心就是组合:咱们将一个庞大复杂的问题,分解成更小,更简单的问题,最终将这些问题的解决办法组合起来就变成了一个应用程序。框架
组合的最小单位就是如下二者之一:
函数
数据结构
他们的组合就定义了应用的结构。
一般,组合对象由类继承实现,其中子类从父类继承其大部分功能,并扩展或覆盖部分。这种方法致使了 is-a 问题,好比:管理员是一名员工,这引起了许多设计问题:
高耦合:因为子类的实现依赖于父类,因此类继承是面向对象设计中最紧密的耦合。
脆弱的子类:因为高耦合,对父类的修改可能会破坏子类。软件做者可能在不知情的状况下破坏了第三方管理的代码。
层次不灵活:根据单一祖先分类,随着长时间的演变,最终全部的类都将不适用于新用例。
重复问题:因为层次不灵活,新用例一般是经过重复而不是扩展来实现的,这致使不一样的类有着类似的类结构。而一旦重复建立,在建立其子类时,该继承自哪一个类以及为何继承于这个类就不清晰了。
大猩猩和香蕉问题:“...面向对象语言的问题是他们会得到全部与之相关的隐含环境。好比你想要一个香蕉,但你获得的会是一只拿着香蕉的大猩猩,以及一整片丛林。” - Joe Armstrong(Coders at Work)
假设管理员是一名员工,你如何处理聘请外部顾问暂时行使管理员职务的状况?(译者:木知啊~)若是你事先知道全部的需求,类继承可能有效,但我从没有看到过这种状况。随着不断地使用,新问题和更有效的流程将会被发现,应用程序和需求不可避免地随着时间的推移而发展和演变。
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 方法,它就会被扩展。
来看下面的例子。
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 函数组合起来。
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 函数很棒的例子:
应用状态管理,好比,Redux
某些横向服务,好比,集中日志处理
组件生命周期函数
功能可组合的数据类型,好比,JavaScript Array
类实现了 Semigroup
, Functor
, Foldable
一些代数结构能够根据其余代数结构得出,这意味着新的数据类型能够经过某些推导组合而成,而不须要定制。
大部分问题均可以使用纯函数优雅地解决。然而,mixin 函数同类继承同样,会形成一些问题。事实上,使用 mixin 函数可以彻底复制类继承的优缺点。
你应当遵循如下的建议来避免这些问题。
使用最简单的实现。从左边开始,根据须要移到右边。纯函数 > 工厂方法 > mixin 函数 > 类继承
避免建立对象,mixin,或数据类型之间的 is-a 关系
避免 mixins 之间的隐含依赖关系,mixin 函数应当是独立的
mixin 函数并不意味着函数式编程
在 JavaScript 中,类继承在极少状况下(也许永远不)会是最佳方案,但这一般是一些不禁你控制的库或框架。在这种场景下,类有时是实用的。
无需扩展你本身的类(不须要你创建多层次的类结构)
无需使用 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 时,你应当适当地使用这两种编码风格。
这意味着,若是你要返回对象的实例,则始终返回 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 函数 > 类继承