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.
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.
/* 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();
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 (Your edit shows that you're not using class
definition across modules.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