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.
}
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();
}
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();
}
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
- 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;
};
}
- 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));
}
That's a mouthful of functions. let's see what happens with them.
- 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();
}
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);
}
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);
}
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);
}
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;
}
}
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);
}
Another option is to re-use the existing container for the predicates as such:
function extendInvitationToBasketBallPlayer (invitee) {
return Predicate(invitee)
.check(canBeExtendedAnInvitationToTeam)
.onSuccess(sendInvitationEmail);
}
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);
};
}
}
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);
}
or
function extendInvitationToBasketBallPlayer (invitee) {
return Predicate(canBeExtendedAnInvitationToTeam)
.check(attendee)
.onSuccess(sendInvitationEmail);
}
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.
Top comments (2)
Overall, if you use ServiceNow, there are excellent solutions you can use for statistics and the analysis of data. I can suggest you read more about integrating the Power BI Connector for ServiceNow alphaservesp.com/products/servicen.... I think that is a great one to work with because with the connector you can visually investigate the data you receive from ServiceNow.****
I'm using ServiceNow, but I don't understand how to get the data quickly so I can analyze it further.