- 原文地址:The Hidden Treasures of Object Composition
- 原文做者:Eric Elliott
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:yoyoyohamapi
- 校对者:IridescentMia PCAaron
(译注:该图是用 PS 将烟雾处理成方块状后获得的效果,参见 flickr。)javascript
这是 “软件编写” 系列文章的第十三部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 < 上一篇 | << 返回第一篇前端
“经过对象的组合装配或者组合对象来得到更复杂的行为” ~ Gang of Four,《设计模式:可复用面向对象软件的基础》java
“优先考虑对象组合而不是类继承。” ~ Gang of Four,《设计模式:可复用面向对象软件的基础》android
软件开发中最多见的错误之一就是对于类继承的过分使用。类继承是一个代码复用机制,实例对象和基类构成了 **是一个(is-a)**关系。若是你想要使用 is-a 关系来构建应用程序,你将陷入麻烦,由于在面向对象设计中,类继承是最紧的耦合形式,这种耦合会引发下面这些常见问题:ios
类继承是经过从基类中抽象出一个可供子类继承或者重载的公共接口来实现复用的。抽象有两个重要的方面:git
目前,有许多方式去完成泛化和具化。注入简单函数、高阶函数、以及对象组合都能很好地代替类继承。github
不幸的是,对象组合很是容易被曲解,许多开发者都难于用对象组合的方式来思考问题。如今,是时候更深层次地探索这一主题了。编程
“在计算机科学中,一个组合数据类型或是复合数据类型是任意的一个能够经过编程语言原始数据类型或者其余数据类型构造而成的数据类型。构成一个复合类型的操做又称为组合。” ~ Wikipedia后端
造成对象组合疑云的缘由之一是,任何将原始数据类型组装到一个复合对象的过程都是对象组合的一个形式,可是继承技术却常常与对象组合做对比,即使它们是全然不一样的两件事。这种二义性的产生是因为对象组合的语法(grammer)和语义(semantic)间存在着一个差异。设计模式
当咱们谈论到对象组合 vs 类继承时,咱们并不是在谈论一个具体的技术:咱们是在谈论组件对象(component objects)间的语义关联和耦合程度。咱们谈论的是意义而非语法,人们一般一叶障目而不见泰山,没法区别两者,并陷入到语法细节中去。
GoF 建议道 “优先使用对象组合而不是类继承”,这启示了咱们将对象看做是更小,耦合更松的对象的组合,而不是大量从一个统一的基类继承而来。GoF 将紧耦合对象描述为 “它们造成了一个统一的系统,你没法在对其余类不知情或者不更改的状况下修改或者删除某个类。这让系统结构变得紧密,从而难于认知、修改及维护。”
在《设计模式中》,GoF 声称:“你将一次又一次的在设计模式中看到对象组合”,而且描述了不一样类型的组合关系,包括有聚合(aggregation)和委托(delegation)。
《设计模式》的做者最初是使用 C++ 和 Smalltalk(Java 的前身)进行工做的。相较于 JavaScript,它们在运行时构建和改变对象关系要更加复杂,因此,GoF 在叙述对象组合时没用牵涉任何的实现细节也是能够理解的。然而,在 JavaScript 中,脱离动态对象扩展(也称为 链接(concatenation))去讨论对象组合是不可能的。
相较于《设计模式》中对象组合的定义,出于对 JavaScript 适用性以及构造一个更清晰的泛化的考虑,咱们会稍作发散。例如,咱们不会要求聚合须要隐式控制子类对象的生命期。对于动态对象扩展的语言来讲,这并不正确。
若是选择了一个错误的公理,会让咱们在得出有用泛化时受到没必要要的限制,强制咱们为具备相同大意的特殊用例起一个名字。软件开发者不喜欢重复作不须要的事儿。
jQuery.fn
上而构建。Array.prototype
上的方法,对象实例的方法则指向了 Object.prototype
上,等等。须要注意的是这三种对象组合形式并非彼此互斥的。咱们可以使用聚合来实现委托,在 JavaScript 中,类继承也是经过委托实现的。许多软件系统用了不止一种组合,例如 jQuery 插件使用了链接来扩展 jQuery 委托原型 —— jQuery.fn
。当客户端代码调用插件上的方法,请求将会被委托给链接到 jQuery.fn
上的方法。
后文的代码实例中的将会共享下面这段初始化代码:
const objs = [
{ a: 'a', b: 'ab' },
{ b: 'b' },
{ c: 'c', b: 'cb' }
];
复制代码
聚合表示一个对象是由一个可枚举的子对象集合构成。一个聚合对象就是包含了其余对象的对象。聚合中的每个子对象都保留了各自的引用,所以可以轻易地从聚合中解构出来。聚合对象能够表现为不一样类型的数据结构。
当集合中的成员须要共享相同的操做时(集合中的某个元素须要和其余元素共享一样的接口),能够考虑使用聚合,例如可迭代对象(iterables)、栈、队列、树、图、状态机或者是它们的组合。
聚合适用于为集合元素应用一个统一抽象,例如为集合中的每一个成员应用一个将标量转换为向量的函数(如:array.map(fn)
)等等。可是,若是有成百上千或者成千上万甚至上百万个子对象,那么流式处理更加高效。
数组聚合:
const collection = (a, e) => a.concat([e]);
const a = objs.reduce(collection, []);
console.log(
'collection aggregation',
a,
a[1].b,
a[2].c,
`enumerable keys: ${ Object.keys(a) }`
);
复制代码
这将生成:
collection aggregation
[{"a":"a","b":"ab"},{"b":"b"},{"c":"c","b":"cb"}]
b
c
enumerable keys: 0,1,2
复制代码
使用 pairs 进行的链表聚合:
const pair = (a, b) => [b, a];
const l = objs.reduceRight(pair, []);
console.log(
'linked list aggregation',
l,
`enumerable keys: ${ Object.keys(l) }`
);
/* linked list aggregation [ {"a":"a","b":"ab"}, [ {"b":"b"}, [ {"c":"c","b":"cb"}, [] ] ] ] enumerable keys: 0,1 */
复制代码
链表构成了其余数据结构或者聚合的基础,例如数组、字符串以及各类形态的树。可能还有其余类型的聚合,但咱们在此不会对它们都进行深度探究。
链接表示一个对象经过向现有对象增长属性而构成。
jQuery.fn
只要装配数据对象的过程是在运行时,就考虑使用链接,例如,合并 JSON 对象、从多个源中合并应用状态、以及不可变状态的更新(经过将新的数据混合到前一步状态)等等。
const c = objs.reduce(concatenate, {});
const concatenate = (a, o) => ({...a, ...o});
console.log(
'concatenation',
c,
`enumerable keys: ${ Object.keys(c) }`
);
// concatenation { a: 'a', b: 'cb', c: 'c' } enumerable keys: a,b,c
复制代码
委托表示一个对象直接指向或者委托到另外一个对象。
Array.prototype
上的方法,对象实例则指向了 Object.prototype
,等等。Object.keys(instanceObj)
这样公共枚举机制时,委托属性是不可枚举的。const delegate = (a, b) => Object.assign(Object.create(a), b);
const d = objs.reduceRight(delegate, {});
console.log(
'delegation',
d,
`enumerable keys: ${ Object.keys(d) }`
);
// delegation { a: 'a', b: 'ab' } enumerable keys: a,b
console.log(d.b, d.c); // ab c
复制代码
咱们已经学到了:
全部由其余对象或者原始类型对象构成的对象都是复合对象。
建立复合对象的过程叫作组合。
存在不一样形式的组合。
当咱们组合对象时,对象间关系和依赖的不一样取决于对象是如何被组合的。
is-a 关系(由类继承所构成的关系)在面向对象设计中是最紧的耦合,实践中应当尽可能避免。
GoF 建议咱们经过组装若干小的特性以造成一个更大的总体来进行对象组合,而不是从一个单一的基类或者基础对象继承。“优先考虑对象组合而不是类继承”。
聚合将对象组合到一个可枚举的集合中,该集合的每一个成员都保留有各自的引用,例如数组、DOM 树等等。
委托经过将对象的委托链链接到一块儿来进行对象组合,委托链上的对象直接指向另外一个对象,或者将属性检索委托到了另外一个对象,例如 [].map
委托到了 Array.prototype.map()
链接经过用新的属性扩展示有对象来进行对象组合,例如 Object.assign(destination, a, b)
、{...a, ...b}
。
不一样类型的对象组合不是彼此互斥的。委托是聚合的一个子集,链接则可用来构造委托和聚合等等。
目前不仅存在三种类型的对象组合。也能够经过 相识(acquaintance)或联合(association)来构建对象间松散、动态的关系,在这种关系下,对象被做为参数传递给了另外一个对象(依赖注入)等等。
全部的软件开发都是组合。可以经过轻松、灵活的方式来组合对象,也存在脆弱而不牢靠的方式来组合对象。一些对象组合的形式构成了对象间松耦合的关系,一些则构成了紧耦合。
竭力寻找一种变动小的程序需求时只须要变动小部分代码实现的组合方式。代码应当清楚且明练地描述你的意图,而且记住:在你须要类继承时,其实有更好的方式替代它。
DevAnyWhere 能帮助你最快进阶你的 JavaScript 能力,如组合式软件编写,函数式编程一节 React:
Eric Elliott 是 “编写 JavaScript 应用” (O’Reilly) 以及 “跟着 Eric Elliott 学 Javascript” 两书的做者。他为许多公司和组织做过贡献,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等 , 也是不少机构的顶级艺术家,包括但不限于 Usher、Frank Ocean 以及 Metallica。
大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一块儿。_
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。