Fun with Stamps. Episode 18. Dependency injection paradise

Vasyl Boroviak
7 min readSep 11, 2017

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:

Advanced Dependency Injection without classes — Fun Fun Function

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.

Example of one of makeCreateUser function

So the final UserManager (aka makeUserUtils) function using an FP approach looks like this:

Final UserManger (aka makeUserUtils) file

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 and makeUserUtils 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 functions createUser and deleteUser, 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!

--

--