We are now going to see how Obix helps to make software more reliable and robust.
First, it is interesting to note two important rules that have already been applied in the previous chapters' source code, without any coding efforts. One of the main goals in Obix is to reduce the number of bugs, and therefore all default values for software components in source code are severe values aimed to reduce error-proneness. Two examples are explained in the following two subsection.
One very important rule states:
In Obix every object is immutable by default.
Let's look at the following excerpt of the source code of type bank_account we defined previously:
type bank_account
attribute customer type:bank_customer end
attribute balance type:zero_positive32 default:0 kind:variable setable:factory end
// ...
end typeAs we didn't specify anything else than the type for attribute customer, the above rule implies that attribute customer is immutable. This means that, once a bank_account object is created, its customer attribute cannot be set to another value. The customer of a bank account remains unchanged over the whole lifecycle.
On the other hand, it is clear that attribute balance cannot be immutable. This has to be specified explicitly by setting the attribute's property kind to variable. However, the attribute's value cannot be set directly with an instruction like account.balance = 1000. The balance can only be changed by the factory, whenever command pay_in or withdraw is called. Therefore, property setable is set to factory. (Remark: If instructions like account.balance = 1000 were allowed, then property setable would have to be explicitly set to all.)
Immutable objects should always be favored, because they are simpler to handle and much less error-prone. For more information see Chapter 14, Object immutability in the programming manual.
For more information on attribute properties see the section called “Attribute” in the programming manual.
Another really important rule is the following one:
In Obix void values are not permitted by default.
![]() | Note |
|---|---|
| void is a synonym for the words null, nil or nothing which are used in other programming languages. |
The principle is analogous to what we have seen for the previous rule: As we didn't specify anything else than the type for attribute customer, the above rule implies that attribute customer cannot be void. This means that, whenever a bank_account object is created, a non-void customer object must be provided.
If we wanted to permit void values (although not appropriate in our case) we would have to specify this explicitly by setting property voidable to yes, as follows:
attribute customer type:bank_customer voidable:yes end
The same is true for attribute balance. As we didn't explicitly specify if void values are allowed, property voidable is implicitly set to no. Hence, every factory is forced to initialize balance with a non-void value, or else a runtime error will immediately occur, as soon as a bank_account object is created.
A further enhancement is to define a default value for attribute balance in type bank_account. This is done by using property default, as follows:
attribute balance type:zero_positive32 default:0 kind:variable setable:factory end
The consequence will be that attribute balance will be 0 after object creation, without the need for any initialization instruction in the factory's creator. The benefits of defining a default value in the type instead of defining it in the factory are manyfold:
In case of several factories implementing the same type, the default value will automatically be the same in all factories, and any changes in the type will automatically be reflected in all the factories.
The default value for an attribute is a design choice rather than an implementation choice and should therefore be defined in the type.
All attribute default values of a type are implicitly inherited in all its child types.
Whenever a programmer uses a type she will look at the type's definition to understand the type's role and behavior.
For more information on void values see Chapter 12, Void values in the programming manual.
For more information on attribute property default see the section called “Attribute property default” in the programming manual.
Suppose we define the following data validation rules for type bank_customer:
identifier must be greater than 1000name and city cannot be empty (they must contain at least one character), and their length is limited to a maximum of 40 characters.This can easily be done with a technique called Contract programming. Contract programming is one of the cornerstones for writing more reliable code. Simply said, Contract programming consists of adding appropriate checks at different locations in the source code.
In our case we just have to add some check properties, as shown below:
type bank_customer default_factory:yes attribute identifier type:positive32 check: i_identifier > 1000 end attribute name type:string check: not i_name.is_empty and i_name.item_count <= 40 end attribute city type:string check: not i_city.is_empty and i_city.item_count <= 40 end end type
Because the use of a non-empty string is quite common, type non_empty_string exists in Obix' standard library, and we can use that type instead of type string. Thus, we don't have to write the check for emptiness anymore. Our code becomes:
type bank_customer default_factory:yes attribute identifier type:positive32 check: i_identifier > 1000 end attribute name type:non_empty_string check: i_name.item_count <= 40 end attribute city type:non_empty_string check: i_city.item_count <= 40 end end type
Besides the advantage of simpler source code, another benefit is that the coding error of assigning an empty string to name or city (i.e. name = "") would now be detected at compile-time. This means that, once the whole application is compiled, it is guaranteed not to contain an instruction that would assign an empty string to name or city!
Because attributes name and city have the same value for property type, we can simplify further and avoid code duplication as follows:
type bank_customer default_factory:yes
attribute identifier type:positive32 check: i_identifier > 1000 end
attribute_list type:non_empty_string
attribute name check: i_name.item_count <= 40 end
attribute city check: i_city.item_count <= 40 end
end attribute_list
end typeAll properties specified after the attribute_list keyword are applied to all attributes embedded between attribute_list and end attribute_list.
That's all we have to do to add the requested data validation rules! The conditions specified in the type can never be bypassed. Obix guarantees that all objects will always fulfill all conditions defined by checks. Anytime a factory or client code violates a condition, a runtime error will immediately be generated.
Checks can also be applied to input and output arguments of commands. Suppose that command pay_in of type bank_account should only accept amounts greater than 100 cents. Moreover, command withdraw must be protected against withdrawing more money than available. Here is the code:
type bank_account
attribute customer type:bank_customer end
attribute balance type:zero_positive32 default:0 kind:variable setable:factory end
command pay_in
in amount type:positive32 check: amount > 100 end
end command
command withdraw
in amount type:positive32 check: amount <= i_object_.a_balance end
end command
end type![]() | Note |
|---|---|
i_object_ in the above code is an implicitly defined input argument available in check scripts and useful to access the object's attributes whenever needed. |
This was just a very brief introduction to Contract programming. For more information see Chapter 17, Contract programming in the programming manual.
Let's look again at the following code of type bank_customer and see how we can further enhance it:
type bank_customer default_factory:yes
attribute identifier type:positive32 check: i_identifier > 1000 end
attribute_list type:non_empty_string
attribute name check: i_name.item_count <= 40 end
attribute city check: i_city.item_count <= 40 end
end attribute_list
end typeWe can see that attributes name and city are of the same type, namely non_empty_string. However, they are semantically different, because they denote different kinds of data. Moreover, an application could contain hundreds or thousands of types with attributes of type non_empty_string. The problem that arises in this case is that the compiler cannot detect wrong assignments of semantically different objects, because the source and target objects are both of type non_empty_string.
Suppose, for example, the following type that also has an attribute of type non_empty_string:
type remote_computer default_factory:yes attribute IP_address type:non_empty_string end attribute port type:positive32 end end type
Now, the following code is valid, although assigning an IP address to the city of a bank customer would probably make little sense in most applications:
var remote_computer computer = fa_remote_computer.create ( & IP_address = "83.99.33.231" & port = 80 ) var non_empty_string address = computer.IP_address var bank_customer albert = fa_bank_customer.create ( & identifier = 100 & name = "Albert" & city = address )
Another non-sense instruction of the same kind would be to withdraw from a bank account an amount that is the port number of a remote computer:
var bank_account account = fa_bank_account.create ( albert ) account.withdraw ( amount = computer.port )
Obviously it is easy to imagine any number of similar errors in a big application.
![]() | Note |
|---|---|
| One might argue that nobody would ever write silly instructions like the above ones. However, we have to keep in mind that we are here just looking at the most simplest instructions that allow us to understand the idea. We want to keep the exercise simple. In a real application, objects are typically retrieved from other routines, possibly in other libraries written by other programmers. This makes it of course more difficult to immediately grasp the error with a quick look at the source code. Experience shows that silly programming errors of the above kind are much more frequent than one would imagine, especially in big and changing applications written by many programmers, with some of them being replaced over time. |
Before looking at the solution for this problem, let's examine another problem that will be solved with the same solution.
Suppose an application contains several types that have a city attribute like the one we defined in type bank_customer. For example, type supplier has attribute city, type country has attribute capital, and so on. In that case, attribute declarations like the following one would be scattered in all those types.
attribute city type:non_empty_string check: i_city.item_count <= 40 end
Obviously this is code duplication. Code duplication must always be avoided, because it makes software maintenance much harder. For example, if countries are limited to 50 characters in a future release, then all types containing a city attribute must be changed. This can get a pain, and there is always the risk of forgetting one or more type.
One solution to this problem would be to use so-called source code templates. But this would not solve our first problem. Moreover source code templates should never be used when other, more suited, techniques are available. Therefore we will not further investigate this solution. However, the reader who wants more information right now can take a look at Chapter 20, Source code templates in the programming manual.
Both problems can easily be solved with a concept called feature redefinition in child types.
Feature redefinition enables us to change features inherited in a child type. In our case, a city is just a non-empty string whose number of characters is limited to 40. Hence, we can create a new type city that inherits from type simple_non_empty_string, and redefine attribute value to limit it to 40 characters. The source code looks like this:
type city
inherit simple_non_empty_string
attribute value and_check: i_value.item_count <= 40 end
end inherit
end type![]() | Note |
|---|---|
We could have inherited from type non_empty_string instead of simple_non_empty_string, but that would require us to implement all features defined in type non_empty_string. For more information please refer to Obix's API documentation. |
Attribute city in type bank_customer can now be redefined as shown below:
type bank_customer default_factory:yes attribute identifier type:positive32 check: i_identifier > 1000 end attribute name type:non_empty_string check: i_name.item_count <= 40 end attribute city type:city end end type
The advantages are twofold:
city object is now semantically different from a string object. Therefore the compiler now detects errors as those explained above (e.g. customer.city = computer.IP_address).city can now be used in all other types that have city attributes. In case of any requirement changes regarding city attributes, there is now only one place to change (i.e. type city).Similar enhancements should of course be applied to all other attributes defined in types bank_customer and bank_account (for example to avoid withdrawing an amount that equals the number of children in a classroom ;-)).
As we can see, feature redefinition in child types is an important concept for making code less error-prone and more maintainable. Therefore it should be used frequently in all kinds of professional applications. There are more ways to utilize it than what we have seen in the above example. For more information see Chapter 18, Feature redefinition in the programming manual.
An important question is: What happens in case of arithmetic overflow errors?
Some languages silently ignore arithmetic overflow errors for performance reasons. For example, consider the following Java statements:
int march_turnover = 1000000000; int april_turnover = 2000000000; int total_turnover = march_turnover + april_turnover; System.out.println ( "March + April turnover: " + total_turnover );
The result displayed is:
March + April turnover: -1294967296
The reason is that in Java numbers just wrap around when the result exceeds the range of the given data type.
![]() | Note |
|---|---|
The same wrong result is displayed by simply executing System.out.println ( 1000000000 + 2000000000 ); Please note that sometimes errors or warnings are produced in Java when there is a risk of arithmetic overflow. For example, doing arithmetic operations with a By default C# also produces erroneous results in overflow situations. However, overflow checking can be enforced by marking a block of code with the |
As the most pursued goal with Obix is to write more reliable software, there is obviously only one acceptable rule that ensures consistent behavior:
In Obix, arithmetic overflow errors always produce runtime errors.
![]() | Note |
|---|---|
Of course, this behavior comes at a performance cost. Checking for overflow takes time. But this shouldn't be a problem with most Obix application, where speed is mostly determined by file input/output operations, network communication, and hardware resources. If maximum speed must be achieved and overflow errors are excluded in a 'number crunching' script, then embedded Java code can be used for maximum performance. See Chapter 10, Embedded Java source code for information on how to do this. It is not excluded that faster arithmetic calculations with explicitly disabled overflow checking will be available in a future version of Obix. |
For example, the following instruction immediately produces a runtime error:
console.message ( (1000000000 + 2000000000).to_string )
In this context it is also worth to mention that the instruction:
console.message ( 1000000000 + 2000000000 )
wouldn't be accepted by the compiler.
The reason is another rule that is applied consistently:
In Obix, there are no implicit type conversions.
console.message requires a string as input argument. Therefore, we have to explicitly convert the integer result into a string with the to_string command.
To see what could happen if the compiler implicitly converted the integer result into a string, we can execute the following Java statement:
System.out.println ( "56 + 1 = " + 56+1 ) ;
The surprising result is:
56 + 1 = 561
![]() | Note |
|---|---|
| It is interesting to note that arithmetic overflow errors and implicit type conversions have produced some of the most dramatic catastrophes in the history of computing. For an example of a spacecraft explosion, search for 'ariane 5 crash' on the net, or visit http://www.around.com/ariane.html. |
For more information and further examples on how to increase software reliability, see Part III, “Advanced concepts” in the programming manual.