Chapter 6. More reliable code, please!

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.

Object immutability

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 type

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

Void values

Another really important rule is the following one:

In Obix void values are not permitted by default.

[Note]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.

Contract programming

Suppose we define the following data validation rules for type bank_customer:

  • attribute identifier must be greater than 1000
  • attributes name 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 type

All 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]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.

Feature redefinition in child types

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 type

We 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]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]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:

  • A 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).
  • Type 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.

Arithmetic overflow errors and implicit type conversions

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]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 byte or short data type generate a 'possible loss of precision' compile-time error. An example of such a statement is: byte b1 = 100 + 100;. However, this behavior is inconsistent with the above one, where no error is generated when int values are used.

By default C# also produces erroneous results in overflow situations. However, overflow checking can be enforced by marking a block of code with the checked keyword. The problem is that unchecked is the default behavior, which means that the programmer must be aware of the problem and not forget to use the checked keyword each time there is a risk of overflow. Overflow checking can also be enforced in the whole program with the /checked compiler option. But this solution raises the risk of compiling the same source code with different compiler settings, leading to different behavior at runtime.

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