Functional Core Imperative Shell
Listed in order of preference. Pure functions preferred over shells which are preferred over “other” functions.
- pure functions:
- no side effects
- just take data in and data out
- they should always produce the same output for the same input (not dependent on result of a DB query, etc)
- no mocks should ever be needed
- test using unit tests
- imperative shell
- must only have one path of execution (no branching)
- not as critical, but really shouldn’t have any logic
- the top level shell needs to decide what to do with errors, but generally it should do so by passing the final promise chain to another function (a pure function)
- other functions
- must be either integration tested or unit tested with mocks
- must only be called by an imperative shell or “other function”
// route handler
// imperative shell
function handleGetCommentsForUserRequest(req, res, next) {
const userId = req.params.userId
const promise = getCommentsForUserFacade(userId)
return sendGetCommentsForUserResponse(res, next, promise)
}
// top level facade
// imperative shell
function getCommentsForUserFacade(userId) {
const promise = getCommentsForUserFromCache(userId)
.then(healCommentsForUserFromApi)
return handle
}
// function that breaks all the rules
// must be integration tested (or unit tested with mocks)
// must be called by an imperative shell
function getCommentsForUserFromCache(userId) {
return pgClient.execute(...)
}
// function that breaks all the rules
// must be integration tested (or unit tested with mocks)
// must be called by an imperative shell
// TODO: can i make this an imperative shell so i don't have to test it w/ mocks?
const healCommentsForUserFromApi = userId => commentsFromCache => {
let dtos
if (commentsFromCache) return Promise.Resolve(commentsFromCache)
return axios.get('http://example.com/some/path/' + userId)
.then(buildUserCommentsDtos)
.then(r => dtos = r)
.then(saveUserCommentDtos)
.then(() => dtos)
}
// function that breaks all the rules
// must be integration tested (or unit tested with mocks)
// must be called by an imperative shell
function saveUserCommentDtos(dtos) {
return dtos.map(dto => pgClient.execute('insert into ...', Object.values(dto)))
}
// pure function
// unit tested
function buildUserCommentsDtos(data) {
return new Dto({
foo: data.foo,
...
})
}
function getCommentsForUserFromApi(userId) {
}