The Zurich release has arrived! Interested in new features and functionalities? Click here for more

Maik Skoddow
Tera Patron
Tera Patron

elephants-279505_1280.jpg

 

In one of my previous articles, DevBasics: Harden Your Signatures, I explored how to build robust function signatures and emulate basic overloading patterns using the ES5 JavaScript engine in ServiceNow. Now, with ServiceNow's support for the ES12 JavaScript engine, developers have access to modern language features that make it easier to implement flexible APIs and advanced scripting patterns. 

 

However, true method overloading as seen in a compiled language such as Java or C# is still not natively supported in JavaScript. Redefining a function with the same name simply overwrites the previous definition. Instead, JavaScript developers must use a workaround I want to introduce with that article.

 

 

 

What is Method Overloading?

 

In compiled object-oriented languages like Java or C#, method overloading allows you to define multiple methods with the same name but different parameter lists. The compiler decides which method to invoke based on the number and types of arguments. This enables, for example, a single add method to handle both numbers and strings without changing its name.

 

//one add() method accepting only number values
objMathHelper.add(1, 4, 5, 6)

//different add() method with the same name accepting only string values
objMathHelper.add("123", "11", "99");

 

 

 

The Use Case

 

In my article ServiceNow Deployment Pipeline - Part 5: Object-oriented Programming - Is it worth? I introduced the class GlideRecordImpl which I have made available for free on GitHub. This class can be used a foundation for your inheriting data access objects (DAOs) and it serves as a comprehensive facade around ServiceNow's out-of-the-box GlideRecord and GlideRecordSecure classes, providing enhanced functionality, improved error handling and additional safety mechanisms.

 

It is quite common in object-oriented development to instantiate objects using different combinations of input parameters in order to cover various use cases. For my GlideRecordImpl class, there are five such options:

 

// (1) with an existing GlideRecord instance (e.g. in a Business Rule)
var objIncident = new GlideRecordImpl(current);

// (2.1) only with a table name (creates a new record that can be inserted)
var objIncident = new GlideRecordImpl('incident');
 
// (2.2) Just like (2.1) but with a GlideRecordSecure object
var objIncident = new GlideRecordImpl('incident', true);
 
// (3.1) with table name and Sys ID (retrieves an existing record)
var objIncident = new GlideRecordImpl('incident', 'b3af7471c31a6a90108c78edd40131aa');
 
// (3.2) Just like (3.1) but with a GlideRecordSecure object
var objIncident = new GlideRecordImpl('incident', 'b3af7471c31a6a90108c78edd40131aa', true);

 

 

In Java, it is possible to define multiple constructors within a single class, as long as they have different signatures, that is, differing in the number or types of parameters. This concept is known as constructor overloading and allows for flexible object initialization.

 

In JavaScript, however, this is not possible, as you can only define a single constructor. So, how did I solve this?

 

 

 

The Solution

 

Let's start with a minimal class including an empty constructor:

 

class GlideRecordImpl {
  constructor () {

  }
}

 

 

You might be irritated because the familiar blueprint for an empty Script Include is:

 

var GlideRecordImpl = Class.create();

GlideRecordImpl.prototype = {
    initialize: function() {
    },

    type: 'GlideRecordImpl'
};

 

 

At the end, both versions represent the same: They define a class within a Script Include that can be instantiated in other scripts via the new operator. However, the first one is utilizing the new language features provided by the ES12 script engine that is available globally since the Xanadu release.

 

Examining the above examples of the various instantiation approaches for the GlideRecordImpl class, we can identify the following five constructor signatures:

  • object
  • string
  • string,boolean
  • string,string
  • string,string,boolean

 

As previously mentioned, in JavaScript, only one constructor is permitted per class. Faced with this limitation, a common but brittle approach is to create a single constructor and fill it with a large if(){}else{} or switch statements that inspects the number and type of arguments, like:

 

constructor(...args) {
  if (args.length === 1 && typeof args[0] === 'object') {
    // handle GlideRecord case...
  }
  else if (args.length === 1 && typeof args[0] === 'string') {
    // handle table name only case...
  } 
  else if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'boolean') {
    // handle table name and secure flag case...
  } 
  // ... and so on.
}

 

 

While functional, this method is a so-called antipattern, as it becomes difficult to read, painful to maintain, and violates the Open/Closed Principle: adding a new way to construct the object requires modifying this ever-growing block of conditional logic. Fortunately, there is a much cleaner, more scalable way, by utilizing a mapping between a signature and the target implementation. 

 

With the ES5 JavaScript engine, key-value mapping was only possible with ordinary objects, which are subject to various restrictions due to their nature. However, the ES6 JavaScript Engine introduced the Map() object, which is more robust and functionally mature.

 

Aspect Plain Object ({}) Map (new Map())
Key Types Only strings and symbols as keys. Any value (objects, functions, numbers, etc.) as keys.
Key Handling Keys are always converted to strings (except symbols). Keys are kept by reference; objects/functions as keys are distinct.
Order No guaranteed key order (in practice, insertion order for own properties since ES2015, but not for inherited). Insertion order is always preserved.
Iterability Not directly iterable; use Object.keys(), Object.values(), Object.entries(). Directly iterable with for...of, .entries(), .keys(), .values().
Size Property No built-in size property; use Object.keys(obj).length. Has a .size property for easy size retrieval.
Prototype Has a prototype by default, which can lead to key collisions (unless using Object.create(null)). No default keys; only what you explicitly set.
JSON Support Directly serializable with JSON.stringify()/JSON.parse(). Not natively serializable to JSON.
Performance Not optimized for frequent additions/removals. Optimized for frequent additions/removals.

 

 

At this stage, the various behavioral variants need to be implemented. There are several ways to achieve this, but I opted for dedicated private class methods for the following reasons:

  1. Separation of Concerns: encapsulating each variant in its own method ensures a clear distribution of responsibilities and promotes better modularity.

  2. Unified Implementation for Multiple Signatures: different public-facing method signatures can delegate to the same underlying logic without duplication.

  3. Improved Documentation and Readability: method-level documentation is more explicit and accessible than documenting logic embedded inline.

  4. Easier Testing and Debugging: isolated methods are simpler to test and debug individually, especially when dealing with edge cases or subtle differences.

  5. Enhanced Maintainability: changes to a specific variant affect only a confined and well-defined part of the codebase.

  6. Consistent Error Handling: encapsulation allows each variant to implement or delegate error handling in a controlled and predictable manner.

 

_newWithGlideRecord(
  objRecord = null,
) {

}

_newWithTable(
  strTableName = 'x',
  isSecure     = false,
) {

}

_newWithTableAndSysID(
  strTableName = 'x',
  strSysID     = 'x',
  isSecure     = false,
) {

}

 

 

Now that everything is in place, the constructor can define the mapping between the five method signatures and their target implementations:

 

constructor(
  ...args
) {
  const OVERLOAD_REGISTRY = 
    new Map([
      ['object',                (...args) => this._newWithGlideRecord(...args)],
      ['string',                (...args) => this._newWithTable(...args)],
      ['string,boolean',        (...args) => this._newWithTable(...args)],
      ['string,string',         (...args) => this._newWithTableAndSysID(...args)],
      ['string,string,boolean', (...args) => this._newWithTableAndSysID(...args)],
    ]);

  // ...
}

 

 

In the next step, the signature must be determined from the parameters passed to the constructor call, and the corresponding implementation executed. Therefore, the final constructor is implemented as follows:

 

constructor(
  ...args
) {
  const OVERLOAD_REGISTRY = 
    new Map([
      ['object',                (...args) => this._newWithGlideRecord(...args)],
      ['string',                (...args) => this._newWithTable(...args)],
      ['string,boolean',        (...args) => this._newWithTable(...args)],
      ['string,string',         (...args) => this._newWithTableAndSysID(...args)],
      ['string,string,boolean', (...args) => this._newWithTableAndSysID(...args)],
    ]);

  //build the constructor signature
  let _strSignature = 
    args.map(arg => 
      Array.isArray(arg) ? 'array' : typeof arg
    ).join(',');

  //is a constructor method registered for the signature?
  if (OVERLOAD_REGISTRY.has(_strSignature)) {
    //invoke the registered constructor method
    OVERLOAD_REGISTRY.get(_strSignature)(...args);
  }
  else { 
    throw new TypeError(
      `[${this.constructor.name}.constructor] ` + 
      `Unsupported signature "${_strSignature}"`
    );
  }
}

 

 

 

Wrapping up

 

My workaround for the implementation of constructor overloading in JavaScript represents a significant advancement in creating more intuitive and flexible APIs within the ServiceNow. By leveraging the ES12 JavaScript engine's modern features, developers can now create sophisticated class constructors that rival the flexibility found in compiled object-oriented languages.

 

I would be delighted to hear from fellow developers who have discovered alternative approaches to this challenge. Please feel free to share your insights and methodologies in the comments below.