Security News
pnpm 10.0.0 Blocks Lifecycle Scripts by Default
pnpm 10 blocks lifecycle scripts by default to improve security, addressing supply chain attack risks but sparking debate over compatibility and workflow changes.
dojo-compose
Advanced tools
A composition library, which works well in a TypeScript environment.
A composition library, which works well in a TypeScript environment.
WARNING This is beta software. While we do not anticipate significant changes to the API at this stage, we may feel the need to do so. This is not yet production ready, so you should use at your own risk.
This library embraces the concepts of "composition" versus classical Object Oriented inheritance. The classical model follows a pattern whereby you add functionality to an ancestor by extending it. Subsequently all other descendants from that class will also inherit that functionality.
In a composition model, the preferred pattern is to create logical feature classes which are then composited together to create a resulting class. It is believed that this pattern increases code reuse, focuses on the engineering of self contained "features" with minimal cross dependency.
The other pattern supported by compose is the factory pattern. When you create a new class with compose, it will return a factory function. To create a new instance of an object, you simply call the factory function. When using constructor functions, where the new
keyword is used, it limits the ability of construction to do certain things, like the ability for resource pooling.
Also, all the classes generated by the library are "immutable". Any extension of the class will result in a new class constructor and prototype. This is in order to minimize the amount of unanticipated consequences of extension for anyone who is referencing a previous class.
The library was specifically designed to work well in a environment where TypeScript is used, to try to take advantage of TypeScript's type inference, intersection types, and union types. This in ways constrained the design, but we feel that it has created an API that is very semantically functional.
The examples below are provided in TypeScript syntax. The package does work under JavaScript, but for clarity, the examples will only include one syntax. See below for how to utilize the package under JavaScript.
The library supports creating a "base" class from ES6 Classes, JavaScript constructor functions, or an object literal prototype. In addition an initialization function can be provided.
The compose
module's default export is a function which creates classes. This is also available as .create()
which is decorated onto the compose
function.
If you want to create a new class via a prototype and create an instance of it, you would want to do something like this:
import compose from 'dojo-compose/compose';
const fooFactory = compose({
foo: function () {
console.log('foo');
},
bar: 'bar',
qat: 1
});
const foo = fooFactory();
If you want to create a new class via an ES6/TypeScript class and create an instance of it, you would want to do something like this:
import compose from 'dojo-compose/compose';
class Foo {
foo() {
console.log('foo');
};
bar: string = 'bar';
qat: number = 1;
}
const fooFactory = compose(Foo);
const foo = fooFactory();
You can also subclass:
import compose from 'dojo-compose/compose';
const fooFactory = compose({
foo: function () {
console.log('foo');
},
bar: 'bar',
qat: 1
});
const myFooFactory = compose(fooFactory);
const foo = myFooFactory();
During creation, compose
takes a second optional argument, which is an initializer function. The constructor pattern for all compose
classes is to take an optional options
argument. Therefore the initialization function should take this optional argument:
import compose from 'dojo-compose/compose';
interface FooOptions {
foo?: Function,
bar?: string,
qat?: number
}
function fooInit(options?: FooOptions) {
if (options) {
for (let key in options) {
this[key] = options[key]
}
}
}
const fooFactory = compose({
foo: function () {
console.log('foo');
},
bar: 'bar',
qat: 1
}, fooInit);
const foo1 = fooFactory();
const foo2 = fooFactory({
bar: 'baz'
});
The compose
module's default export also has a property, extend
, which allows the enumerable, own properties of a literal object or the prototype of a class or ComposeFactory to be added to the prototype of a class. The type of the resulting class will be inferred and include all properties of the extending object. It can be used to extend an existing compose class like this:
import * as compose from 'dojo/compose';
let fooFactory = compose.create({
foo: 'bar'
});
fooFactory = compose.extend(fooFactory, {
bar: 1
});
let foo = fooFactory();
foo.foo = 'baz';
foo.bar = 2;
Or using chaining:
import * as compose from 'dojo/compose';
const fooFactory = compose.create({
foo: 'bar'
}).extend({
bar: 1
});
let foo = fooFactory();
foo.foo = 'baz';
foo.bar = 2;
extend
can also be used to implement an interface:
import * as compose from 'dojo/compose';
interface Bar {
bar?: number;
}
const fooFactory = compose.create({
foo: 'bar'
}).extend<Bar>({});
Or
const fooFactory = compose.create({
foo: 'bar'
}).extend(<Bar> {});
Oftentimes the need arises to take an existing class and add not just properties, but also behavior, or traits. The compose
module's default export has a mixin
property that provides this functionality. It can be used to mix in another compose class:
import * as compose from 'dojo/compose';
const fooFactory = compose.create({
foo: 'bar'
});
const barFactory = compose.create({
bar: function () {
console.log('bar');
}
});
const fooBarFactory = compose.mixin(fooFactory, barFactory);
const fooBar = fooBarFactory();
fooBar.bar(); // logs "bar"
NOTE: Using mixin on a ComposeFactory will result in the init function for the mixed in factory to be called first, and any init functions for the base will follow.
It can also be used to mix in an ES6 class. Note that when mixing in an ES6 class only methods will be mixed into the resulting class, not state.
import * as compose from 'dojo/compose';
const fooFactory = compose.create({
foo: 'bar'
});
class Bar {
bar() { console.log('bar'); }
}
const fooBarFactory = compose.mixin(fooFactory, { mixin: Bar });
const fooBar = fooBarFactory();
fooBar.bar(); // logs "bar"
It can also mixin in a plain object, but extend would be more appropriate in this case:
import * as compose from 'dojo/compose';
const fooFactory = compose.create({
foo: 'bar'
});
const bar = {
bar() { console.log('bar'); }
}
const fooBarFactory = compose.mixin(fooFactory, { mixin: bar });
const fooBar = fooBarFactory();
fooBar.bar(); // logs "bar"
The real benefit of using mixin
is in those cases where simply modifying the type is not enough, and there is additional behavior that needs to be included via an initialization function or aspects.
import * as compose from 'dojo/compose';
const fooFactory = compose.create({
foo: 'bar',
doSomething: function() {
console.log('something');
}
});
const bar = {
bar: 'uninitialized'
};
const initBar = function(instance: { bar: string }) {
instance.bar = 'initialized';
};
const bazFactory = compose.create({
baz: 'baz'
}, function(instance: { baz: string }) {
instance.baz = 'also initialized';
});
const bazAspect: AspectAdvice = {
after: {
doSomething: function() {
console.log('something else');
}
}
};
const fooBarBazFactory = fooFactory
.mixin({
mixin: bar,
initialize: initBar
})
.mixin({
mixin: bazFactory,
aspectAdvice: bazAspect
});
const fooBarBaz = fooBarBazFactory();
console.log(fooBarBaz.bar); // logs 'initialized'
console.log(fooBarBaz.baz); // logs 'also initialized'
fooBarBaz.doSomething(); // logs 'something' and then 'something else'
Additionally, anything with a factoryDescriptor
function that returns a ComposeMixinDescriptor
object can be passed directy to mixin.
const createFoo = compose({
foo: ''
})
const mixin = {
factoryDescriptor: function() {
return {
mixin: {
bar: 1
},
initialize: function(fooBar: { bar: number; foo: string; }) {
fooBar.bar = 3;
fooBar.foo = 'bar';
}
};
}
};
const createFooBar = createFoo.mixin(mixin);
const fooBar = createFooBar();
console.log(fooBar.foo) // logs 'foo'
console.log(fooBar.bar) // logs 3
The previous example, where a ComposeFactory
was passed directly to mixin
is possible because as a convenience all instances of ComposeFactory
are initialized with a version of the factoryDescriptor
function that simply returns the factory itself as the mixin
property. If a more complicated
factory descriptor is required, the factoryDescriptor
method can be overridden using the static
method, documented below.
When mixing in or extending classes which contain array literals as a value of a property, compose
will merge these values
instead of over writting, which it does with other value types.
For example, if I have an array of strings in my original class, and provide a mixin which shares the same property that is also an array, those will get merged:
const createFoo = compose({
foo: [ 'foo' ]
});
const createBarMixin = compose({
foo: [ 'bar' ]
});
const createFooBar = createFoo.mixin(createBarMixin);
const foo = createFooBar();
foo.foo; // [ 'foo', 'bar' ]
There are some things to note:
createFoo.prototype.foo !== foo.foo
.compose
utilizes TypeScript generics and type inference to type the resulting classes. Most of the time, this will work without any need to declare your types. There are situations though where you may want to be more explicit about your interfaces and compose
can accommodate that by passing in generics when using the API. Here is an example of creating a class that requires generics using compose
:
class Foo<T> {
foo: T;
}
class Bar<T> {
bar(opt: T): void {
console.log(opt);
}
}
interface FooBarClass {
<T, U>(): Foo<T>&Bar<U>;
}
let fooBarFactory: FooBarClass = compose(Foo).mixin({ mixin: <any> Bar });
let fooBar = fooBarFactory<number, any>();
If you want to make modifications to the prototype of a class that are difficult to perform with simple mixins or extensions, you can use the overlay
function provided on the default export of the compose
module. overlay
takes one argument, a function which will be passed a copy of the prototype of the existing class, and returns a new class whose type reflects the modifications made to the existing prototype:
import * as compose from 'dojo/compose';
const fooFactory = compose.create({
foo: 'bar'
});
const myFooFactory = fooFactory.overlay(function (proto) {
proto.foo = 'qat';
});
const myFoo = myFooFactory();
console.log(myFoo.foo); // logs "qat"
Note that as with all the functionality provided by compose
, the existing class is not modified.
If you want to add static methods or constants to a ComposeFactory
, the static
method allows you to do so. Any properties
set this way cannot be altered, as the returned factory is frozen. In order to modify or remove a static property
on a factory, a new factory would need to be created.
const createFoo = compose({
foo: 1
}).static({
doFoo(): string {
return 'foo';
}
});
console.log(createFoo.doFoo()); // logs 'foo'
// This will throw an error
// createFoo.doFoo = function() {
// return 'bar'
// }
const createNewFoo = createFoo.static({
doFoo(): string {
return 'bar';
}
});
console.log(createNewFoo.doFoo()); // logs 'bar'
If a factory already has static properties, calling its static method again will not maintain those properties on the returned factory. The original factory will still maintain its static properties.
const createFoo = compose({
foo: 1
}).static({
doFoo(): string {
return 'foo';
}
})
console.log(createFoo.doFoo()); //logs 'foo'
const createFooBar = createFoo.static({
doBar(): string {
return 'bar';
}
});
console.log(createFooBar.doBar()); //logs 'bar'
console.log(createFoo.doFoo()); //logs 'foo'
//console.log(createFooBar.doFoo()); Doesn't compile
//console.log(createFoo.doBar()); Doesn't compile
Static properties will also be lost when calling mixin or extend. Because of this, static properties should be applied to the 'final' factory in a chain.
The easiest way to use this package is to install it via npm
:
$ npm install dojo-compose
In addition, you can clone this repository and use the Grunt build scripts to manage the package.
Using under TypeScript or ES6 modules, you would generally want to just import
the dojo-compose/compose
module:
import compose from 'dojo-compose/compose';
const createFoo = compose({
foo: 'foo'
}, (instance, options) => {
/* do some initialization */
});
const foo = createFoo();
We appreciate your interest! Please see the Contributing Guidelines and Style Guide.
Test cases MUST be written using Intern using the Object test interface and Assert assertion interface.
90% branch coverage MUST be provided for all code submitted to this repository, as reported by Istanbul’s combined coverage results for all supported platforms.
A lot of thinking, talks, publications by Eric Elliott (@ericelliott) inspired @bryanforbes and @kitsonk to take a look at the composition and factory pattern.
@kriszyp helped bring AOP to Dojo 1 and we found a very good fit for those concepts in dojo/compose
.
dojo/_base/declare
was the starting point for bringing Classes and classical inheritance to Dojo 1 and without @uhop we wouldn't have had Dojo 1's class system.
@pottedmeat and @kitsonk iterated on the original API, trying to figure a way to get types to work well within TypeScript and @maier49 worked with the rest of the dgrid team to make the whole API more usable.
© 2015 Dojo Foundation & contributors. New BSD license.
FAQs
A composition library, which works well in a TypeScript environment.
We found that dojo-compose demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
pnpm 10 blocks lifecycle scripts by default to improve security, addressing supply chain attack risks but sparking debate over compatibility and workflow changes.
Product
Socket now supports uv.lock files to ensure consistent, secure dependency resolution for Python projects and enhance supply chain security.
Research
Security News
Socket researchers have discovered multiple malicious npm packages targeting Solana private keys, abusing Gmail to exfiltrate the data and drain Solana wallets.