Skip to content

Latest commit

 

History

History
371 lines (285 loc) · 9.01 KB

Processors.md

File metadata and controls

371 lines (285 loc) · 9.01 KB

Processors

The single object parameter passed to the @define decorator (or define static method) contain processor entries. The name of each property is the name of the processor and the value is the processor's argument:

    import { Widget, define } from '@epiphanysoft/oo';

    @define({
        //... processors go here
        foo: 42  // pass 42 to the "foo" processor
    })
    class MyWidget extends Widget {
        //...
    }

The following processors are built-in:

Each processor maps to a static method on the class. The name of this method is derived from the processor name:

    import { Widget, define } from '@epiphanysoft/oo';

    @define({
        mixins: MyMixin
    })
    class MyWidget extends Widget {
        //...
    }

Would be the same as:

    import { Widget } from '@epiphanysoft/oo';

    class MyWidget extends Widget {
        //...
    }

    MyWidget.applyMixins(MyMixin);

Why Not Use Multiple Decorators?

It is perhaps tempting to view each of these processors as their own decorators (for example, @mixin). While this can work in many cases, using multiple decorators does not ensure a consistent order.

Instead that order is lexically determined. For example, consider these classes:

    @foo @bar
    class FooBar {
    }

    @bar @foo
    class BarFoo {
    }

The different order of the above decorators results in different execution order. In many cases this difference will not matter, but if the @foo and @bar decorators intersect in some way, their order can be important.

chains

Indicates that the specified methods should be managed as a chain. Unlike normal methods that derived classes implement and use super.method() calls to invoke inherited methods, method chains are invoked across the class hierarchy.

Consider these classes:

    @define({
        chains: ['init']
    })
    class MyBase extends Widget {
        initialize (x, y) {
            this.callChain('init', x, y);
        }
        
        init (x, y) {
            console.log('MyBase init', x, y);
        }
    }

    class MyMixin extends Widget {
        init (x, y) {
            console.log('MyMixin init', x, y);
        }
    }

    @define({
        mixins: MyMixin
    })
    class MyDerived extends MyBase {
        init (x, y) {
            console.log('MyDerived init', x, y);
        }
    }
    
    let inst = new MyDerived();
    
    inst.initialize(1, 2);
> MyBase init 1 2
> MyMixin init 1 2
> MyDerived init 1 2

The base MyBase defines a method chain on the init method. The goal is to enable derived classes and mixins to implement init methods without orchestrating the exact call sequence. Instead the initialize() method calls all of the init() implementations in the various classes and mixins using callChain(). This ensures that all init() methods are called and in the correct, top-down order.

For the method chain to work properly, the mixins processor needs to know not to copy such methods. The chains processor tracks these methods so that mixins behave properly.

config

This processor defines one or more config properties.

mixins

Mixins are similar to a base class in that they are a way to inherit functionality from one class to another.

    @define({
        mixins: [ MyMixin, MyOtherMixin ]
    })
    class MyDerived extends MyBase {
        //
    }

Because JavaScript only truly understands single-inheritance via its prototype chain, the properties (both static and on the mixin's prototype) are copied from MyMixin and MyOtherMixin onto MyDerived. This is only performed if there is no collision on the name of the property. In other words, properties already defined on MyDerived or inherited from MyBase are not overridden by the mixins.

See here for more information on mixins.

processors

This processor allows a class to define and order custom processors for its use as well as for use in derived classes.

    @define({
        processors: {
            foo: 'bar',   // "foo" requires "bar" to run first
            bar: true
        }
    })
    class FooBar extends Widget {
        static applyFoo (foo) {
            console.log('applyFoo: ', foo);
        }
        
        static applyBar (bar) {
            console.log('applyBar: ', bar);
        }
    }

In the above, FooBar adds a foo and bar processor and implements their logic in the applyFoo and applyBar static methods. The order of these processors is also indicated so that applyBar will run before applyFoo.

See below for more information on custom processors.

properties

Defines properties on the class prototype. This is primarily useful for controlling the property options as opposed to prototype.

    @define({
        properties: {
            foo: {
                value: 42
            }
        }
    })
    class Something extends Widget {
    }

The above is equivalent to the following:

    Object.defineProperties(Something.prototype, {
        foo: {
            value: 42
        }
    });

prototype

Copies properties to the class prototype. This is an easy way to provide a constant object shape.

    @define({
        prototype: {
            foo: 0,
            bar: true
        }
    })
    class Something extends Widget {
    }

The above is equivalent to the following:

    Object.assign(Something.prototype, {
        foo: 0,
        bar: true
    });

static

Copies properties to the class constructor.

    @define({
        static: {
            all: new Map()
        }
    })
    class Something extends Widget {
    }

The above is equivalent to the following:

    Object.assign(Something, {
        all: new Map()
    });

Custom Processors

Processors are class mutation directives. The processors processor allows class authors to add new processors to the @define mechanism. The primary reason to write processors instead of decorators is to ensure proper order of operations.

By default, inherited processors (such as prototype) will be applied before derived class processors so this order is not typically a concern. When defining two processors, however, it is worth considering their order:

    @define({
        processors: {
            foo: true,
            bar: 'foo'   // "bar" requires "foo" to run first
        }
    })
    class FooBar extends Widget {
        static applyFoo (foo) {
            console.log('applyFoo: ', foo);
        }
        
        static applyBar (bar) {
            console.log('applyBar: ', bar);
        }
    }

This class adds a foo and bar processor and specifies their order of operation. When processors are registered for a class, @define runs their static applier methods in the specified order.

For example:

    @define({
        foo: 1,
        bar: 2
    })
    class FooBarUser extends FooBar {
        //
    }
> applyFoo: 1
> applyBar: 2

The name of the applier method is computed from the processor name:

appierName = 'apply' + name[0].toUpperCase() + name.substr(1);

Advanced Processor Options

Let's now consider a processor that defines properties on the class prototype. Since the prototype processor also places properties on the class prototype, there is room for these processors to conflict.

Assume that the new processor should be executed before prototype:

    @define({
        processors: {
            foo: {
                before: 'prototype'
            }
        }
    })
    class WidgetWithCustomProcessor extends Widget {
        static applyFoo (foo) {
            // runs before prototype processor...
        }
    }

When the value of a key in the object given to the processors processor is an object, it can use two properties to configure its behavior:

  • after: The processors that this processor must execute after.
  • before: The processors that this processor must execute before.

Any value other than an object or string for a processor is ignored. This is also true of any properties other than before and after in an object value.

In the first example, the processors could have be expressed as:

    @define({
        processors: {
            foo: {
                after: 'bar'  // "foo" requires "bar" to run first
            },
            bar: true  // the value "true" is ignored
        }
    })