Data Class Best Practices

Designing good Data Classes is not easy; it requires a lot of thought and planning. It's tempting to just add every property you need to a single Class. But if you do that, you end up with an unmanageable string of confusing properties. This article contains a few rules you should keep in mind when designing Data Classes.

Use the Proper Type

This is mainly important for Boolean types. Do not use Strings for values which can contain true and false, or yes and no. If you use Boolean types, you always know the possible values and don't need to remember which string stands for yes and which for no. Furthermore, using Boolean values, your scripts become easier to read. If you're using strings, your scripts will look like this:

If $person.isNatural == 'yes' Then

However, if you're using Booleans, your script will look like this:

If $person.isNatural Then

As you can see, this looks much nicer and is easily readable.

Remember: Always use Boolean types if the stored values can be yes or no, true or false, or something similar.

Normalize Your Data Classes

In the field of relational database design, there's a term called "normalization." This is basically just a fancy term meaning the designer tries to avoid duplication of information. The same goal exists when you design your Data Classes.

What does that mean? It means every piece of information stored in a Data Class should be stored in exactly one place.

Why is this important? It's important because it avoids inconsistent data. If the same piece of information is stored in two places, it's possible that somewhere in your code, only one of these places is updated, while the other is left in its old state: Now you don't know which piece of information is correct, and which is outdated.

Here's an example. Say you're writing an account opening solution, and your current $opening account has a list of addresses. Say each of these addresses has a type, such as home or work.

If you do use properties containing things such as types, always use plain text values. Think of the developers who have to read your code; it is easier to figure out what this means: $address.type := 'home' than what this means: $address.type := 2

The Data Class used for your $opening object may look like this:

6.1.bmp

So you would add a new Address object to the addresses list, and at the same index, set its type in the addresstypes list:

Copy
$opening.addresses[1] := NewObject(Address);
$opening.addresstypes[1] := 'home'

Seems pretty straightforward: The address at index 1 is a home address. Problem solved.

But there's an issue with this solution. Can you spot which piece of information is stored twice?

Yes, it's the number of addresses. Say you want to print out all addresses in this $opening object. How many addresses are there? The obvious answer would be:

Copy
SIZE($opening.addresses)

But this would be just as valid:

Copy
SIZE($opening.addresstypes)

After all, $opening.addresses and $opening.addresstypes need to contain the same number of entries, since each address has a type! But wait, what if somebody does not realize that $opening.addresstypes contains the types for $opening.addresses? What if somebody wanted to remove an address from this $opening, but did not know that he would also have to remove it from $opening.addresstypes?

Now you've got a new problem: Not only can you not be sure how many addresses there actually should be in this opening, but you also don't know whether the addresstypes map correctly to the addresses. If somebody removed the address at index 5 but kept the addresstypes intact, the address formerly at index 6 is now at index 5, thus its addresstype is that of the address formerly at index 5. Deleting addresses breaks their relationship with address types.

6.2.bmp

A much nicer way would be to add the type to your Address Data Class (the figure below shows Data Class without duplicate information):

6.3.bmp

But perhaps this is not desirable. Say your Address class is used in different parts of your application, but you only need the type here. You want to avoid cluttering your classes with properties that are only used in few places. What you can do here is to use inheritance. Instead of changing Address, create a new Data Class TypedAddress, make it inherit from Address, and add the new property in your new class (the figure below shows a Data Class without duplicated information using inheritance):

6.4.bmp

TypedAddress will receive all properties from Address, so you can just add the new property 'type'. That way, your 'type' property is associated with the actual address object, but you will never have to bother with the type where you don't actually need it. Even better, you can use your TypedAddress object wherever you can use Address objects; the parts of your application which use Address objects will simply ignore the type attribute in your TypedAddress instance.

For an additional solution to this issue, see Use Inheritance Properly.

Remember: Never repeat information in your Data Classes.

Keep Property Count Down

If you need additional properties, the easiest solution is to "just add them to some existing Data Class" without spending too much thought on it.

Don't do that. You will end up with Data Classes that have dozens and dozens of properties, making it impossible to find the properties you're looking for.

There are two ways you can avoid this fate:

  1. Using a new variable
  2. Using inheritance

Use a New Variable

You don't need to put every property into a Data Class. For example, if you only need a property during one specific Workflow, create a local variable inside that Workflow to store the property.

Use Inheritance

You've already seen this technique in the previous chapter. If you need a new property in an existing Data Class, but you only need it in a small part of your application, use inheritance to create a new Data Class which takes all properties from an existing Data Class, but adds some of its own.

Group Properties

Instead of simply adding all properties to a Data Class, group them using additional Data Classes. Let's go back to our account opening. Let's say you need to collect some data specific to the US Patriot Act, such as a Patriot Act address, legal residence, a name for a next of kin, and a relationship to the next of kin.

You could simply add these four properties to the class used for your $opening object (the figure below shows a Data Class with ungrouped properties):

Already, our Data Class is starting to look crowded and unwieldy! Here's a better way: Create a new Data Class containing the properties needed for the Patriot Act. Name the class PatriotActProperties.

Now you only need one additional property for your $opening class (the figure below shows a Data Class with grouped properties):

If you do that, you need to keep in mind that you must create a new instance of your PatriotActProperties class when you create a new $opening variable.

You can easily and automatically do this by adding a constructor to your $opening Data Class. A constructor is a class function which is executed when a new instance of a class is created. In our case, the constructor would look like this:

Copy
Method NEW() : Nothing Begin
$this.patriotAct:=NewObject(PatriotActProperties);
End

To access Patriot Act properties in your $opening instance, you simply call them using the dot notation:

Copy
$opening.patriotAct.address

You can add as many hierarchy levels as you need.

Remember: Keep your Data Class as small as possible.

Use Inheritance Properly

Remember our first example, the one involving addresses? We found out that addresses had types; they were either a home address or a work address. We used a property to distinguish between the two types of addresses: A string containing either the word 'home' or the word 'work'.

So an address is either a home address or a work address.

Whenever you can say "My Data Class is a ...", you should consider whether you can use inheritance to express this relationship. If Data Class 1 inherits from Data Class 2, it means that Data Class 1 is a Data Class 2. In our example, our Address can either be a work address or a home address. You can create two new Data Classes, name them WorkAddress and HomeAddress, and tell them that they inherit from Address:

6.7.bmp

The two new Data Classes don't have any new properties added; we only use them to mark whether an Address is a home address or a work address. In this case, adding new classes is not strictly necessary, because simply adding a type to the address would achieve the same effect without adding two more classes.

But consider this: What if the work address contains an additional field for a company name? Adding this field to Address would be confusing, because it would make it possible to fill in a company name for a home address, which makes no sense.

Using inheritance, we can add this field only to the WorkAddress class and thus avoid cluttering our HomeAddress:

6.8.bmp

So how do you use these classes? You declare your $address variable to be of type Address, and then assign new instances of either HomeAddress or WorkAddress to them:

Copy
Local Address $address;
$address := NewObject(HomeAddress);

To distinguish between the two types of addresses, you used to write this: {code:[language:java,numbering:false]} If $address.type == 'home' Then

While this is reasonably readable, the version using inheritance is much closer to English and thus more readable:

Copy
If $address isa HomeAddress Then

There's one more thing you need to keep in mind: If you want to access a field which only exists in a subclass like HomeAddress or WorkAddress, you should create a specific variable of this type:

Copy
If $address isa WorkAddress Then
Local WorkAddress $workaddress;
$workaddress := $address;
$workaddress.companyName := 'Numcom Software';
End

Since FNZ Studio doesn't know that $address really contains an instance of WorkAddress, accessing $address.companyName directly can cause FNZ Studio to log warnings.

Remember: If you can say "My Data Class is a...", you should consider inheritance instead of properties to describe this relationship.

Do Not Use Data Classes to Group Functions

Note: This section is specifically about using Data Classes as makeshift Function namespaces. It does not cover the use of Static Functions as a workaround for missing enums and constants.

In versions of Appway prior to Appway 7, all Script Function Business Objects existed in the global namespace. In order to meaningfully group these Functions, developers often used Data Classes with Static Functions. This allowed Solutions to have a sort of namespace for their Functions.

Copy
// Global Function
BAM_GenerateInstancesReport();
// Static Data Class Function
BAM.GenerateInstancesReport();

While this might be a good option in the past, with the introduction of Packages (Appway 7), it is no longer recommended to take this approach, since a similar result can be achieved using Packages:

Copy
// Script Function in Package
BAM:GenerateInstancesReport();
Recommendation: Do not use Static Data Class Functions to group Functions into namespaces. Instead, use Script Function Business Objects and Packages for this purpose.

Reasoning

The recommendation described above is based on the following factors:

  • Data Classes are not intended to be used as makeshift namespaces for Functions Data Classes are intended to be used to model your Solution's data model. Polluting your list of Data Classes with Function containers makes your Solution harder to understand, harder to maintain, and harder to develop for. For example, the Data Class Business Object selector on an editor's Variables tab will contain lots of Data Classes that should never be instantiated, and where there will never be a variable of that type in your Solution.
  • Packages are available Since Packages can be used to meaningfully group Functions, it's no longer necessary to use Data Classes for this purpose.
  • Fine-Grained Package Visibility is impossible Only Business Objects can be made visible, not individual properties or Functions of a Data Class. If you group multiple Functions into one Data Class, you have to make the the whole group of Functions visible as a unit. With individual Script Functions, it is possible to make Functions visible individually.
  • Combining Static Data Class Functions and Packages creates a peculiar syntax Since Static Data Class Functions inside Packages use two different mechanisms to create their "namespaces", the end result is a syntax with two different "namespace delimiters", which creates an odd syntax: BAM:Instances.GenerateReport();
  • Using Static Data Class Functions makes adopting new FNZ Studio features harder New FNZ Studio features often allow users to select Script Functions (e.g. to select the actual code that is called by a Web API endpoint). If these Script Functions are implemented as Static Data Class Functions, they can't be called directly, which means that Solution developers have to implement bridge functions that call Static Data Class Functions, at which point the whole purpose of grouping Functions into Data Classes is voided.

Related Sources