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.
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:
So you would add a new Address object to the addresses
list, and at the same index, set its type in the addresstypes
list:
$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:
SIZE($opening.addresses)
But this would be just as valid:
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.
A much nicer way would be to add the type to your Address Data Class (the figure below shows Data Class without duplicate information):
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):
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.
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:
- Using a new variable
- 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:
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:
$opening.patriotAct.address
You can add as many hierarchy levels as you need.
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
:
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
:
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:
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:
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:
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.
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.
// 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:
// Script Function in Package
BAM:GenerateInstancesReport();
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.