Framework Design

06 Jun 2014

What is an API exactly? It stands for Application Programming Interface. What it actually does is determine the way that software systems interact with each other. It's vitally important to mind your API when writing software that will be consumed by other developers because your API will shape the way that your tool is used. Ideally you want to lead developers down the golden path without completely removing all of the flexibility that they might need to tackle their problems.

Start with a Problem

The first thing that any good API designer should do is understand the problem they are trying to solve once-and-for all. It's probably a complex domain with some weird quirks, so make sure to study up. Know as much as you can about the domain before you begin working because lack of fundamental knowledge of a problem can have devastating effects on an API. The fewer surprises about the problem domain, the better.

I'm going to use a client-side validation framework as my example because it's code I've written about a million times so far. I'll use jQuery to sort out all of the nasty edge cases of manipulating the DOM. What I've got is a form with a field for inputting a name, email, and phone number so I can contact someone with important information they want. The general plan goes something like this:

- Name field
    - cannot be empty
- Email field
    - cannot be empty
    - must be a valid email
- Phone number
    - must be a valid phone number

Now that I have a general idea of the problem I will be solving, I need to consider other requirements like how we are going to be displaying errors to the user. We want our forms to be extra-super-awesome and validate whenever somebody changes the value in one of them and display all of their error messages in a summary element.

Model the Problem the Way it Actually Works

At this point you will be tempted to think about things like how we will call into it, how we will mark items as needing validation, but I would like to put that all aside for a second and think about validation. What are the important parts of a validation framework? Well, we've got a list of controls to validate.

var Validation = function () {
    this.controls = [];
};

What exactly is a control? We'll have to think about that for a bit. Basically, a control is a function that can be called to validate whether the input is valid or not. We'll model it like so:

var Control = function () {
};
Control.prototype.validate = function () {};

So, we should be able to validate our whole list of controls in one go, which means we need to define what a control's validate method should be doing. I'm going to use the following contract for the return value of the validate method:

true/undefined - no error
false          - error
string         - error message
array          - list of error messages

I've decided that I'm mostly going to be throwing around strings since I'll need to give the user some sort of feedback about why the form isn't submitting correctly. I believe I'm now ready to start working on the larger validate method in the Validation object. This method will batch up all of the calls to the individual controls and collect the error messages that each one returns.

var Validation = function () {
    this.controls = [];
};
Validation.prototype.validate = function() {
    var errors = [];
    var valid = true;

    for (var i = 0; i < this.controls.length; i++) {
        var result = this.controls[i].validate();

        if (result === false) {
            valid = false;
        } else if (typeof result === 'string') {
            errors.push(result);
        } else if (typeof result.length === 'number') {
            Array.prototype.push.apply(errors, result);
        }
    }

    return errors.length ? errors : valid;
};

Fill in Just Enough of the Details

The big glaring hole in my framework is the lack of controls. I need a few different controls if I'm going to be validating any user input. I'll start with the easiest one: required fields.

var Required = function (input, message) {
    this.input   = input;
    this.message = message;
};
Required.prototype = Object.create(Control);
Required.prototype.validate = function () {
    return !!this.input.val() || this.message || false;
};

This will work great for the first two elements, I'll make sure to add them.

var form = new Validation();
form.controls.push(
        new Required($('[name=name]'), 'Please enter your name'));
form.controls.push(
        new Required($('[name=email]'), 'Please enter your email'));

Now I need to do the email format. I'm going to be lazy and say that I'll accept an email if it has an @ in the middle. Using regex, I'll call it /^.+@.+$/. Since I'm at it, and I know I'm going to need to validate a phone number too, I may as well make my next control a pattern-based control.

var Pattern = function (input, pattern, message) {
    this.input   = input;
    this.pattern = pattern;
    this.message = message;
};
Pattern.prototype = Object.create(Control);
Pattern.prototype.validate = function () {
    if (!input.val()) return;
    return this.pattern.test(input.val()) || this.message || false;
};

Now I'll hook this into the controls on the page.

var form = new Validation();
form.controls.push(
        new Required($('[name=name]'), 'Please enter your name'));
form.controls.push(
        new Required($('[name=email]'), 'Please enter your email'));
form.controls.push(
        new Pattern($('[name=email]'),
                    /^[^@]+@[^@]+$/,
                    'Please enter a valid email address'));
form.controls.push(
        new Pattern($('[name=phone]'),
                    /^\D*(\d\D*){10,11}$/,
                    'Please enter a valid phone number'));

So, now things are humming. We've got all of the basic infrastructure in place and we're ready to start building up more advanced uses. The first thing that springs to mind is a way to mark an input with a class based off of whether or not the most recent validation has succeeded or failed.

var Highlight = function (control, class, input) {
    this.control = control;
    this.class   = class || 'error';
    this.input   = input || control.input;
};
Highlight.prototype = Object.create(Control);
Highlight.prototype.validate = function () {
    var result = this.control.validate();
    this.input.toggleClass(this.class,
                           result !== true && result !== undefined);
    return result;
};

Now here we're faced with a bit of a sticky wicket. This highlight class is very simple as it stands, but it only works on one validation. The problem becomes apparent when you look at the email field:

var form = new Validation();
form.controls.push(
        new Highlight(
            new Required($('[name=name]'), 'Please enter your name')));
form.controls.push(
        new Highlight(
            new Required($('[name=email]'), 'Please enter your email')));
form.controls.push(
        new Highlight(
            new Pattern($('[name=email]'),
                    /^[^@]+@[^@]+$/,
                    'Please enter a valid email address')));
form.controls.push(
        new Highlight(
            new Pattern($('[name=phone]'),
                        /^\D*(\d\D*){10,11}$/,
                        'Please enter a valid phone number')));

Our issue is that we have a bit of a race going on here between the two controls that handle the email validation. It looks as if a failed Required will be superseded by the passed Pattern control. The trick is to realize that we don't have to dismay and abandon the architecture. It just so happens we can reuse the Validation class:

var form = new Validation();
form.controls.push(
        new Highlight(
            new Required($('[name=name]'), 'Please enter your name')));
form.controls.push(
        new Highlight(
            new Validation(
                new Required($('[name=email]'), 'Please enter your email'),
                new Pattern($('[name=email]'),
                    /^[^@]+@[^@]+$/,
                    'Please enter a valid email address'))));
form.controls.push(
        new Highlight(
            new Pattern($('[name=phone]'),
                        /^\D*(\d\D*){10,11}$/,
                        'Please enter a valid phone number');

This makes the Highlight work as expected in the face of multiple validations on a single control. I think it's fair to say that we've proven our design, but it's a bit unwieldy. Most modern tools do a great deal more on the ease of use end, which is our last project.

Make the API Cute

I'm purposefully using some pretty strange language here because I always have to remind myself what's the most important thing with an API: maintenance cost. It's really tempting to start making an API by designing calls into the API first, and while that approach might have some merit, I find that it's best to save the ease of use details until the core concepts have been solidified. Now that we have a reasonably successful approach started, it's time to optimize the common case.

I'm going to assume that requiring and matching fields to patterns are going to be really ubiquitous operations, so I'll make it easier to use.

Validation.prototype.add = function (control) {
    this.controls.push(control);
    return this;
};
var require = function (selector, message) {
    return new Require($(selector), message);
};
var pattern = function (selector, pattern, message) {
    return new Pattern($(selector), pattern, message);
};
Validation.prototype.highlight = function (control, class, input) {
    return add(new Highlight(control, class, input));
};

This changes the code to look like this:

var form = new Validation();
form.highlight(require('[name=name]',
                       'Please enter your name'));

form.highlight(new Validation(require('[name=email]',
                                      'Please enter your email'),
                              pattern('[name=email]',
                                      /^[^@]+@[^@]+$/,
                                      'Please enter a valid email address')));

form.highlight(pattern('[name=phone]',
                       /^\D*(\d\D*){10,11}$/,
                       'Please enter a valid phone number'));