I recently started to work in a startup that been around for almost 6 years now. Our main code base is in Node.js, which means that it has been used here since the early days and therefore there is a fair share of "legacy code" running.
There are some issues, but right now I'm struggling with a specific one: how to deal with code that should just be executed in an specific environment?
There are dozens of code snippets like:
if (isDevelopment) {
doThis();
} else if (isLocal) {
doThat();
} else if (isProduction) {
doOtherStuff();
}
IMHO, this is definetly a code smell.
Remember DRY. Application logic is now scattered through several files, which makes changes very cumbersome.
It's also difficult to remove or add new environments -- this is not a need right now, but who knows? For example, having a local
and a development
environment seems redundant to me.
I've been thinking in better ways of achieving the same results. My first idea was to abstract away the code, writing separate modules for each environments when needed, for example:
// lib/production/some-module.js
module.exports = // some production code
// lib/development/some-module.js
module.exports = // some development code
So, I would set the NODE_ENV
variable and in the module that is a client of some-module
, I could do:
const someModules = require('/path/to/lib/'
+ process.env.NODE_ENV + 'some-module')
I could of course wrap this into an utility function:
function envRequire(prefix, moduleName) {
return require(prefix.replace(/\/$/, '') + '/'
+ process.env.NODE_ENV + '/' + moduleName)
}
Then, in my code I'd have something like:
const commonModule = require('/path/to/common-module')
const envAwareModule = envRequire('/path/to', 'my-module')
The advantages here is that I no longer need conditionals spread all over the application and environment-specific code can be tied toghether. Adding or removing environments it now less convoluted.
The disadvantages I see is that I'd probably have to write more code and if some module has different logic for only one environment, I'd have to replicate the "common" version in the others.
I could work around the latest by using symlinks or a "default" environment that could be used as a fallback.
Is this a good approach or can I do better in terms of workload, flexibility and code resilience?
Edit
Thinking about @jfriend00's answer, I started to wonder:
What if I abstracted some concepts into a base object and implemented specific code in separate module inheriting from the base?
My only question then would be how to load the proper module per environment. I could have a modules
entry in my configuration files that would tell what are the right module to run.
Something like:
// conf.production.json
{
// ...
"modules": {
"database-setup" : "./lib/database/setup-production"
},
// ...
}
// conf.development.json
{
// ...
"modules": {
"database-setup" : "./lib/database/setup-development"
},
// ...
}
Then I'd define an "interface" for them in lib/database/setup-base
and both could implement it.
I could define a helper module:
const conf = require.main.require(`./conf/conf.${process.env.NODE_ENV}`)
module.exports = function envRequire(moduleName) {
if (conf.modules[moduleName] === undefined) {
throw new Error('Module not set in conf')
}
return require(conf.modules[moduleName])
}
And use it at bootstrap
const envRequire = require('./helper/env-require')
const dbSetup = envRequire('database-setup')
Any thoughts about this approach?
if
approach has other disadvantages, such as requiring changing several files whenever the behavior in a given environment should be changed. In our current state, this is not something easily fixable, but I'd like to establish a good standard for new projects.