Richard Lindsey @Velveeta
Decoration is a software pattern that uses a higher order function. The original function is passed to this higher order function, which "decorates" it in a layer that adds some additional functionality, and then invokes the original target function with the passed parameters.
const sourceFunction = (a, b) => {
return someComputedResult(a, b);
}
const decoratorFunction = fn => (...args) => {
someAdditionalTask();
return fn(...args);
}
const decoratedSourceFunction = decoratorFunction(sourceFunction);
sourceFunction(param1, param2); // sourceFunction executes in normal fashion
decoratedSourceFunction(param1, param2); // sourceFunction executes with someAdditionalTaskimport debounce from 'lodash.debounce';
import React from 'react';
const MySearchQueryComponent = class extends React.Component {
constructor(...args) {
super(...args);
this._onKeyPress = debounce(this._onKeyPress, 300);
}
_onKeyPress = (e) => {
this.props.onSearch(e.target.value);
};
render() {
return <input type="text" onKeyPress={this._onKeyPress} />;
}
}import { @autobind, @debounce } from 'shared/decorators';
import React from 'react';
const MySearchQueryComponent = class extends React.Component {
@debounce(300)
@autobind
_onKeyPress(e) {
this.props.onSearch(e.target.value);
};
render() {
return <input type="text" onKeyPress={this._onKeyPress} />;
}
}export decorator @logged {
@wrap(f => {
// Pull the function name property
const name = f.name;
// Create a new version of our function, wrapped with additional functionality
function wrapped(...args) {
console.log(`starting ${name} with arguments ${args.join(", ")}`);
f.call(this, ...args);
console.log(`ending ${name}`);
}
// Define a 'name' property on our new wrapped function, in case another @wrap is applied
Object.defineProperty(wrapped, "name", {
value: name,
configurable: true
});
// Return our wrapped function as the function definition for this decorated item
return wrapped;
})
}import { @logged } from './logged';
class C {
@logged
setPrivateMember(arg) {
this.#privateMember = arg;
}
@logged
set #privateMember(value) { }
}
new C().setX(1);
// starting method with arguments 1
// starting set #privateMember with arguments 1
// ending set #privateMember
// ending methodexport decorator @frozen {
// This decorator fires upon class creation (not instantiattion)
@register(klass => {
// When we create this class, freeze it from extention
Object.freeze(klass);
// Then iterate over any static properties of the class and freeze each one
for (const key of Reflect.ownKeys(klass)) {
Object.freeze(klass[key]);
}
// Then iterate over all keys of the class prototype and freeze each one
for (const key of Reflect.ownKeys(klass.prototype)) {
Object.freeze(klass.prototype[key]);
}
})
}import { @frozen } from "./frozen";
@frozen
class MyClass {
method() { }
}
MyClass.method = () => {}; // TypeError to add a method
MyClass.prototype.method = () => {}; // TypeError to overwrite a method
MyClass.prototype.method.foo = 1; // TypeError to mutate a methodInstead of this:
class Foo {
constructor() {
Object.defineProperty(this, 'bar', {
configurable: true,
enumerable: true,
writeable: false,
value: true,
});
}
}You should do this:
export decorator @readonly {
@initialize((instance, name, value) => {
Object.defineProperty(instance, name, {
configurable: true,
enumerable: true,
writable: false,
get() { return value; },
});
})
}And then this:
import { @readonly } from './decorators';
class Foo {
@readonly bar = true;
}Want to test it?
import { @readonly } from './decorators';
describe('decorators', () => {
describe('@readonly', () => {
it('should create a readonly field on a class member', () => {
class Test {
shouldBeWritable = true;
@readonly shouldNotBeWritable = true;
}
const test = new Test();
// Assert for normal behavior
expect(() => { test.shouldBeWritable = false }).not.toThrow();
// Assert for readonly behavior
expect(() => { test.shouldBeWritable = false; }).toThrow();
});
});
});Richard Lindsey @Velveeta