Fun with Stamps. Episode 18. Dependency injection paradise
Hello. I’m developer Vasyl Boroviak and welcome to the eighteenth episode of Vasyl Boroviak presents Fun with Stamps.
TL;DR: Dependency injection issue risen in the video below solved with stamps.
Please note, that the issues can also be solved with plain functions. It’s your choice what to use — stamps or functions.
Mattias Petter Johansson released a new cool video today. This one:
First he shows classic OOP dependency injection using classes — the UserManager
class:
Then he refactors it to a set of functions called makeCreateUser
, makeDeleteUser
, makeUpdateUser
, makeBanUser
.
So the final UserManager
(aka makeUserUtils
) function using an FP approach looks like this:
The issues
The code above is good, understandable. But let me quote statements and questions risen by Mattias throughout the video.
A little bit of duplication
At 6:25 of the video:
“What we see is a little bit of duplication that will disturb your eye a little”
Typically, this little duplication becomes a large duplication in unit tests. Stamps can help you to avoid DI duplication.
We want more control
At 10:30 of the video:
I guess we could inject all dependencies into all factories. But seems to me we want a little bit more control.
Exactly where stamps shine — your logic can get as few or as many injections as you want. You do not need to over inject.
Two patterns are similar
At 17:23 of the video:
Essentially the two patterns (
UserManager
class andmakeUserUtils
function) are very similar.
Exactly! They are so very similar. They have similar (if not the same) set of downsides.
Oversharing dependencies
At 17:30 of the video:
A problem with this pattern is that we are oversharing. [snip] The
mailService
is being exposed to functionscreateUser
anddeleteUser
, even though they have no business knowing about that. [snip] It can be problematic.
With stamps you can inject two dependencies to the banUser
and one dependency to createUser
and deleteUser
. Precise control with minimal effort.
Obscure dependencies
At 18:17 of the video:
Here we can see exactly what dependencies each thing has.
But this has now become slightly more obscured from us. Dependencies starting to become this blob of things.
With stamps you select what, where and when to inject. You can inject either at the stamp declaration (like on the first image above) or at the object instance creation (like you do with classes).
Naming is hard
At 22:36 of the video:
In Java [snip] we have to name things. And naming is hard. [snip] It requires you to predict the future.
With stamps naming is still hard, but much simpler than in Java and maybe even simpler than in JavaScript. Because you don’t need to name general abstract things (like UserManager
or userUtils
). With stamps you need to name single concrete things (like UserCreator
and UserDeletor
). Stamps, while still being an OOP paradigm, do not require you to predict the future!
Do not try to predict code design upfront
At 25:50 of the video:
I would dump shit into a file until I started figuring out a very clear patterns on organisation. And then, and only then, I would start creating something like
account-services
or something, where I start putting things like user flows… I don’t know… That’s the point! You don’t know what structure make sense for an application until you are quite a bit of time into it.
I am very much agree! This is exactly what stamps give you. Atomicity. Which can be easily composed at a later stage.
Let’s rewrite the whole thing using stamps!
Unfortunately, stamps is not a language feature (yet). Thus, we will use this little module — @stamp/it
.
import stampit from '@stamp/it'
Connection dependency
const HasConnection = stampit({
props: {connection: null},
init({connection}) {
this.connection = connection || this.connection
}
})
Mail service dependency
const HasMailService = stampit({
props: {mailService: null},
init({mailSerivice}) {
this.mailService = mailService || this.mailService
}
})
Create, update, delete user logic
const UserCreator = HasConnection.compose({
methods: {
createUser(name) {
return this.connection.table('users').insert({
is_new: true,
full_name: name
})
.then(user => user.id)
}
}
})UserCreator({connection: myConn}).createUser('Mattias Johansson')
The UserDeleter
and UserUpdater
are going to be similar. So, I’ll skip them for now.
Please note, that the HasConnection
dependency is very explicit!
Also note, that the dependency is single. No dependency oversharing!
Ban user logic
const UserBanner = stampit(HasConnection, HasMailService, {
methods: {
banUser(id) {
return this.connection.table('users').update(
{ _id: id },
{ $set: { banned: true } }
)
.then(user => this.mailService('joe@smith.com', 'banned you'))
}
}
})UserBanner({connection: myConn, mailService: myMailService})
.banUser(34)
As you can see, we have more control over the dependencies a function has. And those are not obscure dependencies, but quite explicit. In the example above we can clearly see that to ban a user we need to supply both connection and mail service.
You can even check which dependencies a stamp has:
> console.log(UserBanner.compose.props)
{connection: null, mailService: null}
And pre-inject them to avoid a little bit of code duplication in unit tests:
const UserBanner = require('./UserBanner').props({
connection: { // mocking connection with a dummy object
table() { return { update() { return Promise.resolve() } } }
},
mailService() {} // mocking the mail service too
})UserBanner().banUser(34) // THIS WILL NOT CONNECT TO DB OR SEND MAIL
Do not try to predict code design upfront
Now we can assemble the UserManager
!
const UserManager = stampit(
UserCreator, UserDeleter, UserUpdater, UserBanner)const userManager =
UserManager({connection: myConn, mailService: myMailService})
.banUser(34)
Or, if the future goes another direction, we can assemble differently:
const UserCrud = stampit(UserCreator, UserDeleter, UserUpdater)// Using EARLY dependency injection
const UserFlow = UserBanner.compose({
props: {
userCrud: UserCrud({connection: myConnection})
}
})// connection was injected EARLIER, at the stamp composition step
UserFlow({mailService: myMailService})
.banUser(34)
Or:
const UserFlow = UserBanner.compose({
props: { userCrud: null },
init({connection}) { // Using LATE dependency injection
this.userCrud = UserCrud({connection})
}
})// need to inject both at the LATER stage of object creation
UserFlow({connection: myConn, mailService: myMailService})
.banUser(34)
Let’s rewrite it all over again but using stamps’ shorter syntax
import stampit from '@stamp/it'
import ArgOverProp from '@stamp/arg-over-prop'const HasMailService = ArgOverProp.argOverProp('mailService')
const HasConnection = ArgOverProp.argOverProp('connection')const HasUserTable = stampit(HasConnection).methods({
getUserTable() { return this.connection.table('users') }
})const UserCreator = HasUserTable.methods({
create(name) {
return this.getUserTable().insert({
is_new: true,
full_name: name
})
}
})const UserUpdater = HasUserTable.methods({
update(query, changes) {
return this.getUserTable().update(query, changes)
}
})const UserDeleter = HasUserTable.methods({
delete(id) {
return this.getUserTable().remove({ _id: id })
}
})const UserBanner = stampit(UserUpdater, HasMailService).methods({
ban(id) {
return this.update({ _id: id }, { $set: { banned: true } })
.then(user => this.mailService(user.email, 'banned you'))
}
})const UserCrud = stampit(UserCreator, UserUpdater, UserDeleter)
That was quite short. Right? But in addition we get very flexible DI!
// EARLY inject dummy dependencies only once!
const UserBanner = require('../UserBanner')
.methods({
update() { return Promise.resolve({email: 'x@y.z'}) }
})
.props({ mailService() {} })describe('UserBanner', () => {
it('should set banned flag to false', (done) => {
const MockedUserBanner = UserBanner.methods({
update(query, changes) { // inject only necessary dependency
expect(query._id).toBe('1234')
expect(changes.banned).toBe(false)
done()
}
}) MockedUserBanner().ban('1234')
}) it('should email after banning', (done) => {
const MockedUserBanner = UserBanner.props({
mailService(email, text) { // inject only necessary dependency
expect(email).toBe('x@y.z')
expect(text).toBe('banned you')
done()
}
}) MockedUserBanner().ban('1234')
})
})
As you can see, we injected dummy dependencies only once on top of the file. Each test mocked only the necessary minimum. This helps us to avoid much copy paste and test file bloat.
Summary
I don’t have any of those problems Mattias Petter Johansson described in his video “Advanced Dependency Injection without classes” because I do stamps. Mattias himself don’t have the problems too because he prefers function composition and does DI with currying, etc.
With stamps you can cover code of any complexity without sinon
or proxyquire
or anything like that. (See the previous episode.)
Stamps are simple, unless you already do classes or FP. In this case your mind would oppose the idea of OOP via stamps.
There is certainly a downside in stamps — they are a very new paradigm. It’s hard to unlearn the frigging classes and move to stamps.
Have fun with stamps!
- Episode 1. Stamp basics
- Episode 2. Dependency injection in FP
- Episode 3. Comparing with the ES2015 classes
- Episode 4. Implementing stamps yourself in 30 LOC
- Episode 5. Composition design pattern
- Episode 6. Statics — properties on stamps
- Episode 7. Early and late dependency injection
- Episode 8. Tracking and overriding composition
- Episode 9. Detaching compose()
- Episode 10. My stamp mental model
- Episode 11. Interfering composition
- Episode 12. New @stamp home
- Episode 13. Method collision control
- Episode 14. New @stamp/it as a replacement of Stampit
- Episode 15. The @stamp/ modules ecosystem
- Episode 16. TypeScript mix-in classes vs Stamps
- Episode 17. Easy 100% unit test coverage in JS
- Episode 18. Dependency injection paradise (this article)
- Episode 19. Java/C# abstract methods in JavaScript
- Episode 20. Stampit v4
- Episode 21. Private data in JavaScript. 4 ways using stamps
- Episode 22. JavaScript instanceof as composable stamp
- Episode 23. New stampit.js.org with all the docs
- Episode 24. New “name” feature