javascriptfunctiondecoratorwrappermethod-modifier

Common check for two functions which prevents calling functions from executing further


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:

  1. Build a common function which would to be called first like
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.

  1. Split up the common function into one which checks the param and one which does the error handling like
    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.


Solution

  • 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.