Search code examples
javascriptooprefactoringecmascript-5

Javascript - How to define a class in multiple modules?


I have a class, which is becoming really long. I have identified multiple different concepts that, together, they give meaning to my class.

"MyService" class is composed by: "auth", "storage", "database", ... concepts.

I have thought (maybe I am wrong) that defining the class "MyService" in multiple files will be a good refactoring. Something like:

MyService/MyService.js import myService from "./myService";

export default function MyService() {
     if(!isInitialized) {
         myService.initializeApp(config);

         // Only after doing myService.initializeApp(config);
         this.auth = myService.auth();
         this.storage = myService.storage();
         this.database = myService.database();

         isInitialized = true;
     }
}

MyService/Auth.js

Add auth methods to MyService class prototype

MyService/Storage.js

Add storage methods to MyService class prototype

...

How can I do that? Does this technique has a name/exists? Is it a good way to refactor my class? If yes, why? And if not, then, why?

Thank you.

UPDATE

Maybe, splitting the class into smaller classes is the best idea, but I don't know how to make it work as for accessing myService functionalities, this line

myService.initializeApp(config);

must have been previously executed on a singleton.

EXAMPLE BASED IN THE SOLUTION

/* Simulate myService CLI SDK */
function myService() {}

myService.key = undefined;

myService.initializeApp = function (key) {
  this.key = key;
} 

myService.auth = function () {
  if(!this.key) {
    throw new Error("Please, initialize app");
  }

  console.log(`You have access to all auth methods! Key: ${this.key}`);
}

myService.storage = function () {
  if(!this.key) {
    throw new Error("Please, initialize app")
  }

  console.log(`You have access to all storage methods! Key: ${this.key}`);
}



/* Classes that compose the main class */

function Auth() {
  this.auth = myService.auth();
}

Auth.prototype.login = function (username, password) {
  console.log("Login!");
}

function Storage() {
  this.storage = myService.storage();
}

Storage.prototype.uploadFile = function (file, path) {
  console.log("Uploading file!")
}




/* Main class */

let isInitialized = false;
function Firebase() {
  if(!isInitialized) { // Singleton
    myService.initializeApp("raul");
    this.auth = new Auth(); // Composition
    this.storage = new Storage(); // Composition
    isInitialized = true;
  }
}

const f = new Firebase();
f.auth.login();
f.storage.uploadFile();


Solution

  • Instead of spreading MyService across multiple modules, you're probably better off breaking it into smaller classes. You've said:

    "MyService" class is composed by: "auth", "storage", "database", ... concepts.

    so it may make sense to have a class for auth, another for storage, another for database, and then have MyService use instances of those other classes as part of its implementation.

    You can't spread a class definition across modules. (Your edit shows that you're not using class syntax for some reason.) You can add near-methods to a class after the fact, like this:

    class Example {
        // ...
    }
    

    Then in another module:

    import Example from "./Example.js";
    
    // (You'd probably have a utility method for this, `addMethod` or similar)
    Object.defineProperty(Example.prototype, "methodName", {
        value() {
            // ...method implementation...
        },
        enumerable: false, // (You can leave this off, false is the default)
        configurable: true,
        writable: true,
    });
    

    but it's probably not best practice to have the definition scattered around like that, and there are limitations, especially in subclasses (super in a method added this way will not work the way super in a method defined within the class definition would). (Your edit shows that you're not using class syntax, so that isn't relevant to you.)

    So I would stick to breaking MyService into smaller classes that can be composed.


    You may be wondering why I used Object.defineProperty above, rather than just:

    Example.prototype.methodName = function() {
        // ...
    };
    

    The difference is enumerability. If you add a method using assignment like that, it creates an enumerable property, but if you use Object.defineProperty as shown above it doesn't. I'm using the same flags for enumerable, configurable, and writable that class definitions use when creating methods. Here's an example of the difference:

    class A { }
    Object.defineProperty(A.prototype, "example", {
        value() {},
        enumerable: false,
        configurable: true,
        writable: true,
    });
    
    class B { }
    B.prototype.example = function() { };
    
    const a = new A();
    console.log(`typeof a.example = ${typeof a.example}`);
    console.log(`a's enumerable non-Symbol properties:`);
    let count = 0;
    for (const name in a) {
        console.log(`* ${name}`);
        ++count;
    }
    console.log(`Total: ${count}`);
    
    const b = new B();
    console.log(`typeof b.example = ${typeof b.example}`);
    console.log(`b's enumerable non-Symbol properties:`);
    count = 0;
    for (const name in b) {
        console.log(`* ${name}`);
        ++count;
    }
    console.log(`Total: ${count}`);
    .as-console-wrapper {
        max-height: 100% !important;
    }

    Note that a doesn't have any enumerable properties, but b does — because it inherits an