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:

Copy
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:

Copy
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:

Copy
SendNotificationEmail()

ComputeDeadline()

Variables

The general rule for variables is to make them descriptive and self-explanatory.

Bad example:

Copy
Integer $days

Good example:

Copy
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:

Copy
 

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:

Copy

 
// 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:

Copy

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:

Copy


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:

Copy
 

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:

Copy

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:

Copy

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)

Copy
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.

Copy
Function processTransaction(String $accountNo, String $accountStatus, Double $currentBalance, Double $amount)

can be rewritten as:

Copy
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:

Copy
 
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:

Copy
 
// 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:

Copy

// 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:

Copy


// 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:

Copy
 
// 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:

Copy

If $persons != null and Collection:Size($persons) > 0 Then

    // do something

End

you can directly use:

Copy

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:

Copy

If $name == null || LENGTH($name) == 0 Then

// do something

End

you can use:

Copy

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:

Copy
CONTEXT().getWorkflowInstanceService().getWorkflowInstance($id).getAttribute($someAttribute)

Use the following function instead:

Copy
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:

Copy
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

Copy
NewIndexed(String)

Indexed Any $collection := []:Any 

Avoid using NEWARRAY() as it will be deprecated in the future.

Named Collection

Copy
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, not AND() or &&

  • Use the or operator, not OR() or ||

  • Use the not operator, not NOT() 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 not LESS()
  • Use <= and not LESSEQUAL()
  • Use > and not GREATER()
  • Use >= and not GREATEREQUAL()
  • Use == and not EQUAL()

Arithmetic Operators

The following recommendations apply for arithmetic operators:

  • Use + and not ADD()
  • Use - and not SUB()
  • Use * and not MUL()
  • Use / and not DIV()

Functions with Different Casing

The following recommendations apply:

  • Use print() and not PRINT()

Functions with Identical / Similar Functionalities

The following recommendations apply:

  • Use ABSOLUTEPATH() and not DATAPATH(), as it will be deprecated.
  • Use RM() and not DELETEFILE(), as it will be deprecated.
  • Use DATEPARSE() and not PARSEDATE(), as it will be deprecated.

Functions with Alternative Operators

The following recommendations apply:

  • Use ISA() and not the isa operator
  • Use MOD() and not the mod operator
  • Use DIVV() and not the div operator

Miscellaneous (but still important)

Multiple Lines

Have a look at these lines of code:

Copy

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:

Copy

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:

Copy

// 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:

Copy

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.