[译]对象组合中的宝藏(软件编写)(第十三部分)

(译注:该图是用 PS 将烟雾处理成方块状后获得的效果,参见 flickr。)javascript

这是 “软件编写” 系列文章的第十三部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 < 上一篇 | << 返回第一篇前端

“经过对象的组合装配或者组合对象来得到更复杂的行为” ~ Gang of Four,《设计模式:可复用面向对象软件的基础》java

“优先考虑对象组合而不是类继承。” ~ Gang of Four,《设计模式:可复用面向对象软件的基础》android

软件开发中最多见的错误之一就是对于类继承的过分使用。类继承是一个代码复用机制,实例对象和基类构成了 **是一个(is-a)**关系。若是你想要使用 is-a 关系来构建应用程序,你将陷入麻烦,由于在面向对象设计中,类继承是最紧的耦合形式,这种耦合会引发下面这些常见问题:ios

  • 脆弱的基类问题
  • 猩猩/香蕉问题
  • 不得已的重复问题

类继承是经过从基类中抽象出一个可供子类继承或者重载的公共接口来实现复用的。抽象有两个重要的方面:git

  • 泛化(Generalization):该过程提取了服务于广泛用例的共享属性和行为。
  • 具化(Specialization):该过程提供了一个被特殊用例须要的实现细节。

目前,有许多方式去完成泛化和具化。注入简单函数、高阶函数、以及对象组合都能很好地代替类继承。github

不幸的是,对象组合很是容易被曲解,许多开发者都难于用对象组合的方式来思考问题。如今,是时候更深层次地探索这一主题了。编程

什么是对象组合?

“在计算机科学中,一个组合数据类型或是复合数据类型是任意的一个能够经过编程语言原始数据类型或者其余数据类型构造而成的数据类型。构成一个复合类型的操做又称为组合。” ~ Wikipedia后端

造成对象组合疑云的缘由之一是,任何将原始数据类型组装到一个复合对象的过程都是对象组合的一个形式,可是继承技术却常常与对象组合做对比,即使它们是全然不一样的两件事。这种二义性的产生是因为对象组合的语法(grammer)和语义(semantic)间存在着一个差异。设计模式

当咱们谈论到对象组合 vs 类继承时,咱们并不是在谈论一个具体的技术:咱们是在谈论组件对象(component objects)间的语义关联耦合程度。咱们谈论的是意义而非语法,人们一般一叶障目而不见泰山,没法区别两者,并陷入到语法细节中去。

GoF 建议道 “优先使用对象组合而不是类继承”,这启示了咱们将对象看做是更小,耦合更松的对象的组合,而不是大量从一个统一的基类继承而来。GoF 将紧耦合对象描述为 “它们造成了一个统一的系统,你没法在对其余类不知情或者不更改的状况下修改或者删除某个类。这让系统结构变得紧密,从而难于认知、修改及维护。”

三种不一样形式的对象组合

在《设计模式中》,GoF 声称:“你将一次又一次的在设计模式中看到对象组合”,而且描述了不一样类型的组合关系,包括有聚合(aggregation)和委托(delegation)。

《设计模式》的做者最初是使用 C++ 和 Smalltalk(Java 的前身)进行工做的。相较于 JavaScript,它们在运行时构建和改变对象关系要更加复杂,因此,GoF 在叙述对象组合时没用牵涉任何的实现细节也是能够理解的。然而,在 JavaScript 中,脱离动态对象扩展(也称为 链接(concatenation))去讨论对象组合是不可能的。

相较于《设计模式》中对象组合的定义,出于对 JavaScript 适用性以及构造一个更清晰的泛化的考虑,咱们会稍作发散。例如,咱们不会要求聚合须要隐式控制子类对象的生命期。对于动态对象扩展的语言来讲,这并不正确。

若是选择了一个错误的公理,会让咱们在得出有用泛化时受到没必要要的限制,强制咱们为具备相同大意的特殊用例起一个名字。软件开发者不喜欢重复作不须要的事儿。

  • 聚合(Aggregation):一个对象是由一个可枚举的子对象集合构成。换言之,一个对象能够包含其余对象。每一个子对象都保留了它本身的引用,所以它能够在信息不丢失的状况下直接从聚合对象中解构出来。
  • 链接(Concatenation):一个对象经过向现有对象增长属性而构成。属性能够一个个链接或者是从现有对象中拷贝。例如,jQuery 插件经过链接新的方法到 jQuery 委托原型 —— jQuery.fn 上而构建。
  • 委托(Delegation):一个对象直接指向或者委托到另外一个对象。例如,Ivan Sutherland 的画板 中的实例都含有 “master” 的引用,其被委托来共享属性。Photoshop 中的 “smart objects” 则做为了委托到外部资源的局部代理。JavaScript 的原型(prototype)也是代理:数组实例的方法指向了内置的数组原型 Array.prototype 上的方法,对象实例的方法则指向了 Object.prototype 上,等等。

须要注意的是这三种对象组合形式并非彼此互斥的。咱们可以使用聚合来实现委托,在 JavaScript 中,类继承也是经过委托实现的。许多软件系统用了不止一种组合,例如 jQuery 插件使用了链接来扩展 jQuery 委托原型 —— jQuery.fn。当客户端代码调用插件上的方法,请求将会被委托给链接到 jQuery.fn 上的方法。

后文的代码实例中的将会共享下面这段初始化代码:

const objs = [
  { a: 'a', b: 'ab' },
  { b: 'b' },
  { c: 'c', b: 'cb' }
];
复制代码

聚合

聚合表示一个对象是由一个可枚举的子对象集合构成。一个聚合对象就是包含了其余对象的对象。聚合中的每个子对象都保留了各自的引用,所以可以轻易地从聚合中解构出来。聚合对象能够表现为不一样类型的数据结构。

例子

  • 数组(Arrays)
  • 映射(Maps)
  • 集合(Sets)
  • 图(Graphs)
  • 树(Trees)
  • DOM 节点 (一个 DOM 节点能包含子节点)
  • UI 组件(一个组件能包含子组件)

什么时候使用

当集合中的成员须要共享相同的操做时(集合中的某个元素须要和其余元素共享一样的接口),能够考虑使用聚合,例如可迭代对象(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 插件经过链接被添加到 jQuery.fn
  • 状态 reducer(例如:Redux)
  • 函数式 mixin

什么时候使用

只要装配数据对象的过程是在运行时,就考虑使用链接,例如,合并 JSON 对象、从多个源中合并应用状态、以及不可变状态的更新(经过将新的数据混合到前一步状态)等等。

注意事项

  • 谨慎地改变现有对象。共享的可变状态是滋生 bug 的温床。
  • 可使用链接来模拟类继承和 is-a 关系。这也会面临和类继承同样的问题。多考虑组合小的、独立的对象,而不是从一个 “基础” 实例上继承属性,亦或使用差分继承(differential inheritance,译注:参看 MDN - Differential inheritance in JavaScript
  • 注意隐式的内在组件依赖。
  • 链接时的顺序可以解决属性名冲突:后进有效(last-in wins)。这一点对于默认值和重载行为颇有帮助,但若是顺序无关的话,也会形成问题。
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
复制代码

委托

委托表示一个对象直接指向或者委托到另外一个对象。

例子

  • JavaScript 内置类型使用了委托来让内置方法调用原型链上的方法。例如,数组实例的方法指向了内置的数组原型 Array.prototype 上的方法,对象实例则指向了 Object.prototype,等等。
  • jQuery 插件依赖了委托去让全部 jQuery 实例共享内置方法和插件方法。
  • Ivan Sutherland 画板的 “masters” 则是动态委托(委托在被建立后仍会被修改)。对于委托对象的修改将马上影响到全部对象实例。
  • Photoshop 使用了被叫作 “smart objects” 的委托来引用被定义在不一样文件的图像和资源。更改 smart objects 引用的对象(译注:例如修改被引用的图像)将影响全部 smart object 的实例。

什么时候使用

  • 节约内存:当存在许多对象实例时,委托对于在各个实例间共享相同属性或者方法将会颇有用,避免了更多的内存分配。
  • 动态更新大量实例:当对象的许多实例共享同一个状态时,这个状态须要动态更新,且该状态的更改能当即做用到每一个实例时,也须要委托。例如 Ivan Sutherland 画板的 “master” 和 Photoshop 的 “smart objects”。

注意事项

  • 委托一般用来模拟 JavaScript 中的类继承(固然,如今有了 extends 关键字),但这实际上不多须要。
  • 委托能够被用来精确模拟类继承的行为和限制。实际上,经过原型委托链,JavaScript 构建了基于静态委托模型的类继承,从而避免了 is-a 的思考方式。
  • 在使用诸如 Object.keys(instanceObj) 这样公共枚举机制时,委托属性是不可枚举的。
  • 委托是经过牺牲了属性检索性能来得到内存上的节约的,一些 JavaScript 引擎的优化会关闭动态委托(在建立后仍会改变的委托)。然而,即使在最慢的场景下,属性检索性能仍能有百万级的 ops —— 除非你正构建一个服务于对象操做或者图形程序的工具函数库,例如 RxJS 或是 three.js,不然对象属性检索都不会成为你的性能瓶颈。
  • 须要区分实例状态和委托状态。(译注:相似于区分实例对象的自由属性和原型链上的属性)
  • 在动态委托上共享状态不是实例安全的。对状态的改变将会做用到全部实例,这是滋生 bug 的温床。
  • ES6 的类并无建立动态委托。动态委托可能会在 Babel 编译后的代码中正常工做,但没法在真正的 ES6 环境下工做。

代码示例

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)来构建对象间松散、动态的关系,在这种关系下,对象被做为参数传递给了另外一个对象(依赖注入)等等。

全部的软件开发都是组合。可以经过轻松、灵活的方式来组合对象,也存在脆弱而不牢靠的方式来组合对象。一些对象组合的形式构成了对象间松耦合的关系,一些则构成了紧耦合。

竭力寻找一种变动小的程序需求时只须要变动小部分代码实现的组合方式。代码应当清楚且明练地描述你的意图,而且记住:在你须要类继承时,其实有更好的方式替代它。

须要 JavaScript 进阶训练吗?

DevAnyWhere 能帮助你最快进阶你的 JavaScript 能力,如组合式软件编写,函数式编程一节 React:

  • 直播课程
  • 灵活的课时
  • 一对一辅导
  • 构建真正的应用产品

https://devanywhere.io/

Eric Elliott“编写 JavaScript 应用” (O’Reilly) 以及 “跟着 Eric Elliott 学 Javascript” 两书的做者。他为许多公司和组织做过贡献,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC 等 , 也是不少机构的顶级艺术家,包括但不限于 UsherFrank Ocean 以及 Metallica

大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一块儿。_


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索