ServiceNow - Handling rogue preconditions

jibarican

Javier Arroyo

Posted on September 15, 2021

ServiceNow - Handling rogue preconditions

This article talks about preemptive methods against overzealous preconditions. Preconditions are conditional (true/false) statements that must evaluate to true to continue code execution. For the purpose of this article I’ll concentrate on those preconditions placed at the top lines of code inside a function that are used to assert the validity of parameters.
Example A:

function sendGiftToOldPeople (age) {
    If (age >= 40) { return; } 
  // if age is not 40, code below this point won’t execute.
}
Enter fullscreen mode Exit fullscreen mode

A use case will be presented, solved the ServiceNow way then, cleaned up to prevent it from becoming a cryptic conglomerate for the next reader to decipher. Because having to interpret code increases the amount of time required to make sense of code, it makes sense to try to reduce code noise anywhere possible to prevent future-self from the rogue precondition jitters.

Just like anything else. Handling preconditions is part of a skillset used within a larger code structure scheme. A scheme aimed at building relatively uniform code everywhere; even if the skillset required to work at different parts of the codebase is different. To get there, well, that’s where the ideas here may help.
Before moving on, a little disclosure. We are at this point of cleaning preconditions because of a lack of oversight. A different view on programming could have prevented this.
On to the example:

A Basketball team has an open position. Tryouts are held, and data collected will be used to determine team invites. The coach gives us the following requirements.

• Must be at least 6’11 (83 inches) tall
• Must be at least 250 lbs (not to be pushed around)
• Must be older than 18
• Must have an outside shot
• Must have quick feet
• Must run at least a 6 second 40 dash
• Must reside within 15 miles of the facility

After each of those conditions is successfully met, an email invitation should be sent out.
Because this is ServiceNow, bang easy job:

function extendInvitationToBasketballPlayer (height, weight, age, hasOutSideShot, hasQuickFeet, fourtyDashTime, distanceFromFacility, address) {

    if (!hasOutSideShot || !hasQuickFeet) {
        return false;
    } //must be able to shoot from outside, and be quick
    if (height < 83 && weight < 250 && age >= 18) {
        return false;
    } //must be at least 83 inches tall, must be at least 250 lbs tall, and must be older then 18

    if (fourtyDashTime < 5000 || distanceFromFacility > 15) { //must be fast and live close enough to facility
        return false;
    }

//everyone else can be invited
    var mail = new Email(address.email);
    mail.body("we invite you to play with us. You are just the right fit. Lets win a championship");
    email.send();

}
Enter fullscreen mode Exit fullscreen mode

After a number of unsuccessful tryouts, it comes to light that key requirements have been missing. No invites have been sent out because of these two new conditions:
• Must have own car
• Must be related to the coach (there is something fishy going on)
Easy-peasy, ServiceNow makes this very easy. Let's revisit the function to append the latest preconditions.

function extendInvitationToBasketballPlayer  (height, weight, age, hasOutSideShot, hasQuickFeet, fourtyDashTime, distanceFromFacility, address, car, relationshipToCoach) {

    if (!hasOutSideShot || !hasQuickFeet) {
        return false;
    } //must be able to shoot from outside, and be quick
    if (height < 83 && weight < 250 && age >= 18) {
        return false;
    } //must be at least 83 inches tall, must be at least 250 lbs tall, and must be older then 18

    if (fourtyDashTime < 5000 || distanceFromFacility > 15) { //must be fast and live close enough to facility
        return false;
    }

    if (car !== null) { //needs a car to drive back and forth to the games
        return false;

    }

    if (['nephew,son,brother'].toLowerCase().indexOf(relationshipToCoach.toLowerCase()) == -1) {
        return false;
    }

//everyone else can be invited
    var mail = new Email(address.email);
    mail.body("we invite you to play with us. You are just the right fit. Lets win a championship");
    email.send();

}
Enter fullscreen mode Exit fullscreen mode

Trivial as they are, organic growth has cluttered things up. The most important parts of the function can’t be spot-lighted in that reading burden.

To keep this post relatively read-able, I won’t labor any further; instead, I will attempt to convert extendInvitationToBasketballPlayer into an executive summary without necessarily going into best practices. I won’t explain much either, the outcome should yield a closer, at-a-glance understanding.
Replacing extendInvitationToBasketballPlayer

  1. Start with reusable utility functions not to have to get away from having to type code best interpreted by a compiler.
 function not (x) {
        return !x;

    }

    function isNothing (x) {
        return typeof x === undefined || x === null;

    }

    function isLessOrEqualThan (x) {
        return function applyLessThan (y) {
            return x <= y;

        };
    }

    function isGreaterOrEqualThan (x) {
        return function applyGreaterThan (y) {
            return x >= y;

        };
    }
Enter fullscreen mode Exit fullscreen mode
  1. Convert all predicates into functions. This is purely to give meaning and gain elasticity. By meaning I mean seeing the business requirement written outright in code.
     function meetsMinimumShootingDistance (invitee) {
        return isGreaterOrEqualThan(invitee.restrictions.MINIMUM_SHOOTING_DISTANCE)(invitee.shootingDistance);

    }

    function hasQuickFeet (invitee) {
        return invitee.hasQuickFeet;

    }

    function meetsMinimumHeightRestriction (invitee) {
        return isGreaterOrEqualThan(invitee.restrictions.MINIMUM_HEIGHT)(invitee.height);

    }

    function meetsMinimumWeightRestriction (invitee) {
        return isGreaterOrEqualThan(invitee.restrictions.MINIMUM_WEIGHT)(invitee.weight);
    }

    function meetsMinimumAgeRestriction (invitee) {
        return isGreaterOrEqualThan(invitee.restrictions.MINIMUM_AGE)(invitee.age);

    }

    function meets40DashRestriction (invitee) {
        return isGreaterOrEqualThan(invitee.restrictions.MINIMUM_40_DASH)(invitee.fourtyDash);

    }

    function isCloseToFacility (invitee) {
        return isLessOrEqualThan(invitee.restrictions.MAXIMUM_DISTANCE_TO_FACILITY)(invitee.address.distanceFromFacility);

    }

    function hasOwnTransportation (invitee) {
        return not(isNothing(invitee.transporationMethod));

    }

    function hasRequiredRelationshipToCoach (invitee) {
        var relationships = invitee.restrictions.REQUIRED_COACH_RELATIONSHIPS;

        var userRelationships = relationships.join(',')
            .toLowerCase()
            .split(',');

        return userRelationships.indexOf(invitee.relationship.toLowerCase()) !== -1;

    }

    function hasAddress (invitee) {
        return not(isNothing(invitee.address));

    }
Enter fullscreen mode Exit fullscreen mode

That's a mouthful of functions. let's see what happens with them.

  1. Not to get sidetracked after the predicates, the most important part of the code needs to be accounted for: sending the invite
    function sendInvitationEmail (invitee) {
        var mail = new Email(invitee.email);
        mail.body("we invite you to play with us. You are just the right fit. Lets win a champtionship");
        email.send();

    }
Enter fullscreen mode Exit fullscreen mode

Now that each independent thought has been articulated, they can used to build a comprehensive idea.
Putting it all together 1:

Step A: Build a function that determines what it means to be extended an invitation. It clumps all the preconditions into a container of their own.

function canBeExtendedAnInvitationToJoinTeam (invitee) {
    return hasAddress(invitee) &&
        hasRequiredRelationshipToCoach(invitee) &&
        hasOwnTransportation(invitee) &&
        isCloseToFacility(invitee) &&
        meets40DashRestriction(invitee) &&
        meetsMinimumAgeRestriction(invitee) &&
        meetsMinimumWeightRestriction(invitee) &&
        meetsMinimumHeightRestriction(invitee) &&
        hasQuickFeet(invitee) &&
        meetsMinimumShootingDistance(invitee);

}
Enter fullscreen mode Exit fullscreen mode

Step B: A low level utility to separate the use of predicates from the behavior that needs to take place when all conditions are satisfied.

function Predicate (predicateFn, onSuccessFn, data) {
    return predicateFn(data) && onSuccessFn(data);

}
Enter fullscreen mode Exit fullscreen mode

Step C: Rebuild original function extendInvitationToBasketballPlayer using the utility function, the container for the predicates, and… sending the email. Please note that I took the liberty to create an object called “invite” from function arguments.

function extendInvitationToBasketBallPlayer (invitee) {
    return Predicate(canBeExtendedAnInvitationToTeam, sendInvitationEmail, invitee);

}
Enter fullscreen mode Exit fullscreen mode

The above is much cleaner but, because it’s a m-nary method I’m now stuck having to remember parameter order and their meaning.

Cleanup method 2
Step A. Another utility to handle the predicates. This one is going to be more involved because a small API will be exposed to see if less ambiguity can be attained.

function Predicate (data) {

    var predicates = [];

    return {
        check: check,
        onSuccess: onSuccess
    };

    function check (predicate) {
        predicates.push(predicate);

        return this;
    }

    function onSuccess (callback) {

        var totalTruthy = _countTrutyPredicates(_sum, predicates, data);
        return _wereAllPredicatesSuccessful(predicates, totalTruthy) ? callback(data) : null;

    }

    function _sum (data) {
        return function applySum (reducer, fn) {
            return reducer += fn(data);
        };
    }

    function _countTrutyPredicates (reducerCallback, preds, data) {
        return preds.reduce(reducerCallback(data), 0);

    }

    function _wereAllPredicatesSuccessful (preds, truthyCount) {
        return preds.length === truthyCount;

    }
}
Enter fullscreen mode Exit fullscreen mode

Step B: Rebuild original function extendInvitationToBasketballPlayer using The new Predicate utility. This time each predicate will be listed individually.

function extendInvitationToBasketballPlayer (invitee) {
    return Predicate(invitee)
        .check(hasAddress)
        .check(hasRequiredRelationshipToCoach)
        .check(hasOwnTransportation)
        .check(isCloseToFacility)
        .check(isCloseToFacility)
        .check(meets40DashRestriction)
        .check(meetsMinimumAgeRestriction)
        .check(meetsMinimumWeightRestriction)
        .check(hasQuickFeet)
        .check(meetsMinimumShootingDistance)
        .onSuccess(sendInvitationEmail);

}
Enter fullscreen mode Exit fullscreen mode

Another option is to re-use the existing container for the predicates as such:

function extendInvitationToBasketBallPlayer (invitee) {
    return Predicate(invitee)
        .check(canBeExtendedAnInvitationToTeam)
        .onSuccess(sendInvitationEmail);

}
Enter fullscreen mode Exit fullscreen mode

Hopefully this looks prettier than having all the preconditions lexically placed inside a fuction.
Cleanup Method 3:
The same idea but, this time passing the predicates as the initial parameter

function Predicate(predicates) {

    var successful = false;
    var data;
    var funs = Array.prototype.slice.call(arguments);

    return {
        check: test,
        onSuccess: onSuccess
    };

    function onSuccess(fn) {
        return successful && fn(data);

    }

    function test(value) {
        successful = _areAllPredicatesTrue(_filterByTruePredicates(_applyPredicateTestToData(value)));
        data = value;

        return this;
    }

    function _filterByTruePredicates(isTrueFn) {
        return funs.filter(isTrueFn);

    }

    function _areAllPredicatesTrue(result) {
        return result.length === funs.length;

    }

    function _applyPredicateTestToData(data) {
        return function test(fn) {
            return fn(data);

        };

    }

}
Enter fullscreen mode Exit fullscreen mode

With the new Predicate built, it’s time to give it a go

function extendInvitationToBasketballPlayer (invitee) {
    var predicate = Predicate(hasAddress, hasRequiredRelationshipToCoach, hasOwnTransportation, isCloseToFacility, meets40DashRestriction, meetsMinimumAgeRestriction, meetsMinimumWeightRestriction, meetsMinimumHeightRestriction, hasQuickFeet, meetsMinimumShootingDistance);

    return predicate
        .check(attendee)
        .onSuccess(sendInvitationEmail);

}
Enter fullscreen mode Exit fullscreen mode

or

function extendInvitationToBasketBallPlayer (invitee) {

    return Predicate(canBeExtendedAnInvitationToTeam)
        .check(attendee)
        .onSuccess(sendInvitationEmail);

}
Enter fullscreen mode Exit fullscreen mode

While I’ve left other strategies unexplored, I hope the ones here help the next time a bunch of predicates clutter your code.
Whichever way is chosen, keep in mind that functions tend to grow organically in the absence of conventions. It is a good practice to nip this stuff upon seeing it, rather than letting it linger. By being diligent, a codebase will slowly gain a common structure that is flexible and easy to navigate.

💖 💪 🙅 🚩
jibarican
Javier Arroyo

Posted on September 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related