in NestJS
@Global()
@Module({
imports: [...],
controllers: [...],
providers: [...],
exports: [...],
})
export class SomeModule {}← I don't care
Crazy idea, huh?
Nope
@AtomicService({
imports: [AppConfigModule],
})
export class SurveyScheduleAutoremindersFn {
constructor(
private readonly appConfig: AppConfigService,
private readonly someOtherFn: SomeOtherFn
) {}
...
}automated sugar
@Module({
imports: [AppConfigModule, SomeOtherFn],
providers: [SurveyScheduleAutoremindersFn],
exports: [SurveyScheduleAutoremindersFn]
})
@Injectable()
export class SurveyScheduleAutoremindersFn {
constructor(
private readonly appConfig: AppConfigService,
private readonly someOtherFn: SomeOtherFn
) {}
...
}which will be convered into smth like this
(that's a lie, it's a bit more complex than that, but PoC works)
@Module({
imports: [AppConfigModule, SomeOtherFn],
providers: [SurveyScheduleAutoremindersFn],
exports: [SurveyScheduleAutoremindersFn]
})
@Injectable()
export class SurveyScheduleAutoremindersFn {
constructor(
private readonly appConfig: AppConfigService,
private readonly someOtherFn: SomeOtherFn
) {}
...
}auto-imports
@Module({
providers: [
...,
// uses SurveyScheduleAutoremindersFn
SurveysService,
],
imports: [
...,
// thus we need to import module
getModule(SurveyScheduleAutoremindersFn)
],
exports: [...]
})
export class SurveysModuleinterop with regular modules (ugly)
@SpanModule({
providers: [
...,
// uses SurveyScheduleAutoremindersFn
SurveysService,
],
imports: [
...,
// we don't need to import AtomicModule
// cuz @SpanModule does that for us
],
exports: [...]
})
export class SurveysModuleinterop with regular modules (nice)
what's left in regular modules?
Why?
@Global()
@Module({
imports: [
FirstControllerModule,
SecondControllerModule,
// ugly imports
FirstServicesModule,
SecondServicesModule,
ThirdServicesModule,
]
})
export class AppModule {}Atomic Services
@Module({
imports: [
FirstControllerModule,
SecondControllerModule,
// no ugly imports
// at all
// forever
// cuz it's automated
]
})
export class AppModule {}easier to add services → more services → granular services
The vast majority (70%? 80%? 90%?)
of code is not modeled in OOD paradigm
(or any paradigm at all)
(and the 25% of OOD code is modeled in wrong way)
Metrics Engine is interesting example, while we can say it's proper OOD-modeling, it doesn't use NestJS DI
We wanna some new code
We write new function
We push that function into service class
"Oh, new function doing smth with people, that should be a PeopleService"
— my mental process
there is no model
behind this process
WHYYYYYYYY
Because it's too hard to write a separate service with a single function
Because you need to import new service not only into callee but also provide it in feature module (or app module)
and because we don't have time to invest into proper design - we are a startup after all
trying to achieve functions with DI
and without too much boilerplate
not trying to add any new model
we won't adopt that easily
thus why bother
Let's fix this
Oh wait, Atomic Services already almost fix that!
Let's fix this
@AtomicService({
imports: [
AppConfigModule
]
})
class SomeFn {
constructor(
private readonly appConfig: AppConfigService
) {}
async handle(...) {
...
}
}Let's fix this
@AtomicService()
class SomeFn {
constructor(
private readonly appConfig: AppConfigService
) {}
async handle(...) {
...
}
}AppConfigModule (and some other basic modules) are @Global() now
No need to import it
Let's fix this
@AtomicService()
class SomeFn {
constructor(
private readonly appConfig: AppConfigService
) {}
async handle(...) {
...
}
}AppConfigModule (and some other basic modules) are @Global() now
No need to import it
For sure:
Let's fix this
@AtomicService()
class SomeFn {
constructor(
private readonly appConfig: AppConfigService
) {}
async handle(...) {
...
}
}AppConfigModule (and some other basic modules) are @Global() now
No need to import it
Maybe:
For sure:
@AtomicFn()
class SomeFn {
constructor(
private readonly appConfig: AppConfigService
) {}
async handle(...) {
...
}
private someHelper(...) {
}
}← Simple directive, easy to use
← DI always DI
↑ Small amount of deps - cuz fns are small
← single public method
(convention, maybe eslint rule?)
← helpers are private
It's kinda hard to introduce new services because you need to manually name files:
SurveysService -> surveys.service.ts.
Let's just name it as SurveysService.ts
One more trouble
VS Code generates name SurveysService.ts
when using "Move to a new file"
When?
Old code:
When?
New code:
still can be a little pain in some cases
→
Old code:
When?
New code:
still can be a little pain in some cases
→
→
need to have some new eslint rule or smth
Old code:
I really prefer this
→
Why?
How soon we will reach 5k LoC per file?
it just becomes hard to navigate
my childhood trauma was being locked in project with 10k LoC files
Why not nestjs/cqrs?
Why not use Query/Command separation?
I like the approach if that's just a naming convention.
Not sure if we are ready to embed anything more powerful
(I have experience building that, I can build smth like that, but that just would take a time)
Pros
Cons
→
we can introduce some AtomicApiService
that can be imported only from controllers
so we can have strict separation guarantees
(not sure about this idea)