Stones

Fluent interface in Structured Text

A popular programming design in high level languages as C# is the so-called ‘fluent code’ or ‘fluent interface’. Described in 2005 by Martin Fowler. But what is a fluent interface and how to implement this in Structured Text? In this post I briefly explain what a fluent interface is, (considering, there is already a lot info available) but I will focus on a implementation of a fluent interface in Structured Text.

What is a fluent interface?

Well according to wikipedia :

In software engineering, a fluent interface is a method for designing object oriented APIs based extensively on method chaining with the goal of making the readability of the source code close to that of ordinary written prose, essentially creating a domain-specific language within the interface. Source.

A good example of this ‘method chaining’ can been seen with C#’s LINQ statements:

 
EmployeeNames = EmployeeList.Where(x=› x.Age › 65) 
                            .Select(x=› x) 
                            .Where(x=› x.YearsOfEmployment › 20) 
                            .Select(x=› x.FullName); 

By continously chaining the methods we can build our full statement. It’s good to know that a fluent interface is often used together with a builder pattern!

Why would we want this in Structured Text?

Consider the following code which concats several strings to an error message:


ErrorString := CONCAT(CONCAT(CONCAT(CONCAT('Error number: ' , INT_TO_STRING(ErrorNumber)),'. Message: '),ErrorMessage),'.');

Yes this code works, but in terms of readability and maintainability it scores a zero out of 10. Separating the CONCAT statements to different assign statements will help a little bit:


ErrorString := Concat('Error number: ' , INT_TO_STRING(ErrorNumber));
ErrorString := Concat(ErrorString , '. Message: ');
ErrorString := Concat(ErrorString , ErrorMessage);
ErrorString := Concat(ErrorString , '.');

It is  better, but is still doesn’t feel totally right to constantly assign and reuse a variable string in four statements.

Now think of a StringBuilder function block which exposes a fluent interface. We could constantly reuse this instance and chain the methods to build an error string. The next example code shows how the same error string as in the previous examples could be composed:

ErrorString := _stringBuilder
                .Reset()
                .Append('Error number: ')
                .Append(INT_TO_STRING(ErrorNumber))
                .Append('. Message: ')
                .Append(ErrorMessage)
                .Append('.')
                .ToString();

With help of the fluent interface this code has become much more clear, easy to write and easy to extend!

How do we build a fluent interface?

Alright we are convinced. Now, how do we write such an fluent interface?

The basis is very simple. Each method which is part of the fluent interface returns an object which exposes the new available methods. When there are no domain restrictions this means that each method can return the current instance of the StringBuilder function block itself.

In case of the StringBuilder the first step is to create the StringBuilder function block.  It contains a private string variable which will act as memory string for the different available methods.


FUNCTION_BLOCK PUBLIC StringBuilder
VAR
    {attribute 'hide'}
    _workString:STRING;
END_VAR

Nothing special going on here, but for some more info about the hiding attribute have a look here.

Now to add the methods to the StringBuilder function block which are part of the fluent interface:

METHOD PUBLIC Reset : REFERENCE TO StringBuilder //Reset current build string to an empty string.
_workString:='';
Reset ref= THIS^;
METHOD PUBLIC Append : REFERENCE TO StringBuilder //Append text to the current build string.
VAR_INPUT
	text:STRING; //Text to append to current build string
END_VAR
_workString:= Concat(_workString,text);
Append ref= THIS^;

For this example the StringBuilder has a ‘Reset’ method which sets the work string back to an empty string. Secondly it has an ‘Append’ method which appends an input string to the end of the current work string.

Notice that both methods return a reference to a StringBuilder function block. The method implementations are using the THIS statement to return its own instance of the StringBuilder function block. The THIS keyword is a pointer to itself. In this case the exact type is thus : ‘POINTER TO StringBuilder’. To return it as a ‘REFERENCE TO StringBuilder’ it has first to be dereferenced with the circumflex (^). After that convert it to a reference with the REF= assign statement.

The ToString method which is not part of the fluent interface for completeness:

METHOD PUBLIC ToString : STRING //Returns current build string.
ToString := _workString;

Observe that the StringBuilder function block could easily be expanded with additional methods like ‘Insert’, without modifying the original content. But it could also constrain the available methods by not returning the full function block, but only a part of it exposed by an interface, this is a so called ‘domain optimization’.

Domain optimization

Lets add an additional method to the StringBuilder  function block for inserting a string a specified position:

METHOD PUBLIC InsertAtLocation : REFERENCE TO StringBuilder
VAR_INPUT
	Text:STRING;
	Position:INT;
END_VAR
_workString := Tc2_Standard.INSERT(_WorkSTring,Text,Position);
InsertAtLocation  REF= THIS^;

Like the other methods invoke the method like this:

ErrorString := _stringBuilder.Append('a example.')
                             .InsertAtLocation('This is ',0)
                             .ToString();

Small side note here, I tried to call this method ‘Insert’ instead of ‘InsertAtLocation’. This gave however some strange compiler crashes which I have not discussed with CoDeSys yet..

Now the InsertAtLocation only makes sense after an Append or InsertAtLocation call and not after a reset call. Lets modify the fluent interface such that we avert this call. This is a form of domain optimization. One way to achieve this is by not returning the complete StringBuilder function block, but an interface which only exposes certain methods.

As example I created to interfaces : ‘IStringBuilder’ and ILimitedStringBuilder.

Picture of created interfaces

The IStringBuilder returns all methods. The ILimitedStringBuilder however does not return the InsertAtLocation method. The StringBuilder function block now implements both interfaces:


FUNCTION_BLOCK PUBLIC StringBuilder IMPLEMENTS IStringBuilder, ILimitedStringBuilder
VAR
    {attribute 'hide'}
    _workString:STRING;
END_VAR

Now modify the methods of the fluent interface in such way that they return one of the StringBuilder interfaces:

METHOD PUBLIC Append : IStringBuilder // Append text to the current build string.
VAR_INPUT
    text:STRING; //Text to append to current build string
END_VAR
_workString:= Concat(_workString,text);
Append := THIS^;

Notice that it’s not necessary to use the REF= assignment anymore to assign an interface.
The Reset method however now returns the ILimitedStringBuilder interface to prevent the call to the InsertAtIndex method directly after a reset function:

METHOD PUBLIC Reset : ILimitedStringBuilder //Reset the current build string to an empty string.
_workString:='';
Reset := THIS^;

By returning limited interface the method call possibilities are now constraint.

Using a fluent interface is very convenient way of writing code. The IDE automatically assists you in possible calls:

Example of IDE help Example of IDE help2

Conclusion

This post described the how to build and use a fluent interface in Structured Text. A fluent interface or fluent code is is a design method heavily relying on method chaining. With as purpose making code more clear. The post contains an example of a StringBuilder function block which exposes an fluent interface.

For more interesting topics on PLC coding in Structured Text have look at the posts in The Three Pillars of OOP.

Happy coding 🙂

 

 

Gerhard Barteling

Gerhard is a mechatronic engineer with a predilection for software engineering.

Leave a Reply