Introducing Design Patterns to Coding Practices
This post is meant to introduce industry tested patterns to coding and architecture practices. It is an effort to separate concerns, decrease maintenance and dev cycles, as well as increase scale-ability. A stepping point to what is possible with industry established patterns that have yet to be introduced to the ServiceNow community at large.
DISCLAIMER: THIS CODE WAS CREATED USING VISUAL STUDIO CODE IDE ON MY ONE HOUR TRAIN RIDE HOME FROM WORK. IT HAS NOT BEEN TESTED, NOR REVISED FOR BUGS OF ANY KIND. IT IS TO BE USED AS GUIDE... HOWEVER, THE ARCHITECTURE FOUND HERE IS VERY SIMILAR TO MY CURRENT CODING PRACTICES.
Templeting I use it to mean a practice that dictates full separation of concerns into their own items, grouping them if you will. Code meant for object Foo, should not be mixed with code for object bar, neither should technologies like html, css, and JavaScript as commonly done with ServiceNow Notification scripts. Templeting ideology introduces highly scale-able and maintainable code. Because SNow is a suite of products detailing business processes, just about every piece of code should be easily accessible to each other. Corralled objects with their own look/feel/functionality don't necessarily have a place in integrated business processes.
Code should scale upwards, sideways, shrink and be refactored with minimal effort and impact. As an example, the amount of time required for a face-lift or to port a desktop angular app to mobile specific app should be trivial. It should only require a new UI and connecting the UI to existing Business Layers. If that is not possible, then code isn't scale-able.
In this article the concept of Database Access Layer (DAL) and Business Access Layer (BAL) will be skimmed over as an example of division of concerns/templating. There will be little explanations other than comments within the code itself.
Addressed will be four artifacts:
- A Database Layer as a Script Include BaseDAL
- * ideally, this file wouldn't be called directly from a BAL, rather called from other DALs. In this example, it will be called directly from BAL.
- A Business Logic Layer as a Script Include IncidentBAL
- * Business Uses Cases should all go into some form of BAL. They should not be spread out in buttons, ui policies, business rules, Action Scripts, AngularJS widgets, Notification Scripts, etc.
- A Messages Data Dictionary BaseDALMessage
- * this can be a loaded from sys_messages as a whole, or in parts, or be created within a script include without the use of messages/properties
- A DAL Options data dictionary IncidenOption
- * Location that identifies available functionality to DALs. Outside code should not define their own behavior, rather use what's identified within the configuration of an options object.
1. BaseDAL (Database Access Layer)
This file is a wrapper to GlideRecord, eliminating the need to tightly couple GlideRecords (a database functionality) with Business Logic. This will replace writing GlideRecords directly in Business Logic, and instead be delegated to DALs.
var BaseDAL = {
developerMessage: BaseDALMessages.REQUIRED_TABLE_AND_PAYLOAD,
/**
* @description
* Crude and final basic check
*
* @param {Object} options
* var options = {
* table: 'cmdb_ci',
* encodedQuery: 'sys_class_name=cmdb_ci_computer',
* behaviors: {
* setLimit: 10000,
* orderBy: 'name'
* }
* };
*
* @param {Object} payload
* key/value pairs to be "truthied"
*/
successfullyChecked: function (options, payload) {
if (!options.hasOwnProperty('table') || !payload) {
return false;
}
return true;
},
/**
* @description
* Initialize GlideRecord with behaviors
*/
initGlideRecord: function (options) {
var record = new GlideRecord(options.table);
/**
* sets GlideRecord behaviors such as
* setWorkflow, autSysFields, etc
*/
if (options.behaviors) {
var behaviors = options.behaviors
var behavior = null;
for (behavior in behaviors) {
if (typeof record[behavior] === 'function') {
if (behaviors[behavior] != null) {
record[behavior](behaviors[behavior]);
} else {
record[behavior]();
}
}
}
}
return record;
},
/**
* @description
* Inserts any record into DataBase
*
* @param {Object} options
* key/vaue pair description for GlideRecord
* var options = {
* table: 'cmdb_ci',
* encodedQuery: 'sys_class_name=cmdb_ci_computer',
* behaviors: {
* setLimit: 10000,
* orderBy: 'name'
* }
* };
*
* @param {Object} payload
* key/value pairs to set in record
* var payload = {
* name: 'My Name',
* sys_class_name: 'cmdb_ci_computer'
* }
*
* @return {sys_id} sys_id of record inserted
*/
insert: function (options, payload) {
if (!BaseDAL.successfullyChecked(options, payload)) {
return BaseDAL.developerMessage;
}
var record = BaseDAL.initGlideRecord(options, behavior);
BaseDAL.handlePayload(record, payload);
var id = record.insert();
return id;
},
/**
* @description
* Updates a single record in the database
*
* @param {Object} options
* key/vaue pair description for GlideRecord
* var options = {
* table: 'cmdb_ci',
* encodedQuery: 'sys_class_name=cmdb_ci_computer',
* behaviors: {
* setLimit: 10000,
* orderBy: 'name'
* }
* };
*
* @param {Object} payload
* key/value pairs to set in record
* var payload = {
* name: 'My Name',
* sys_class_name: 'cmdb_ci_computer'
* }
*
* @return {sys_id} sys_id of record inserted
*/
update: function (options, payload) {
if (!BaseDAL.successfullyChecked(options, payload)) {
return BaseDAL.developerMessage;
}
var record = BaseDAL.query(options);
BaseDAL.handlePayload(record, payload);
var id = record.update();
return id;
},
/**
* @description
* Crude and final basic check
*
* @param {Object} options
* key/vaue pair description for GlideRecord
* var options = {
* table: 'cmdb_ci',
* encodedQuery: 'sys_class_name=cmdb_ci_computer',
* behaviors: {
* setLimit: 10000,
* orderBy: 'name'
* }
* };
*
* @param {Object} payload
* key/value pairs to set in record
* var payload = {
* name: 'My Name',
* sys_class_name: 'cmdb_ci_computer'
* }
*
* @return {sys_id} sys_id of record inserted
*/
updateMultiple: function (options, payload) {
if (!BaseDAL.successfullyChecked(options, payload)) {
return BaseDAL.developerMessage;
}
var record = BaseDAL.query(options);
BaseDAL.handlePayloadValues(records, payload);
record.updateMultiple();
},
/**
* @description
* queries database
*
* @param {Object} options
* key/value pair to be used to build query
* options Object format
* {
* table: 'incident',
* encodedQuery: 'active=true'
* }
* @returns {GlideRecord}
* query results
*
* @example
* var options = {
* table: 'change_request',
* sys_id: UID
* }
*
* @example
* var options = {
* table: 'cmdb_ci',
* encodedQuery: 'sys_class_name=cmdb_ci_computer',
* behaviors: {
* setLimit: 10000,
* orderBy: 'name'
* }
* };
*
* var cmdb_ci_computers = BaseDAL.query(options, behaviors);
*/
query: function (options) {
if (!options.table) {
return BaseDAL.developerMessage;
}
var record = BaseDAL.initGlideRecord(options);
if (options.sys_id) {
record.get(options.sys_id);
return record;
}
if (options.encodedQuery) {
record.addEncodedQuery(options.encodedQuery);
}
record.query();
return null;
},
/**
* @description
* sets record value using strategy record[fieldname] = value
*
* @param {GlideRecord} record
* Any GlideRecord type
*
* @param {Object} payload
* key/value pairs to set in record
*
*/
handlePayload: function (record, payload) {
for (var key in payload) {
if (record.hasOwnProperty(key)) {
record[key] = payload[key]; //set record values
}
}
},
/**
* @description
* sets record value using strategy record.setValue(fieldName, fieldValue)
*
* @param {GlideRecord} record
* Any GlideRecord type with field values to set
*
* @param {Object} payload
* key/value pairs to set in record
*
*/
handlePayloadValues: function (record, payload) {
for (var key in payload) {
record.setValue(key, payload[key]);
}
}
};
2. IncidentBAL (Business Logic)
This file will perform Business Logic, calling BaseDAL to perform CRUD operations. It uses IncidentOption for available operations. There will never be rouge code. Any code wishing to communicate with an Incident, must come through some form of IncidentBAL.
/**
* @description
* Business Layer for incident related behavior
*/
var IncidentBAL = {
/**
* @description
* Container for functionality available to insert
*/
insert: {
/**
* @description
* Insert basic incident
*
* @description {object} payload
* key/value pair fields to set in an incident
*/
basic: function (payload) {
var incidentPayload = handlePayload(payload, IncidentOption.insert.basic.payload);
var options = IncidentOptions.insert.basic.options;
var id = BaseDAL.insert(options, incidentPayload);
return id;
}
},
/**
* @description
* Container for update related functionality
*/
update: {
/**
* @description
* Updates assingment group
*
* @param {sys_id} from_sys_id
* sys_user_group.sys_id to change assignment group
*
* @param {sys_id} to_sys_id
* sys_user_group.sys_id to change assignment group to
*
*/
assignmentGroupFromTo: function (from_sys_id, to_sys_id) {
var options = IncidentOption.update.assignmentGroupFromTo.options;
var payload = IncidentOption.update.assignmentGroupFromTo.payload;
//add assignment group to encodedQuery
options.encodedQuery.replace('{0}', from);
payload = IncidentBAL.handlePayload({assignment_group: from_sys_id}, payload )
BaseDAL.updateMultiple(options, payload);
}
},
/**
* @description
* adds only those values found in both objects to a targetObject
*
* @param {Object} sourcePayload
* key/value pair to be used for source values
*
* @param {Object} targetPayload
* key/value pair to be set by from sourcePayload
*
* @returns {Object}
* targetPayload populated with values from
*
*/
handlePayload: function( sourcePayload, targetPayload ) {
for (var key in sourcePayload) {
//only add those values indicated in targetPayload
if (targetPayload.hasOwnProperty(key)) {
targetPayload[key] = sourcePayload[key]
}
}
return targtePayload;
}
}
3. IncidentOption (Data Dictionary with operations, and structure expected by BaseDAL)
The goal of this file is to become a configurable item (sys_message or sys_properties) that, instead of creating more GlideRecords, adding them to IncidentOption would trigger a corresponding function in a DAL. In an advance implementation of this approach, DALs dynamic are dynamically created with info from ItemsOption-like objects, allowing the programmer to concentrate solely on Business Logic.
/**
* @description
* Basic Options Data Dictionary for Incident
*/
var IncidentOption = {
table: 'incident',
GLIDE_MAX_LIMIT: 10000, //@TODO: grab from system default settings
GLIDE_USER_MAX_LIMIT: 50, //@TODO: grab from system default settings
/**
* @description
* container for all queries options available from Incident
*/
query: {
/**
* @description
* Option to retrieve "all" incidents
*/
all: {
table: IncidentOption.table,
behaviors: {
setLimit: IncidentOption.GLIDE_MAX_LIMIT
}
},
/**
* @description
* Option to retrieve all active incidents
*/
active: {
table: IncidentOption.table,
encodedQuery: 'active=true',
behaviors: {
setLimit: IncidentOption.GLIDE_MAX_LIMIT
}
},
/**
* @description
* Option available to retrieve all closed incidents
*/
closed: {
table: IncidentOption.table,
encodedQuery: 'active=false',
behaviors: {
setLimit: IncidentOption.GLIDE_MAX_LIMIT
}
},
/**
* @description
* Option available to retrieve opened tickets requested by a user
*/
openedRequestedBy: {
table: IncidentOption.table,
encodedQuery: 'active=true^requested_by={0}',
behaviors: {
setLimit: IncidentOption.GLIDE_USER_MAX_LIMIT
}
}
},
/**
* @description
* container for all insert options available for Incident
*/
insert: {
/**
* @description
* Options and payload available to insert a basic incident
*/
basic: {
payload: {
requested_by: null,
short_description: null,
description: null,
state: IncidentState.NEW,
},
options: {
table: IncidentOption.table
},
},
/**
* @description
* Options and payload available to insert a Major Incient incident
*/
mi: {
payload: {
requested_by: null,
short_description: null,
description: null,
major_incident: null,
state: null,
assignment_group: null
},
options: {
table: IncidentOption.table
},
}
},
/**
* @description
* container for all update options available from Incident
*/
update: {
/**
* @description
* Reuse options from insert
*/
basic: IncidentOption.insert.basic,
/**
* @description
* options and payload available to update a major incident
*/
mi: {
payload: {
assigned_to: null,
state: null
},
options: {
}
},
/**
* @description
* Options and payload available when updating a group to another
*
*/
assignmentGroupFromTo: {
payload: {
assigned_to: null
},
options: {
table: IncidentOption.table,
encodedQuery: 'assignment_group={0}',
behaviors: {
setWorkflow: false
}
}
}
}
}
4. BaseDALMessage (Message container for DAL Messages)
There should seldom be, if at all, direct calls to gs.getMessage or gs.getProperty outside of Data Dictionaries. The basic theory is that there should be extremely few places in a code base that contain their own messages (be it a hard-coded string, or "ENUM" or SN getProperty/Message. By placing all messages in a strategic location, it will always be trivial to update any amount of messages, especially so if the messages are a configuration file editable by an Admin. Every other piece of code then reads from the properties file.
var BaseDALMessage = {
REQUIRED_TABLE_AND_PAYLOAD: gs.getMessage('Table name and payload are mandatory')
}
https://www.servicenow.com/community/architect-articles/introducing-design-patterns-to-coding-practices/ta-p/2330543