Design Patterns for IF Statements
Ever question how an IF statement managed to spawn out of control? Ever witness its maturation from one, to three, to ten, to 100 to 200 conditions?
Everyone must have gone through it. The:
why should it be changed?
It's simple to update, just add it to the end. Easy pickings and move on to the next item.
Consultant: "I've been coding for years, since before you were born. Look, right here, add it right at the end. As more come, keep on adding to the end."
Bewildered Client: "what happens if the choices change?"
Consultant: "The use case we were given read that the choices will never change but, if the do, just go right here, make the update and you are done. No more than 5 minutes"
Bewildered Client: "Can I get back my 300K?"
Consultant: "do you require a service agreement? our professional services is the best in the industry."
Conversations as the one above are ubiquitous in our field. A platinum partner and consulting firm, staffed with ServiceNow accredited Sr. Developers and Architects implementing solutions that are accompanied by the likes of a single script 1700 lines long, using 299 IF conditions as flow control.
Quality is being ignored somewhere along the line of programming. Should we, as Implementation or Code Architects, not be able to identify when code won’t scale? When to use one design over another? When to push back against developers for trivial programming techniques?
The programming world has guidelines that have surfaced through trial and error, at times plain ol’e ingenuity. It is those very practices that should be familiar... from Sr. Developers, Code Evangelists, Code Reviewers, all the way to Implementation Architects. Unmanageable code should not be an issue at that stage of one's career, at the Stage of product maturity of ServiceNow. It is simply impossible to solution a quality implementation if basic design and coding architectural knowledge is a weak point.
This article will examine IF statements, attempting to introduce established ideas for manageable and readable conditional controls. The basic guidelines that work well are:
1. An IF statement should roughly have no more than three conditions.
2. Refactor IF statements with more than three conditions into a switch statement.
3. Switch statements with more than five conditions should become Objects.
Guidelines in action:
The target will be the excerpt of inline code below. It will be refactored into a scalable solution, as if maturing over time.
if (carBrand === 'Honda') {
gs.info('It has pretty round wheels');
} else if (carBrand === 'Toyota') {
gs.info('It has big round wheels');
} else if (carBrand === 'Ford') {
gs.info('It has nice wheels and a spare');
} else {
gs.info("your car wheels aren't nice");
}
Step 1:
Wrap inline code with a properly named function to begging addressing scalability, cleanliness and readability, while massaging it some to return a value rather than directly printing it.
function saySomethingNiceAboutACarBrandIF(carBrand) {
var message = null;
if (carBrand === 'Honda') {
message = 'It has pretty round wheels';
} else if (carBrand === 'Toyota') {
message = 'It has big round wheels';
} else if (carBrand === 'Ford') {
message = 'It has nice wheels and a spare';
} else {
message = "Oops. your car wheels aren't nice";
}
return message;
}
Step 2:
With the addition of a new car brand (GMC), the IF will be turned into a SWITCH to improve readability.
function saySomethingNiceAboutACarBrandSwitch(carBrand) {
var message = null;
switch (carBrand) {
case 'Honda':
message = 'It has pretty round wheels';
break;
case 'Toyota':
message = 'It has big round wheels';
break;
case 'Ford':
message = 'It has nice wheels and a spare';
break;
case 'GMG':
message = 'None have the wheels I have';
break;
default:
message = "Oops. your car wheels aren't nice";
break;
}
return message;
}
Upon inspection it becomes aparent that neither the IF or SWITCH staments will lead to scalable approcaches. Redability will also deteriorate as more car brands are added; more importantly, as more complex logic is added to each condtion. Because the goal is to create maintainable code, then numerous lines of code under each condition can not be acceptable.
In addition, are the IF or SWITCH flexible enough to account for executing a specific brand without having to traverse the condition tree? or can the conditions be 'overwritten' or extended? If the execution tree can be traversed up to as many times as there are conditions, this implies the IF and SWITCH grow slower.
By refactoring IF/SWITCH statements into Objects code can be decoupled into single units, allowing for a plethora of positive results, including a performance boost.
Quality code leads to reduce maintenance and cost.
3rd and Final Step:
3.A:
Extract SWITCH cases into a Data Object/Data Dictionary
var carBrandNiceThings = {
honda: 'It has pretty round wheels',
toyota: 'It has big round wheels',
ford: 'It has nice wheels and a spare',
gmc: 'None have the wheels I have',
generic: "Oops. your car wheels aren't nice"
}
3.B:
Refactor function to use the Data Object
function saySomethingNiceAboutACarBrandObject(carBrand) {
carBrand = carBrand.toLowerCase();
//user ternary to get correct brand
var message = ((carBrandNiceThings[carBrand]) ?
carBrandNiceThings[carBrand] :
carBrandNiceThings['generic']);
return message;
}
Thoughts on final step.
By introducing a data object, the artifact becomes fully scalable, maintainable and easy on the eyes. It also separates messages from code, a quick win...
As business needs evolve, each property may grow independently without cluttering the object.
The maintenance advantage to Data Dictionaries is the ability to expose the item as configurable, where it can be updated as necessary. Be it through sys_properties, sys_ui_message or table.
The final code is a robust solution to IF/SWITCH statements.
Extending functionality based on a new requirement
Use Case:
Add tracking specific to each available car brand.
Step 1:
Refactor Data Object into a configuration item and create a factory method to build the messages
var carBrandNiceThingsData = (function loadCarBrandNiceThingsData() {
var niceThings = gs.getMessage('carBrand.niceThings.data'); //messages have become a sys_ui_message that contains a JSON string
niceThings = JSON.parse(niceThings);
return niceThings;
})();
Step 2:
Introduce tracker object BrandTracker
BrandTracker = {
trackNiceThingsAbout( carBrand ) {
gs.log( carBrand + 'is being tracked');
},
trackNiceThingsAboutFord: function( carBrand ) {
BrandTracker.trackNiceThingsAbout(carBrand);
//do ford specific stuff
},
trackNiceThingsAboutGeneric: function(carBrand) {
BrandTracker.trackNiceThingsAbout(carBrand);
//do generic stuff
},
trackNiceThingsAboutGMC: function(carBrand) {
BrandTracker.trackNiceThingsAbout(carBrand);
//do gmc stuff
},
trackNiceThingsAboutHonda: function(carBrand) {
BrandTracker.trackNiceThingsAbout(carBrand);
//do honda stuff
},
trackNiceThingsAboutToyota: function(carBrand) {
BrandTracker.trackNiceThingsAbout(carBrand);
//do toyota stuff
}
}
Step 3:
Refactor Data Dictionary into a "functional" Mediator, leaving untouched the return value expected by saySomethingNiceAboutACarBrandObject function
var carBrandNiceThings = {
honda: function (carBrand) {
var message = carBrandNiceThingsData.honda;
BrandTracker.trackNiceThingsAboutHonda(carBrand);
return message;
},
toyota: function (carBrand) {
var message = carBrandNiceThingsData.toyota;
BrandTracker.trackNiceThingsAboutToyota(carBrand);
return message;
},
ford: function (carBrand) {
var message = carBrandNiceThingsData.ford;
BrandTracker.trackNiceThingsAboutFord(carBrand);
return message;
},
gmc: function (carBrand) {
var message = carBrandNiceThingsData.gmc;
BrandTracker.trackNiceThingsAboutGMC(carBrand);
return message;
},
generic: function (carBrand) {
var message = carBrandNiceThingsData.generic;
BrandTracker.trackNiceThingsAboutGeneric(carBrand);
return message;
}
}
Step 4:
Refactor saySomethingNiceAboutACarBrandObject function to account for new functionality. Because there hasn't been a change to the return value of the function, changes to the code are limited to how it calls the data dictionary. (Executing them as parameterized functions)
function saySomethingNiceAboutACarBrandObject(carBrand) {
carBrand = carBrand.toLowerCase();
//execute carBrand mediator functions by carBrand
var message = ((carBrandNiceThings[carBrand]) ?
carBrandNiceThings[carBrand](carBrand) :
carBrandNiceThings['generic']('generic'));
return message;
}
https://www.servicenow.com/community/architect-articles/design-patterns-for-if-statements/ta-p/2330611
