Using Services
A component in your program - usually a class - that another component depends on for something, instead of doing it itself.
Bots get the most value from Hades when they adopt a service oriented architecture where we keep the various parts of our programs small, doing just one thing. Instead of big components that do a lot, we'll prefer small components that rely on each other.
Hades excels at wiring your components automatically. This lets you focus on writing small easy to understand components while Hades fits them together for you.
Learn more about service oriented architecture and dependency injection.
Baby's First Service
Consider the minimal example bot:
export class BotService extends HadesBotService {
async onReady() {
console.log(`Logged in as ${this.client.user.username}.`);
}
}
This bot is claiming for itself the responsibility for how we do logging. In this case,
console.log
is the logging approach taken.
-
But what if we wanted to change how logging works?
-
Well we just change the line!
-
Sure, but if we have a lot of logging throughout a large project?
-
RIP.
Instead, when we need to log, we should delegate to some service specialized just for that purpose.
Delegating to a Service
Let's define a ConsoleLogger
that will capture our current logging approach:
class ConsoleLogger {
log(msg: string) { console.log(msg) }
warn(msg: string) { console.warn(msg) }
error(msg: string) { console.error(msg) }
}
Now we can delegate logging to the service:
export class BotService extends HadesBotService {
logger = new ConsoleLogger()
async onReady() {
this.logger.log(`Logged in as ${this.client.user.username}.`);
}
}
BotService
is still too picky about logging, however, deciding for itself to always use
console.log
.
Delegating to an Interface
To get BotService
fully out of the business of making decisions about logging it should instead
delegate to an interface. Because real interfaces disappear at runtime (TypeScript is type-erased),
we'll instead use abstract classes:
export abstract class ILogger {
abstract log(msg: string): void
abstract warn(msg: string): void
abstract error(msg: string): void
}
Now the bot can declare and use an ILogger
:
export class BotService extends HadesBotService {
@inject(ILogger)
logger: ILogger
async onReady() {
this.logger.log(`Logged in as ${this.client.user.username}.`);
}
}
But where does the actual logger come from? Hades! The @inject(ILogger)
decorator tells Hades
where an ILogger
is expected. When BotService
is created, Hades will ensure that it receives
one.
Binding Interfaces to Services
Hades knows that it needs to provide services for all @inject
decorated fields. But when it sees
that it needs to provide an ILogger
, it doesn't automatically know which class should be used.
So we need to tell Hades:
- Anyone who needs an
ILogger
should get aConsoleLogger
.
There are a few ways to to do that. For now we'll pass an arrow function to boot
:
boot(BotService, [
(container) => {
container
.bind(ILogger)
.to(ConsoleLogger)
.inSingletonScope()
}
])
inSingletonScope()
ensures that everyone shares the same ConsoleLogger
instance.
Again, this tells Hades that anytime someone requests an ILogger
, they should be given a
ConsoleLogger
. We can now control how logging works for everyone in a single place.
Fancy Binding Logic
Now that we know how to delegate to interfaces and tell Hades which services should be bound to which interfaces we can get a bit more sophisticated.
Let's define another ILogger
:
export class NullLogger extends ILogger {
log(msg: string) { return; }
warn(msg: string) { return; }
error(msg: string) { return; }
}
This logger doesn't do anything. But there could be cases where we want to disable logging entirely. Now that's trivial with a bit of logic in our binding function:
boot(BotService, [
(container) => {
container
.bind(ILogger)
.to(process.env.DEBUG ? ConsoleLogger : NullLogger)
.inSingletonScope()
}
])
With this change, logging will only be enabled when the DEBUG
environment variable is present. A
service oriented architecture removes this kind of decision making from your components, making them
leaner - therefore clearer. Maybe in production, instead of no logging, you log to some third-party
provider, etc.
Hopefully you can start to see the hygiene of this approach. If you are working on your project because you think it could grow into something worthwhile, Hades could be what helps your project withstand that growth.