Code Architecture – Introducing Piping to Help Enhance Code Readability
The goal of coding is to write code that is easily interpreted by those who read it, to find clearer and more articulate ways to convey a precise message. In this post, I’ll introduce piping and some of the effects that lead to enhanced readability...
Piping is the process of sequentially executing a list of functions so that the output of the previous function is used as the input of the next. This means an array of functions to the tune of
var leaversPipeline = [firstFunction, secondFunction, thirdFunction];
var outcome = pipe(leaversPipeline)(firstFunctionParameter);
Enhanced readability can come through:
- Concentrates fully on the high-level process
- function definition uniformity
- * singular arity
- always return something
- less comments
- clean call-sites
- concise function body (smaller code surface)
- decreased complexity
- easier to name functions, thereby gaining more meaning
- abstractions
Concentration on high level process means that the first layer of abstraction is very basic, removing all possibly sugar, leaving for the reader simpler entry points that demonstrate what is happening without having to fully dissect code.
Function definition uniformity
To map outputs to inputs smoothly between function calls, single arity functions work best. For example, say 5 functions in the executing sequence have varying arity: 1, zero, 4, 2, and 3. Matching outputs to inputs would become daunting; couple that to figuring out how to use .apply or .call to match arguments to parameters, their order, and such, piping would turn into an adventure. What if they didn’t have a return value, there goes the pipe...
While functions can return complex objects, the result set is always one. Returning multiple items, say an object, then an array, then a primitive, isn’t possible as individual parameters are. So instead of trying to mimic outputs to varying arity functions, the obvious choice is to limit a parameters to one. Mapping output to input then becomes trivial. No need to worry about argument order, number of, etc. The concern is then, what does each function expect as to be ordered accordingly in the pipeline.
Cleaner call-sites
One parameter is simply less words than multiple ones. So calling a function is functionName(argument).
Concise function body
Coding for one parameter, rather then 2 or more takes less code.
Decreased complexity
The decrease in complexity is caused by managing the single function parameter. Code what needs to be done, then return the outcome. The need to have parameters interact with one another is gone. Parameter count directly affects the complexity of a function.
Easier to name functions and more meaningful
Functions limited in functionality become easier to name, thereby gaining more meaning. countCarWheels, postMessage. It’s just easier to come up with a name to something that does one and only one thing.
Reduced Comments
Because single parameter functions tend to be much easier to name meaningfully, function become self documenting, reducing the use of comments to functionality that requires some sort of cognitive reasoning beyond what is apparent with in the script.
As example
// find today’s leavers
var gr = new GlideRecord(‘sys_user’);
gr.addQuery(‘departure_date’, TODAY);
gr.query();
var leavers = [];
while( gr.next() ) {
//build list of leavers
leavers.push( gr.getUniqueValue());
}
//broadcast leavers
gs.eventQueue(‘boardcase.todays.leavers’,….);
Above is a common practice: comment, code, comment, code more. Yet, what needs to happen isn't quickly apparent; comments must be read from top to finish. That simple functionality still required 3 comments. But, if the comments can be replaced with a meaningful function call broadcastTodaysLeavers the entry point then abstracts everything away informing of what needs to happen*:*
broadcastTodaysLeavers(TODAY);
The one line documented itself, forgoing the need for three lines of comments embedded in multiple lines of code. Further break the code into logical groupings then, we got a nice sentence to read.
Piping and the practices it sort of entices one to follow lead to simpler and cleaner practices than those found in code with irregular function arity, or line by line coding how things should happen...
The goal is to allow the reader to more quickly interpret without reasoning what is happening at the top, the middle, the end, even more difficult, having to consider what might be happening outside the function. What happens to this param if that one is missing, etc. Hopefully rendering familiarity biases futile.
Example to Pipe or not to Pipe: Use Case
- Audit deactivate itil user by,
- reassign open tickets to reporting manager,
- revoking group membership only from groups managed by reporting manager
- notify the manager of the adjusted group membership, displaying in the email a list of current members and those removed.
No piping, and common to ServiceNow: How to programming style:
Some script include with some functionality
var ItilUserAuditorNoPipe = {
deactivationProcess: function deactivationProcess(deactivateUser) {
//get user manager
var manager = sysUser.getValue('manager');
//get assignee open tickets
var openTickets = new GlideRecord(task);
openTickets.addActiveQuery();
openTickets.addQuery('assigned_to', deactivateUser);
openTickets.query();
//reassign to manager
openTickets.setValue('assigned_to', manager);
openTickets.updateMultiple();
//remove from manager managed groups.
var managerMembersManaged = new GlideRecord('sys_user_grmember');
addQuery('group.manager', manager);
addQuery('user', deactivateUser);
managerMembersManaged.query();
managerMembersManaged.hasNext() && managerMembersManaged.deleteMultiple();
//notify of altered groups
gs.eventQueue('send.group.altered', manager, manager.getUniqueValue(), manager.getValue('email'));
}
}
With some Action script or another code to act on the queued event
var NotificationEmailScriptSendGroupAltered = function NotificationEmailScriptSendGroupAltered() {
//build object to notify of groups altered
var groupMembership = new GlideRecord('sys_user_grmember');
groupMembership.addQuery('group.manager', event.param2);
groupMembership.query();
var groupMembers = {};
var groupName;
var value;
//create object with group name as key and members as value array
while (groupMembership.next()) {
groupName = groupMembership.group.getDisplayValue();
value = groupMembership.user.getDisplayValue();
groupMembers[groupName] ?
groupMembers[groupName].push(value) :
(groupMembers[groupName] = [value]);
}
//print to template
var bodyContent = '<table>';
for (var group in groupMembers) {
var members;
bodyContent != '<tr><td>' + groupName + '</td><td><ul>';
members = groupMembers.reduce( function addMembers(content, member){
content.push('<li>'+member+'</li>');
return content;
}, []).join('');
bodyContent+= members + '</ul></tr>'
}
bodyContent+='<table>';
template.print( bodyContent);
}
The above two scripts are the normal SNow practice: Create a script include, place a function that carries out various functionality, then another somewhere else to respond. Placed the How to solve the use case inside a function, and line after line each step is carried out. This, while reasonable, leads to all sorts of maintenance overhead, be it testing, finding bugs, re-usability, readability. The code, while somewhat clean, requires too much interpretation. It behaves as if a library rather than a business process definition. There is just too much sugar just to know what is going on. I've done worse than that.
Piping, on the other hand, is like psuedocode. Present a high-level description of the use case as not to have to fully describe how to achieve the result. I like to think of it as this: I'm going to Knowledge by plane, rather than, describing the details of what happens... I'm waking up, brushing my teeth, calling uber, driving to air port, dismounting the vehicle, checking in, getting through security...
The details can instead be abstracted away to fulfill the use case to the tune of turning ItilUserAuditorNoPipe into:
var ItilUserAuditorPipe = {
deactivationProcess: function deactivationProcess(sysUser){
var pipeline = [
getAssigeeOpenTickets,
reassignUserTicketsToManager,
removeUserFromManagerManagedGroups,
notifyOfAlteredGroups
];
return pipe(pipeline)(sysUser);
}
};
For the sake of brevity, I won't refactor the above two scripts into a more logical grouping and summarize it.
Each one of the functions in the pipeline represent a step to take. The function name tells what process is happening, and the call site of the pipe() kicks it off.
The function array pipeline came from each step commented in the first script that was then turned it into functions. This way what is happening requires very little interpretation: deactivationProcess gets assignee opened tickets, reassigns them to the manager, removes user from groups, notify of altered groups. What happens is abstracted away.
https://www.servicenow.com/community/architect-articles/code-architecture-introducing-piping-to-help-enhance-code/ta-p/2330556