A thing I very often see in NodeJS applications written in Typescript is code being split into logical chunks called services (not related to microservices architecture) which are represented by classes.
When one service needs to interact with a different service it needs to have an instance of the other service, so the code very often looks something like this:
class Aservice {
constructor(private bService: Bservice) {}
public food() {
return "Arancini";
}
public foo() {
console.log(this.bService.bar());
}
}
class Bservice {
public bar() {
return "I like trains";
}
}
Like this when you want to use Aservice
you will have to make an instance of it and for that you will need to provide an instance of Bservice
, even if you won’t use any functions from Aservice
that rely on Bservice
. This is pretty annoying even with this small example but if you had 20+ services that rely on each other it would become basically impossible to provide the instances manually.
Thats why most people that structure their code like this rely on some dependency injection library, which would make the code look something like this:
@Service()
class Aservice {
constructor(
@Inject()
private bService: Bservice
) {}
public food() {
return "Arancini";
}
public foo() {
console.log(this.bService.bar());
}
}
@Service()
class Bservice {
public bar() {
return "I like trains";
}
}
Its the same as before but with added @Service()
and @Inject()
decorators which tell the dependency injection which classes it should instantiate and inject into constructors. You will be able to get an instance of Aservice
by doing something like this:
const instance = DI.getInstance(Aservice);
instance.foo()
The problem of having to provide instances manually is solved but the code has gained a bunch of boilerplate which is just not nice. And things might turn ugly again when you run into circular dependencies between services, some DI libraries don’t deal with that too well…
Why this is very often not necessary
When services are used just to structure code they don’t hold any actual state in them, the only “state” they have are the isntances of other services.
If we can get the services to not be dependent on instances of other services, they won’t have any state so we can define all their methods as static and in turn the service can be used fully without having to create an instance of it, which means other services won’t be dependant on an instance of it.
Lets see how this would look:
class Aservice {
public static food() {
return "Arancini";
}
public static foo() {
console.log(Bservice.bar());
}
}
class Bservice {
public static bar() {
return "I like trains";
}
}
// Usage
Aservice.foo();
Like this we don’t have to bother with making any instances and we still get all the benefits of using classes to structure code (inheritance, private/protected methods etc…)
Also at this point making instances of the classes is kind of useless so we can make them abstract to make it more clear that you are not supposed to make instances of them.
Full example:
abstract class ParentService {
protected static breakfast() {
return "Coffee & cigarette";
}
}
abstract class SupplierService {
public static bestFood() {
return "Pineapple";
}
}
abstract class RestaurantService extends ParentService {
public static lunch() {
return this.pasta() + " Cacio e pepe";
}
public static dinner() {
return SupplierService.bestFood() + " pizza";
}
private static pasta() {
return "Spaghetti";
}
}
// Usage
console.log(RestaurantService.lunch())
console.log(RestaurantService.dinner())
console.log(RestaurantService.breakfast()) // Typescript error because breakfast is protected
console.log(RestaurantService.pasta()) // Typescript error because pasta is private
I hope this idea will help you simplify your codebase, it certainly did for me.