Selecting the wrong axioms can unnecessarily restrict a useful generalization, and force us to come up with another name for a special case of the same general idea. Software developers don’t like to repeat ourselves when we don’t need to.
Aggregation When an object is formed from an enumerable collection of subobjects. In other words, an object which contains other objects. Each subobject retains its own reference identity, such that it could be destructured from the aggregation without information loss.
Concatenation When an object is formed by adding new properties to an existing object. Properties can be concatenated one at a time or copied from existing objects, e.g., jQuery plugins are created by concatenating new methods to the jQuery delegate prototype, jQuery.fn
.
Delegation When an object forwards or delegates to another object. e.g., Ivan Sutherland’s Sketchpad (1962) included instances with references to “masters” which were delegated to for shared properties. Photoshop includes “smart objects” that serve as local proxies which delegate to an external resource. JavaScript’s prototypes are also delegates: Array instances forward built-in array method calls to Array.prototype
, objects to Object.prototype
, etc…
It’s important to note that these different forms of composition are not mutually exclusive. It’s possible to implement delegation using aggregation, and class inheritance is implemented using delegation in JavaScript. Many software systems use more than one type of composition, e.g., jQuery’s plugins use concatenation to extend the jQuery delegate prototype, jQuery.fn
. When client code calls a plugin method, the request is delegated to the method that was concatenated to the delegate prototype.
The code examples below will share the following setup code:
const objs = [
{ a: 'a', b: 'ab' },
{ b: 'b' },
{ c: 'c', b: 'cb' }
];
Aggregation
Aggregation is when an object is formed from an enumerable collection of subobjects. An aggregate is an object which contains other objects. Each subobject in an aggregation retains its own reference identity, and could be losslessly destructured from the aggregate. Aggregates can be represented in a wide variety of structures.
Examples
- Arrays
- Maps
- Sets
- Graphs
- Trees
- DOM nodes
- UI components
When to use
Whenever there are collections of objects which need to share common operations, such as iterables, stacks, queues, trees, graphs, state machines, or the composite pattern (when you want a single item to share the same interface as many items).
Considerations: Aggregations are great for applying universal abstractions, such as applying a function to each member of an aggregate (e.g., array.map(fn)
), transforming vectors as if they’re single values, and so on. If there are potentially hundreds of thousands or millions of subobjects, however, stream processing may be more efficient.
Code examples
Array aggregation:
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) }`
);
This will produce:
collection aggregation
[{"a":"a","b":"ab"},{"b":"b"},{"c":"c","b":"cb"}]
b c
enumerable keys: 0,1,2
Linked list aggregation using 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
*/
Linked lists form the basis of lots of other data structures and aggregations, such as arrays, strings, and various kinds of trees. There are many other possible kinds of aggregation. We won’t cover them all in-depth here.
Concatenation
Concatenation is when an object is formed by adding new properties to an existing object.
Examples
- Plugins are added to
jQuery.fn
via concatenation - State reducers (e.g., Redux)
- Functional mixins
When to use: Any time it would be useful to progressively assemble data structures at runtime, e.g., merging JSON objects, hydrating application state from multiple sources, creating updates to immutable state (by merging previous state with new data), etc…
Considerations
- Be careful mutating existing objects. Shared mutable state is a recipe for many bugs.
- It’s possible to mimic class hierarchies and is-a relations with concatenation. The same problems apply. Think in terms of composing small, independent objects rather than inheriting props from a “base” instance and applying differential inheritance.
- Beware of implicit inter-component dependencies.
- Property name collisions are resolved by concatenation order: last-in wins. This is useful for defaults/overrides behavior, but can be problematic if the order shouldn’t matter.
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
Delegation
Delegation is when an object forwards or delegates to another object.
Examples
- JavaScript’s built-in types use delegation to forward built-in method calls up the prototype chain. e.g.,
[].map()
delegates toArray.prototype.map()
,obj.hasOwnProperty()
delegates toObject.prototype.hasOwnProperty()
and so on. - jQuery plugins rely on delegation to share built-in and plugin methods among all jQuery object instances.
- Sketchpad’s “masters” were dynamic delegates. Modifications to the delegate would be reflected instantly in all of the object instances.
- Photoshop uses delegates called “smart objects” to refer to images and resources defined in separate files. Changes to the object that smart objects refer to are reflected in all instances of the smart object.
When to use
- Conserve memory: Any time there may be potentially many instances of an object and it would be useful to share identical properties or methods among each instance which would otherwise require allocating more memory.
- Dynamically update many instances: Any time many instances of an object need to share identical state which may need to be updated dynamically and changes instantaneously reflected in every instance, e.g., Sketchpad’s “masters” or Photoshop’s “smart objects”.
Considerations
- Delegation is commonly used to imitate class inheritance in JavaScript (wired up by the
extends
keyword), but is very rarely actually needed. - Delegation can be used to exactly mimic the behavior and limitations of class inheritance. In fact, class inheritance in JavaScript is built on top of static delegates via the prototype delegation chain. Avoid is-a thinking.
- Delegate props are non-enumerable using common mechanisms such as
Object.keys(instanceObj)
. - Delegation saves memory at the cost of property lookup performance, and some JS engine optimizations get turned off for dynamic delegates (delegates that change after they’ve been created). However, even in the slowest case, property lookup performance is measured in millions of ops per second — chances are good that this is not your bottleneck unless you’re building a utility library for object operations or graphics programming, e.g., RxJS or three.js.
- Need to differentiate between instance state, and delegate state.
- Shared state on dynamic delegates is not instance safe. Changes are shared between all instances. Shared state on dynamic delegates is commonly (but not always) a bug.
- ES6 classes don’t create dynamic delegates in ES6. They may seem to work in Babel, but will fail hard in real ES6 environments.
Code example
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
0 Comments