0

I want a JavaScript class that can conditionally add additional methods into itself from separate files. The idea is to separate different concerns of the app into more manageable self-contained modules that nevertheless can interact with the methods in the mother app class. Therefore, the additional methods in the separate file must be able to reference the methods and variables in the main class. See the code sample below.

I looked at a lot of different solutions, but all of them have a downside for what I'm wanting.

  • I could do new Uploads(), but I could not find a way for methods in Uploads.js to reference methods in the main App class
  • I could use extends, but this would not allow for conditional extension AFAIK
  • I could just define new methods into the prototype itself, but this would mean that the external file would need to "know" about the class it's going to be used in, which doesn't make it widely reusable.

The best I have so far is the following:

app.js

const Uploads = require("./Uploads.js");
const config = { hasUploads: true }; // Probably loaded from a file

class App {
    constructor() {
        /* Only include the separate module if the config says so */
        if(config.hasUploads) {
            Object.assign(this, Uploads);
        }
    }

    foo() {
        /* Something */
    }
}

Uploads.js

module.exports = {
    bar() {
        this.foo();
    }
};

It works, but I don't know if this is the best solution;

  • There's no constructor, so if Uploads.js needs to do some setup, app.js needs to contain the logic to do so (or at least know to call some uniquely named faux constructor method), which doesn't seem ideal.
  • If Uploads.js contains a method with the same name as app.js or any other possible module being loaded, they will be overwritten, leading into unexpected behaviour.
  • The Uploads.js is an object of functions, whereas app.js defines a class. Ideally (though I guess not necessarily) for code manageability they should both use the same syntax.

Is there a better/cleaner/nicer way of doing this?

Emphram Stavanger
  • 4,158
  • 9
  • 35
  • 63
  • This smacks of violating a number of object oriented principles, but I'll let the OO scholars chime in on that. Practically speaking, I would typically define the base class with a dummy or abstract Uploads implementation (that does nothing) and then you can either derive from that class and supply a new implementation of that method in the derived class or you can conditionally replace the existing implementation on any given instance. – jfriend00 Sep 04 '20 at 23:59
  • Splitting one huge `class` across multiple files is just not going to work. As you said yourself, you want *self-contained* modules instead. – Bergi Sep 05 '20 at 00:09
  • 1
    "*I could do `new Uploads()`, but I could not find a way for methods in `Uploads.js` to reference methods in the main `App` class*" - pass the `App` instance as an argument to the uploads constructor: `this.uploads = new Uploads(this);` (inside the `constructor` of `App`). – Bergi Sep 05 '20 at 00:11
  • I can fully accept that my approach may not be what is meant to be done... but I do think the underlying problem of wanting to split out a single huge class into smaller more manageable files is in itself a valid problem. If I understood correctly though, deriving from a dummy class would also mean that the ultimately "complete" class would end up being Uploads, not App, right? And whatever class the final app needs to invoke would change based on the config? – Emphram Stavanger Sep 05 '20 at 00:14
  • Use JS "prototype". See several previously answered questions e.g. https://stackoverflow.com/questions/13521833/javascript-add-method-to-object – Ari Singh Sep 05 '20 at 00:17

2 Answers2

3

Instead of trying to perform some kind of crazy multi inheritance, why not try embracing composition? Its very good for solving these kinds of problems.

class App {
    
    constructor(modules) {
      if (modules.uploads) {
        this.uploads = modules.uploads(this);
      }
    }
    
    foo() {
        console.log('foo!');
    }
}

class Uploads {
  constructor(context) {
    this.context = context;
  }
  
  method() {
    this.context.foo();
  }
}

const app = new App({ uploads: (ctx) => new Uploads(ctx) });
app.uploads.method();

You can get really fancy with this and use builders to configure apps with specific types of modules.

Depending on your anticipated complexity, you might want to think about using event buses, mediators, or commands to decouple things from the host itself.

Brenden
  • 1,947
  • 1
  • 12
  • 10
0

One option to fix overwriting an existing method from the uploads file is to assign new methods in a loop and check for duplicates (Object.assign is not ideal in this case) and only add updates once:

const Uploads = {
    bar() {
         this.foo("called from bar");
    }
};

const config = { hasUploads: true, // Probably loaded from a file
                 configured: false
};

class App {
    constructor() {
        /* Only include the separate module if the config says so */
        if(config.hasUploads && !config.configured) {
             const proto = this.constructor.prototype;
             const methods = Object.keys(Uploads);
             methods.forEach( name=> {
                 if( proto[ name] ) {
                    throw new Error( "App already has method " + name);
                 }
                 proto[name] = Uploads[name];
             });
             config.configured = true;
        }
    }

    foo(arg) {
        /* Something */
        console.log( arg );
    }
}

const app = new App();
app.bar();

A better (cleaner) alternative might be to add updates to the class before calling its constructor, using a static class method because its this value is the constructor function. Tested example:

static addMethods(uploads) {  // inside class declaration
    const proto = this.prototype;
    for (const [name, method] of Object.entries(uploads)) {
         if( proto[name]) {
             throw new Error("App already has a ${name} method");
         }
         proto[name] = method;
    }
}

to be called as needed by

 if( config.hasUploads) {
     App.addMethods( Uploads);
 }
traktor
  • 17,588
  • 4
  • 32
  • 53
  • "*The uploads file shown in the post is not a valid object initialiser*" - actually it is, using [concise method definitions](https://stackoverflow.com/q/32404617/1048572) – Bergi Sep 05 '20 at 00:14
  • Thanks for letting me know @Bergi , I hadn't been aware of concise function/getter syntax. Answer updated in accordance, with completion and testing of the static method alternative. – traktor Sep 05 '20 at 01:01