This article focuses on the application of THE JS decorator pattern when coding a custom modeler using open source BPMN-JS, and some design tips.

The basics of decorators

Decorators are familiar to anyone who develops React or uses TS to write VUe2.0.

It is mainly used to describe classes, or classes, in ES6.

Decoration, in real life, the decoration of an item is dispensable, that is, if you remove the decoration, this thing is still a complete item, and it will not change the main function of the item itself because of the change of the decoration.

At the coding level, the design pattern “decorator pattern” is involved, and the most typical example in the Java world is logging an interface.

The actual code looks like this

@ClassDecorator
class BpmnModuleImp {
    @MethodDecorator
    method() {
        console.log('call method! '); }}Copy the code

It can be modified

  • The class itself
  • Methods of a class
  • Attributes of a class

For the implementation of the decorator itself, see ECMAScript 6 Getting Started – Decorators

Because the decorator proposal is still in flux, the old decorator specification is still in use.

Why decorators

In the implementation of the process engine modeler, the core relies on BPMN-JS, which uses an architecture similar to Java’s dependency injection approach. Each complete feature is a module, which is embodied as a class class when the code is implemented.

Module definition of BPMN-JS

Similar to the following:

// A Feature module
export default class FeatureModule {
    static $inject = ['eventBus'];
    constructor(eventBus) {
        this._eventBus = eventBus; }}Copy the code

Code analysis:

  • Declare dependencies; It uses static variables$injectTo declare.
  • Dependency injection; Dependency injection will inject an instance of a dependent module into the current module through the constructor of the class.
  • Privatization dependence; In most scenarios, dependent modules are manually privatized as member variables in order to make them available to other class methods.

Bpmn-js module coding problems

For bPMN-JS module definitions, the following problems occur in actual coding:

  • Distinguish Bpmn module from ordinary class module; If a partner is added to the project and wants to develop an original feature, he may get lost between classes and put up a conspicuous decoration. At a glance, he will know which is bPMN-JS module and which is ordinary class
  • Privatized dependency module; If a module has a lot of dependencies, you might spend 10 minutes privatizing dependent modules as member variables.
  • Coding errors are inevitable. Because it’s not written in typescript, you can’t avoid low-level errors during development. There was a link when it went live to production$injectwritten$injectsThe command cannot run: -d.

Preparation of the decorator development environment

React I forgot, but if you are developing using VUE-CLI > 3.0, the built-in Babel Preset comes with decorator escape support.

If you define your own development environment and use Babel 7, install Babel: @babel/plugin-proposal-decorators

And configure it into your Babel config to support it

npm i @babel/plugin-proposal-decorators --dev
Copy the code

Decorator application

Ok, let’s implement our decorator. Since it’s for the BPMN module, I’ll name it BpmnModule

Identity module

To solve the first problem, distinguish between ordinary classes and BPMN modules.

Inject $inject declare with decorator, uniform writing, avoid typos

The decorator implementation is as follows:

// BpmnModuleDecorator.js
/** * Bpmn module decorator *@param {{ injects: string[] }} param0
 **/
export default function BpmnModule({ injects }) {
    return function (moduleCtor) { moduleCtor.$inject = injects; }}Copy the code

Let’s modify the original BPMN module definition

import BpmnModule from './BpmnModuleDecorator';

@BpmnModule({
    injects: ['eventBus']})class FeatureModule {
    constructor(eventBus) {
        this._eventBus = eventBus; }}export default FeatureModule;
Copy the code

Note that export default and decorator cannot be directly written together, because parsing will fail.

Here’s how to use it incorrectly:

import BpmnModule from './BpmnModuleDecorator';

/ / complains
@BpmnModule({
    injects: ['eventBus']})export default class FeatureModule {
    constructor(eventBus) {
        this._eventBus = eventBus; }};Copy the code

The reason is that the execution semantics of the escaped code change

Export default escaped from Webpack to module.exports; In addition, the decorator itself is a higher-order function, so the result of the compilation is changed, or directly into the code.

Correct compilation:

class FeatureModule {
    constructor(eventBus) {
        this._eventBus = eventBus; }}module.exports = BpmnModule(
    injects: ['eventBus']
)(
    FeatureModule
);
Copy the code

Error syntax compilation:

BpmnModule(
    injects: ['eventBus']
)(
    module.exports = class FeatureModule {
        constructor(eventBus) {
            this._eventBus = eventBus; }})Copy the code

The execution semantics have changed, but I have not seen the compilation result of the error, because of the error, there is no result, haha ~

Automate privatization dependency modules

It implements a simple decorator. At first glance, it looks redundant.

But it also foreshadows the current event.

Implementation effect

All we need to do now is automate the code commented below

import BpmnModule from './BpmnModuleDecorator';

@BpmnModule({
    injects: ['eventBus']})class FeatureModule {
    constructor(eventBus) {
        this._eventBus = eventBus; // These private codes need to be removed}}export default FeatureModule;
Copy the code

The effect is that you don’t need to write any code for privatized dependency modules, as shown below

import BpmnModule from './BpmnModuleDecorator';

@BpmnModule({
    injects: ['eventBus']})class FeatureModule {
    method() {
        console.log(this._eventBus) // Output dependent modules}}export default FeatureModule;
Copy the code

In order to let you gentlemen feel the stench of privatization, take each stench example, the following case for real adaptation:

/** * ExtensionElements quick operation module for child elements **/
@BpmnModule({
  injects: ['canvas'.'eventBus'.'bpmnFactory'.'modeling'.'elementHelper'.'elementFactory'.'elementRegistry']})class ExtensionElementsHelper {
    constructor(canvas, eventBus, bpmnFactory, modeling, elementHelper, elementFactory, elementRegistry) {
        this._canvas = canvas;
        this._eventBus = eventBus;
        this._bpmnFactory = bpmnFactory;
        this._modeling = modeling;
        this._elementHelper = elementHelper;
        this._elementFactory = elementFactory;
        this._elementRegistry = elementRegistry; }}Copy the code

The specific implementation

In practice, you’ll find that the decorator itself can only modify the constructor passed in, and you can’t operate on instances of its modules. And we want to automate the private dependency module is to operate module instances, so it will be very contradictory, so can only think of a special way, that is nesting baby!

Start with code ~

// BpmnModuleDecorator.js
/** * Bpmn module decorator *@param {{ injects: string[] }} param0
 **/
export default function BpmnModule({ injects }) {

    injects = Array.isArray(injects) ? injects : [];
    
    return function (moduleCtor) {
        // Module proxy
        function ModuleProxy(. injectedModules) {
            InjectedModules is a list of modules instantiated by bpmn-js
            // Create the target object
            const instance = new moduleCtor();
            // Make dependency injection a module instance private variable
            injects.forEach((moduleName, index) = > {
                instance['_' + moduleName] = injectedModules[index];
            });
            // Returns the instance, replacing the result returned by the proxy new call
            return instance;
        }

        // Copy the dependency declaration to the agent
        ModuleProxy.$inject = injects;

        // Return the proxy module and let bPMN-JS handle it
        returnModuleProxy; }}Copy the code

The idea of implementation:

  • Agent design pattern, agent module creation operation
  • Construct the properties of the call using new

Principle:

  • The agent; That is, the operations that BPMN creates the module are given to the agent in order to get the dependency injection list passed in by the construct call.

  • New constructs the properties of the call; This is basically javascript. When a new call is made, if the function returns an object, it replaces that object with the object created by the new call. (If you don’t know, you should make up for the new call.)