Let's say we have two functions which first do some parameter checking. They return early if the checks are not met. Then they do stuff. Quite simple:
function funcA (inputParam) {
if(!inputParam.valueA) {
// Do error handling
return;
}
// Do stuff
}
function funcB (inputParam) {
if(!inputParam.valueB) {
// Do error handling
return;
}
// Do stuff
}
Now, after implementing and using those functions frequently, we realize that there is another condition which needs to be met in both functions. Of course, we do not want to copy&paste that parameter check, but re-use it for both functions. It is not that simple to put it into a function and call the function because the called function would need to make sure that the rest of the caller function is not executed if the param check fails.
A function like
function checkC (inputParam) {
if(!inputParam.valueC) {
// Do error handling
return;
}
}
will not work, because if we call it like
function funcA (inputParam) {
checkC(inputParam)
// Do stuff
}
then it will always reach Do stuff
no matter the outcome of the parameter check, but if you do
function funcA (inputParam) {
return checkC(inputParam)
// Do stuff
}
then Do stuff
will never be reached of course.
So, what would be the most elegant way which requires the least refactoring?
Ideas I came up with:
function commonFunction(inputParam, proceedWithAOrB) {
if(!inputParam.valueC) {
// Do error handling
return;
}
if(proceedWithAOrB === 'a') {
return funcA (inputParam);
}
if(proceedWithAOrB === 'b') {
return funcB (inputParam);
}
}
But that would require a significant amount of refactoring.
function checkParamC (inputParam) {
if(!inputParam.valueC) {
return false;
}
return true
}
function checkParamCErrorHandling (inputParam) {
// Do error handling
}
function funcA (inputParam) {
if(!checkParamC (inputParam)) {
return checkParamCErrorHandling (inputParam);
}
// Do stuff
}
That way, we would not need to copy&paste actual logic, but it is still not very elegant.
I feel like that I'm missing something and there must be a more elegant solution to this?
Edit For some reason, throw new Error...
is not an option in my case.
The scenario the OP describes asks for a solution based on what is known as decorators. In JavaScript one way of implementing this pattern was to provide them as function- respectively method-modifiers, implemented as methods itself at the Function.prototype
.
The OP's specific use case can be solved best with an universal around
modifier. One just has to integrate the slightly changed implementation of the newly provided "C"-check (hence the OP's checkC
function) into a specific around
-handler function where the latter gets passed as the first parameter to the before mentioned around
method of e.g. funcA
and funcB
in order to create modified versions of each function with both now featuring the additional behavior that has been provided with the passed around
-handler.
... example code ...
// - code which should not anymore be touched.
function funcA(inputParam) {
if (!inputParam.valueA) {
console.log('`funcA`/`valueA` specific error handling.');
return;
}
console.log('`funcA` ... do stuff ...');
}
function funcB(inputParam) {
if (!inputParam.valueB) {
console.log('`funcB`/`valueB` specific error handling.');
return;
}
console.log('`funcB` ... do stuff ...');
}
// - a much later additonally spcified check
// which has to be run with the invocation
// of either of the above functions.
function doesPassAdditionalCheckC (inputParam) {
let isValid = true;
if (!inputParam.valueC) {
console.log('`additionalCheckC`/`valueC` specific error handling.');
isValid = false;
}
return isValid;
}
// - implement an `around`-modifier and
// "C"-check specific handler-function.
function proceedOnlyInCaseCheckCDoesPass(proceed, handler, ...args) {
const inputParam = args[0];
if (doesPassAdditionalCheckC(inputParam)) {
proceed(inputParam);
}
}
const valueA = 'foo';
const valueB = 'bar';
const valueC = 'baz';
console.log('\n+++ BEFORE MODIFICATION ... `A`/`B`-checks +++\n');
console.log('\nfuncA({}) ...'), funcA({});
console.log('\nfuncB({}) ...'), funcB({});
console.log('\nfuncA({ valueA }) ...'), funcA({ valueA });
console.log('\nfuncB({ valueB }) ...'), funcB({ valueB });
// - create and reassign the modified
// version of a function, each from
// and to its original reference.
funcA = funcA.around(proceedOnlyInCaseCheckCDoesPass);
funcB = funcB.around(proceedOnlyInCaseCheckCDoesPass);
console.log('\n+++ AFTER MODIFICATION ... `C` check-first +++\n');
console.log('\nfuncA({}) ...'), funcA({});
console.log('\nfuncB({}) ...'), funcB({});
console.log('\nfuncA({ valueC }) ...'), funcA({ valueC });
console.log('\nfuncB({ valueC }) ...'), funcB({ valueC });
console.log('\nfuncA({ valueC, valueA }) ...'), funcA({ valueC, valueA });
console.log('\nfuncB({ valueC, valueB }) ...'), funcB({ valueC, valueB });
console.log({
funcA,
'funcA.origin': funcA.origin,
'funcA.handler': funcA.handler,
});
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
(() => {
function isFunction(value) {
return (
typeof value === 'function' &&
typeof value.call === 'function' &&
typeof value.apply === 'function'
);
}
function asConfiguredModification(modifierName, origin, handler, modified) {
const nameDescriptor = Object.getOwnPropertyDescriptor(origin, 'name');
Object.defineProperty(modified, 'name', {
...nameDescriptor,
value: `modified::${ modifierName } ${ nameDescriptor.value }`,
});
Object.defineProperty(modified, 'origin', { get: () => origin });
Object.defineProperty(modified, 'handler', { get: () => handler });
return modified;
}
function asConfiguredModifier(name, modifier) {
Object.defineProperty(modifier, 'name', {
...Object.getOwnPropertyDescriptor(modifier, 'name'),
value: name,
});
return modifier;
}
const modifierConfig = { writable: true, configurable: true };
// - implementing the behavior of
// an `around` modifier method.
function aroundModifier(handler, target) {
'use strict';
const proceed = this;
target = target ?? null;
return (
isFunction(proceed) &&
isFunction(handler) &&
asConfiguredModification(
'around', proceed, handler,
function /* aroundType */(...args) {
return handler.call((this ?? target), proceed, handler, ...args);
},
)
) || proceed;
}
Object.defineProperty(Function.prototype, 'around', {
...modifierConfig, value: asConfiguredModifier('aroundModifier', aroundModifier),
});
})();
</script>
Additional note
Method modifiers can be implemented in a way that they handle synchronous as well as asynchronous functions/methods for following scenarios around
, before
, after
(Returning
), afterThrowing
, and afterFinally
with the latter two being capable of wrapping each a special try...catch
handling around a function/method.
Thus with a combination of afterThrowing
/afterFinally
and around
the OP can apply, to code bases that for "reasons" can not be touched anymore, a clean solution in terms of how to wrap for instance repetitive tests (e.g. guards) around other base functionality or even how to handle throwing code for functions which never have been provided with try...catch
based error handling in the first place.