开发者

Should we validate method arguments in JavaScript API's?

I'm developing a JavaScript library that will be used by 3rd party developers. The API includes methods with this signature:

function doSomething(arg1, arg2, options)

  • arg1, arg开发者_开发百科2 are 'required' simple type arguments.
  • options is a hash object containing optional arguments.

Would you recommend to validate that: - argument types are valid? - options attributes are correct? For example: that the developer didn't pass by mistake onSucces instead of onSuccess?

  • why do popular libraries like prototype.js do not validate?


You have the right to decide whether to make a "defensive" vs. a "contractual" API. In many cases, reading the manual of a library can make it clear to it's user that he should provide arguments of this or that type that obey these and those constraints.

If you intend to make a very intuitive, user friendly, API, it would be nice to validate your arguments, at least in debug mode. However, validation costs time (and source code => space), so it may also be nice to leave it out.

It's up to you.


Validate as much as you can and print useful error messages which help people to track down problems quickly and easily.

Quote this validation code with some special comments (like //+++VALIDATE and //--VALIDATE) so you can easily remove it with a tool for a high-speed, compressed production version.


Thanks for the detailed answers.

Below is my solution - a utility object for validations that can easily be extended to validate basically anything... The code is still short enough so that I dont need to parse it out in production.

WL.Validators = {

/*
 * Validates each argument in the array with the matching validator.
 * @Param array - a JavaScript array.
 * @Param validators - an array of validators - a validator can be a function or 
 *                     a simple JavaScript type (string).
 */
validateArray : function (array, validators){
    if (! WL.Utils.isDevelopmentMode()){
        return;
    }
    for (var i = 0; i < array.length; ++i ){            
        WL.Validators.validateArgument(array[i], validators[i]);
    }
},

/*
 * Validates a single argument.
 * @Param arg - an argument of any type.
 * @Param validator - a function or a simple JavaScript type (string).
 */
validateArgument : function (arg, validator){
    switch (typeof validator){
        // Case validation function.
        case 'function':
            validator.call(this, arg);
            break;              
        // Case direct type. 
        case 'string':
            if (typeof arg !== validator){
                throw new Error("Invalid argument '" + Object.toJSON(arg) + "' expected type " + validator);
            }
            break;
    }           
}, 

/*
 * Validates that each option attribute in the given options has a valid name and type.
 * @Param options - the options to validate.
 * @Param validOptions - the valid options hash with their validators:
 * validOptions = {
 *     onSuccess : 'function',
 *     timeout : function(value){...}
 * }
 */
validateOptions : function (validOptions, options){
    if (! WL.Utils.isDevelopmentMode() || typeof options === 'undefined'){
        return;
    }
    for (var att in options){
        if (! validOptions[att]){
            throw new Error("Invalid options attribute '" + att + "', valid attributes: " + Object.toJSON(validOptions));
        }
        try {
            WL.Validators.validateArgument(options[att], validOptions[att]);
        }
        catch (e){
            throw new Error("Invalid options attribute '" + att + "'");
        }
    }   
},

};

Heres a few examples of how I use it:

isUserAuthenticated : function(realm) {
WL.Validators.validateArgument(realm, 'string');



getLocation: function(options) {            
    WL.Validators.validateOptions{
        onSuccess: 'function', 
        onFailure: 'function'}, options);


makeRequest : function(url, options) {
    WL.Validators.validateArray(arguments, ['string', 
        WL.Validators.validateOptions.carry({
        onSuccess : 'function', 
        onFailure : 'function',
        timeout   : 'number'})]);


We have to discover and eliminate problems as soon as possible. If don't use TypeScript or Flow, you rather do it with a validation lib. It will help you avoiding spending hours hunting obscure errors caused by invalid types given as arguments. It looks like many take it serious - https://www.npmjs.com/package/aproba gets currently 9M(!) downloads per week.

To me it doesn't suite, explained here http://dsheiko.com/weblog/validating-arguments-in-javascript-like-a-boss I go with https://www.npmjs.com/package/bycontract that based on JSDoc expressions:

import { validate } from "bycontract";

const PdfOptionsType = {
  scale: "?number"
}

function pdf( path, w, h, options, callback ) {
  validate( arguments, [
    "string",
    "!number",
    "!number",
    PdfOptionsType,
    "function=" ] );
  //...
  return validate( returnValue, "Promise" );
}

pdf( "/tmp/test.pdf", 1, 1, { scale: 1 } ); // ok
pdf( "/tmp/test.pdf", "1", 1, { scale: 1 } ); // ByContractError: Argument #1: expected non-nullable but got string

On methods you can just reuse existing JSDoc comment block:

import { validateJsdoc, typedef } from "bycontract";

typedef("#PdfOptionsType", {
  scale: "number"
});

class Page {
  @validateJsdoc(`
    @param {string}          path
    @param {!number}         w
    @param {!number}         h
    @param {#PdfOptionsType} options
    @param {function=}       callback
    @returns {Promise}
  `)
  pdf( path, w, h, options, callback ) {
    return Promise.resolve();
  }
}

However I keep this validation on dev/test environments, but skip it on live:

import { config } from "bycontract";
if ( process.env.NODE_ENV === "production" ) {
  config({ enable: false });
}


It depends. How big this library would be? It is said that typed languages are better for big projects with complex API. Since JS is to some extent hybrid, you can choose.

About validation - I don't like defensive programming, the user of the function shall be obliged to pass valid arguments. And in JS size of code matters.


An intermediate way would be to return a reasonable default value (e.g. null) when required arguments are missing. In this way, the user's code will fail, not yours. And it will probably be easier for them to figure out what is the issue in their code rather than in yours.


When I've developed APIs like these in the past, I've validated anything that I feel is a "major" requirement - in your example, I'd verify the first two arguments.

As long as you specify sensible defaults, it should be pretty simple for your user to determine that "optional" arguments aren't specified correctly, since it won't make any change to the application, but everything will still work properly.

If the API is complex, I'd suggest following Aaron's advice - add comments that can be parsed by a compressor around your validation so the developers get the benefit of validation, but can extract the extra dead weight when pushing the code into production.

EDIT:

Here're some examples of what I like to do in the cases where validation is necessary. This particular case is pretty simple; I probably wouldn't bother with validation for it, since it really is trivial. Depending on your needs, sometimes attempting to force types would be better than validation, as demonstrated with the integer value.

Assume extend() is a function that merges objects, and the helper functions exist:

    var f = function(args){
      args = extend({
        foo: 1,
        bar: function(){},
        biz: 'hello'
      }, args || {});

      // ensure foo is an int.
      args.foo = parseInt(args.foo);

      //<validation>
      if(!isNumeric(args.foo) || args.foo > 10 || args.foo < 0){
        throw new Error('foo must be a number between 0 and 10');
      }

      if(!isFunction(args.bar)){
        throw new Error('bar must be a valid function');
      }

      if(!isString(args.biz) || args.biz.length == 0){
        throw new Error('biz must be a string, and cannot be empty');
      }
      //</validation>
    };

EDIT 2:

If you want to avoid common misspellings, you can either 1) accept and re-assign them or 2) validate the argument count. Option 1 is easy, option 2 could be done like this, although I'd definitely refactor it into its own method, something like Object.extendStrict() (example code works w/ prototype):

var args = {
  ar: ''
};
var base = {
  foo: 1,
  bar: function(){},
  biz: 'hello'
};
// save the original length
var length = Object.keys(base).length;
// extend
args = Object.extend(base, args || {});
// detect if there're any extras
if(Object.keys(args).length != length){
  throw new Error('Invalid argument specified. Please check the options.')
}


Don't validate. More code is more code the user has to download, so it's a very real cost on the user and production systems. Argument errors are easy enough to catch by the developer; don't burden the user with such.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜