Skip to Main Content

ServiceNow allows code to be stored in many different places. The instance we work with most often has more than 300 dictionary definitions for scripts. Code for Business Rules, ACLS, Workflow Scripts, Relationship Definitions can be entered directly in the record. Even more single-line fields allow Javascript to store conditions, restrict related lists or filter records.

Implementing a moderately complex feature, may require entering code in a few dozen locations. This leads to a high amount of code fragmentation not usually encountered in other projects.

Managing this code fragmentation (and the resulting code duplication) can make the difference between a successful project implementation and a hard to reason about, unmaintainable, cludgy mess.

 

Our Approach

Over time we found techniques that allow us to manage the code fragmentation & duplication resulting from ServiceNow's myriad ways of storing code.

 

Storing the Code in One Location

We very early on decided to store all our code in Script Includes. This means UI Actions, Business Rules, ACLs, etc. only call out to Script Includes and do not contain any code besides that.

While storing our code exclusively in Script Includes made our code less fragmented it exposed us to another challenge. We had a harder time being sure if changing or removing some piece of code, would have unwanted consequences, because it was called from somewhere we did not expect.

The default Script Include pattern exacerbated this problem because it has no concept of private members, static methods and there is no indirection between function names and public interfaces, making it very hard to confidently change code.

var DefaultScriptInclude = Class.create();
DefaultScriptInclude.prototype = {
    initialize: function() {
        this._privateVar = this._loadPrivateVar();
    },

    _loadprivateVar: function() {
        return gs.getProperty('expertsnow.private.variable');
    },

    interfaceFunction: function(record) {
        var gr = new GlideRecord('incident');
        gr.addEncodedQuery('assignment_group=' + record.getValue('sys_id') + '^state=' + this.privateVar);
        gr.query();
        if(gr.next()) {
            gs.addErrorMessage('Unable to foo assignment group while group has incidents');
        }
    },

    type: 'DefaultScriptInclude'
};
 
Usage:
var dsi = new DefaultScriptInclude();
dsi._loadprivateVar(); // still able to access this function
dsi._privateVar; // still able to access this variable
dsi.interfaceFunction(record); // works

 

A Better Script Include pattern

Searching for a way to solve the problems we had with storing all our code in Script Includes lead us to find out about alternative Script Include patterns. Read more about alternative Script Include patterns.

Instead of the default Script Include pattern we most often use the revealing module pattern. With this Script Include pattern we are able to create private variables, private methods, decouple the private from the public interface and it allows us to treat Script Includes as collections of static methods if we want.

var RevealingModulePattern = (function() {
    var privateVar = loadprivateVar();

    function loadprivateVar() {
        return gs.getProperty('expertsnow.private.variable');
    }

    function interfaceFunction(record) {
        var gr = new GlideRecord('incident');
        gr.addEncodedQuery('assignment_group=' + record.getValue('sys_id') + '^state=' + privateVar);
        gr.query();
        if(gr.next()) {
            gs.addErrorMessage('Unable to foo assignment group while group has incidents');
        }
    }

    return {
        'foo': interfaceFunction
    }
})();
 
Usage:
RevealingModulePattern.loadprivateVar(); // fails
RevealingModulePattern.privateVar; // fails
RevealingModulePattern.interfaceFunction(record); // fails
RevealingModulePattern.foo(record); // works

 

Using the Revealing Module Pattern

The revealing module pattern is very flexible allowing us to use it in very different ways. We found the following approaches very useful.

 
Behaviour Classes

Behaviour classes are the bread and butter of our implementation strategy. They implement our Business Rules, ACLs and Scripted Web Services. They can be easily called from anywhere, requiring no setup and encapsulate all handling of business logic.

var IncidentEvents = (function() {

    function triggerEvents(current, previous) {
        if(current.getValue('assignment_group') != previous.getValue('assignment_group')) {
            // raise event
        }

        ...
    }

    return {
        'triggerEvents': triggerEvents
    }
})();

var IncidentRestrictions = (function() {

    function onUpdate(record) {
        ...
    }
    
    function onInsert(record) {
        ...
    }

    function onDelete(record) {
        ...
    }

    return {
        'onUpdate': onUpdate,
        'onInsert': onInsert,
        'onDelete': onDelete
    };
})();
 
Usage:
IncidentEvents.triggerEvents();
IncidentRestrictions.onUpdate();
IncidentRestrictions.onInsert();
IncidentRestrictions.onDelete();
 
Compound Interfaces

Using the revealing module pattern we are able to create compound interfaces exposing the functionality of many different Script Includes in a way that simplifies reasoning about the code by creating a common jumping off point for related functionality.

var IncidentInterface = (function() {
    return {
        'triggerEvents': IncidentEvents.triggerEvents,
        'Restrictions': IncidentRestrictions 
    }
})();
 
Record Classes

During development we often find that we create small helper functions that reason about a specific record type. We group these functions in what we call a record class. Record classes encapsulate the GildeRecord, boolean predicates concerning the record, constants abstracting away magic values for choice fields and functionality related to logging.

var IncidentRecord = function(record) {
    var status = {
        'resolved': 5,
        'inProgress': 4,
        'closed': 6
    };

    function log(message) {
        gs.log(message, record.getValue('sys_id'));
    }

    function canBeClosed() {
        return record.getValue('state') === status.resolved;
    }

    function canBeResolved() {
        return record.getValue('state') === status.inProgress && record.getValue('resolve_notes') != null;
    }

    return {
        'record': record,
        'canBeClosed': canBeClosed,
        'canBeResolved': canBeResolved,
        'log': log,
        'status': status
    }
};

 

Usage:

var incident = new IncidentRecord(record);
if(incident.canBeClosed() || incident.canBeResolved()) {
    incident.log('This is a log message ' + incident.record.getValue('state'));
}