Scripting Best Practices
Introduction
This article contains information on best practices with regards to scripting in FNZ Studio. These recommendations result in better solution uniformity, readability, and maintainability when used within a project’s development cycle.
The goals of this article are the following:
- New developers joining an ongoing project can get up to speed quicker.
- Common mistakes are avoided, increasing reliability and stability of the solution.
- Developers can go to any place in the code and have a better understanding of what’s going on, improving readability, reusability, and maintainability.
Also see the article on Script Language An Introduction for more details.
Naming
Use names that are able to explain themselves, from which readers of the code are able to ascertain what a Data Class, function or variable does.
Data Classes
Data Classes typically represent a business domain element, and as such, should be represented by nouns.
Examples:
Customer
Client
Address
Avoid mixing ambiguous words such as CustomerInfo and CustomerDetail. In this example, it would confuse the developer as to what the difference between the two is.
Data Class Functions
A function call — both static and not — means we are asking for something to be done, and as such, should be represented by a verb. For Data Class functions, start with a lowercase letter, and then apply camel case capitalization.
Examples:
generatePdfDocuments()
submitForApproval()
sendMessage()
Business Object Functions
Functions which are implemented as Business Objects start with an uppercase letter, and also use camel case capitalization.
Examples:
SendNotificationEmail()
ComputeDeadline()
Variables
The general rule for variables is to make them descriptive and self-explanatory.
Bad example:
Integer $days
Good example:
Integer $daysSinceCreation
Constants
Any unchanging or static value that is consistent across a Solution should be contained in a Data Class. Consider this script snippet:
If $amount > 5000.00 Then
$requiresApproval := true;
End
When reading the code, it is easy to understand that approval is required if the amount is greater than 5000. It is unclear, however, what 5000 is, and if another check is required in other sections of the code. This will lead to maintenance issues, e.g. when the value requires changing: you need to search for all occurrences of '5000'. It is possible that you will update some and not others, resulting in unexpected code behavior / defects. For the purposes of code clarity / readability and maintainability, the example above should be modified to have a Data Class with a static function, such as:
// Any deposit amount greater than this requires approval
// @return The threshold amount to require approval
StaticFunction getTransferApprovalThreshold() : Double Begin
Return 5000.00;
End
The script can then be modified to:
If $amount > SolutionConstants.getTransferApprovalThreshold() Then
$requiresApproval := true;
End
The code is clearer, and should a change request arise in the future to change the amount, only a single place in code requires changing and reflects across the entire solution.
Functions
Sizing
Generally, functions should be small. Consider this code:
Function submitForApproval(Form $form) : Nothing Begin
Person $approver := null;
String $approver := $form.getApprover();
If EMPTY($approver) Then
$approver := 'Default Approver';
End
Integer $approverLevel := $form.getApproverLevel();
If EMPTY($approverLevel) Then
$approverLevel := 1;
End
String $approverId := GetFromDb($approver, $approverLevel);
ForEach FormElement $formElement In $form.getFormElements() Do
$formElement.setApproverId($approverId);
If $formElement.getAmount() >
SolutionConstants.getDepositApprovalThreshold() Then
$formElement.setDeadline(DATEADD(NOW(), 1, 'D'));
Else
$formElement.setDeadline(DATEADD(NOW(), 5, 'D'));
End
End
ForEach Person $subscriber In $this.subscribers Do
If ($subscriber.hasEmail()) Then
SendEmail($form);
ElseIf ($subscriber.hasSms()) Then
SendSmsNotification($form);
Else
InsertToSubscriptionDb($form);
End
End
End
This function does three main things:
- Retrieves the approver ID from a database
- Loops through all the elements in the form, and sets the appropriate deadline for each
- Sends a notification to any subscribers of this form
The above function is quite long, and could be broken down into the following:
Function submitForApproval(Form $form) : Nothing Begin
String $approverId := _getApproverId($form);
_setFormDeadlines($form, $approverId);
_sendNotifications($form);
End
Note: As FNZ Studio has no concept of private or public functions, private functions should, by way of convention, be prefixed with an underscore (_). This serves a guide that it is an internal function and should not be called from outside the Data Class.
The methods:
_getApproverId($form)
_setFormDeadlines($form, $approverId)
_sendNotifications($form)
are all private functions of the Data Class, breaking down the long function into multiple small functions that are easier to comprehend.
The resulting code is more concise, and the function is intuitive enough for the developer to determine what the function does at a glance.
As a general rule, functions should only do one thing. If your function does multiple things, consider breaking it down into multiple private sub-functions.
Data Class Functions (Instance Functions)
Let’s assume that we have a person with a collection (Indexed elements) of Address, stored in a property called "addresses". The address Data Class has an attribute "isPrimary" serving as a flag whether an address is a primary address or not. To find out whether a person object has a primary address, we can use:
Boolean $hasPrimaryAddress := false;
ForEach Address $address In $person.addresses Do
If ($address.isPrimary) Then
$hasPrimaryAddress := true;
Break;
End
End
This works, of course, until you realize that you have to check whether a person already has a primary address in 50 other places. A better solution would be to create a function inside the Person class, like this:
Function hasPrimaryAddress() : Boolean Begin
If (Collection:Size($this.addresses) == 0) Then
Return false;
End
ForEach Address $address In $this.addresses Do
If $address.isPrimary Then
Return true;
End
End
Return false;
End
and use it as opposed to looping, effectively containing this piece of logic in one area (the Person class)
Boolean $hasPrimaryAddress := $person.hasPrimaryAddress()
Argument Sizing
Limit the number of arguments passed into a function unless backwards compatibility is absolutely required. A large number of arguments tends to become confusing. If a function requires lots of arguments, then it might make more sense to pass in an object containing the arguments than using separate arguments.
Function processTransaction(String $accountNo, String $accountStatus, Double $currentBalance, Double $amount)
can be rewritten as:
Function processTransaction(Account $account, Double $amount)
where the 'Account' object contains the attributes for accountNo, accountStatus and currentBalance.
Collection Return Types
If you have a function that returns a collection, do not return null. Return an empty collection instead. This means that code calling this function will not cause exceptions when it does not do a null check.
Example:
Function getAssignedTasks() : Indexed Task Begin
If ($this.tasks == null) Then
Return NewIndexed(Task);
Else
Return $this.tasks;
End
End
Comments
Yes, comment your code. We’re sure you understand what you’re doing, no doubt about that, but we’re also sure that if you look at the same code 6 months from now, you’ll have a hard time understanding it yourself.
The following are basic guidelines for good code comments with their samples:
- Informing — This function returns an instance of the client that is being evaluated
- Explaining — The logic is done this way to prevent a memory leak
- Warning — We create a new instance to avoid thread clashes
- TODO — TODO this is a tactical solution, to be replaced when the new external system becomes available
Built-in Functions
FNZ Studio has various built-in functions that can be used by developers to write better solutions. These built-in functions are:
- Supported by the product team
- Optimized for efficiency (e.g. they are null-safe)
When a built-in function is available, it is preferable to use it than creating your own.
Collection:Filter
The Collection:Filter()
function is more efficient and readable than looping. Consider a looping example:
// assume $persons is an initialized, non-empty collection
// and the Person class has an attribute "age"
Indexed Person $oldies := NewIndexed(Person);
ForEach Person $person In $persons Do
If $person.age > 60 Then
Collection:AddElement($oldies, $person);
End
End
The same functionality above can be achieved using the Collection:Filter
function, such as:
// assume $persons is an initialized, non-empty collection
// and the Person class has an attribute "age"
Indexed Person $oldies := Collection:Filter($persons, $person.age > 60, $person, false);
Collection:Map
The Collection:Map()
function is more efficient and readable than looping. Consider the example:
// assume $persons is an initialized, non-empty collection
// and the Person class has an attribute "lastName"
Indexed Person $persons := NewIndexed(Person);
Indexed String $lastNames := NewIndexed(String);
ForEach Person $person In $persons Do
If ! Collection:ContainsElement($lastNames, $person.lastName) Then
Collection:AddElement($lastNames, $person.lastName);
End
End
The same functionality above can be achieved using the Collection:Map
function, such as:
// assume $persons is an initialized, non-empty collection
// and the Person class has an attribute "lastName"
Indexed String $unfilteredLastNames := Collection:Map($persons, $person.lastName, $person, 'String');
Indexed String $lastNames := UNIQUE($unfilteredLastNames);
Collection:Size
The Collection:Size()
function can be used to read the size of a nullable collection. E.g., instead of:
If $persons != null and Collection:Size($persons) > 0 Then
// do something
End
you can directly use:
If Collection:Size($persons) > 0 Then
// do something
End
EMPTY
The EMPTY()
function is useful to check if String is null or has length 0. E.g., instead of:
If $name == null || LENGTH($name) == 0 Then
// do something
End
you can use:
If EMPTY($name) Then
// do something
End
Alternative (and Better) Functions
Avoid the use of CONTEXT().getXXXService().get…
functions when an alternative exists. The methods from CONTEXT()
might be removed or modified in future FNZ Studio releases and if used in a solution, can lead to code not working when FNZ Studio is upgraded.
The functions below are the preferred and recommended way as opposed to their CONTEXT()…
function counterparts.
Workflow Instance Attributes
When working with workflow instance attributes, avoid:
CONTEXT().getWorkflowInstanceService().getWorkflowInstance($id).getAttribute($someAttribute)
Use the following function instead:
ProcessInstanceGetAttribute($id, $someAttribute)
Similarly, use ProcessInstanceSetAttribute()
when modifying an attribute, or ProcessInstanceRemoveAttribute()
to remove an attribute.
Recommended Functions
In some cases, there are multiple functions for the same purpose, or alternative operators or keywords are available. In the latter case, we recommend using functions over using keywords or operators, however there are exceptions to the rule.
See the following sections for recommendations.
Object Instantiation
For creating new objects, use:
NewObject(String)
Avoid using NEW()
as it is deprecated.
Collection Instantiation
For creating collections, there are two possible variants for each collection type:
Indexed Collection
NewIndexed(String)
Indexed Any $collection := []:Any
Avoid using NEWARRAY()
as it will be deprecated in the future.
Named Collection
NewNamed(String)
Named Any $collection := {}:Any
Assignment
Use :=
instead of ASSIGN()
as it will be deprecated.
Boolean Operators
The following recommendations apply for Boolean operators:
-
Use the
and
operator, notAND()
or&&
-
Use the
or
operator, notOR()
or||
-
Use the
not
operator, notNOT()
or!
Note: For more complicated code with nested expressions, `AND()` and `OR(`) can be used as this can lead to more readable code.
Comparison Operators
The following recommendations apply for comparison operators:
- Use
<
and notLESS()
- Use
<=
and notLESSEQUAL()
- Use
>
and notGREATER()
- Use
>=
and notGREATEREQUAL()
- Use
==
and notEQUAL()
Arithmetic Operators
The following recommendations apply for arithmetic operators:
- Use
+
and notADD()
- Use
-
and notSUB()
- Use
*
and notMUL()
- Use
/
and notDIV()
Functions with Different Casing
The following recommendations apply:
- Use
print()
and notPRINT()
Functions with Identical / Similar Functionalities
The following recommendations apply:
- Use
ABSOLUTEPATH()
and notDATAPATH()
, as it will be deprecated. - Use
RM()
and notDELETEFILE()
, as it will be deprecated. - Use
DATEPARSE()
and notPARSEDATE()
, as it will be deprecated.
Functions with Alternative Operators
The following recommendations apply:
- Use
ISA()
and not theisa
operator - Use
MOD()
and not themod
operator - Use
DIVV()
and not thediv
operator
Miscellaneous (but still important)
Multiple Lines
Have a look at these lines of code:
Record $r;
Return Collection:Map(Collection:Filter(RECORDS($catalogId), (CONTAINS(TOSTRING($r.getValue('processes')), $filter.processId) or TOSTRING($r.getValue('processes')) == '*'), $r), $r.getValue('id'), $r, 'String');
No doubt, the code is efficient — both in terms of performance and characters typed — but the readability is sacrificed for efficiency. This line of code can be broken down into multiple lines, such as:
Record $r;
Indexed Record $catalogRecords := RECORDS('Currencies');
Function filterCondition(Record $r) : Boolean Begin
Return CONTAINS(TOSTRING($r.getValue('id')), 'CHF') or TOSTRING($r.getValue('processes')) == '*';
End
Indexed Any $filteredResults := Collection:Filter($catalogRecords, filterCondition($r), $r);
Return Collection:Map($filteredResults, $r.getValue('id'), $r, 'String');
While the refactored code results in more lines, it is clearer than what was previously defined, greatly increasing code readability.
Self-descriptive Code
While comments are important to aid the developer in understanding what the code does, nothing beats written code that explains what it does just by reading it. Consider the example:
// Check if the deadline has elapsed based on the creation date for anything greater than the approval amount
If $elapsedInDays >= 5 && $approvalAmount > 5000 Then
_escalateIssue();
End
The condition being evaluated by the "if" statement does not fully explain what it is for, unless the comment is read. While this works, a better approach would be:
If _deadlineBreachedForAmount($creationDate, 5000.00) Then
_escalateIssue();
End
The check to determine whether the elapsed number of days is greater than 5 and the amount is greater than 5000 is contained in the private _deadlineBreachedForAmount
function.