Traits in EK9

The trait has been touched on in the structure section. This shows how a trait can be used to control/limit class extension.

Main uses for traits

If you are familiar with Java you may consider that traits are just like a Java Interface; there are some similarities but also several features in EK9 where traits play a significantly different role. In fact the trait unlike the Java Interface provides real structural enabling/constraining power. In many ways it provides much more control than a class, but less implementation functionality; and no retained state.

Take your time when reading this section and reviewing the example. If you've used other programming languages don't assume that the trait in EK9 is just some minimal cut down abstract class that can be skimmed over.

The main features/characteristics

Traits Example

There are quite a few ideas covered in the following example. The details of these ideas are discussed following the example.

The program 'ProcessorTest' below creates a number of 'Processors' and then iterates over those 'Processors' and calls process to get a 'ProcessingResponse'. That response is then passed to 'ResultBuilder' via the build method to create an output String.

There are several different 'Processor' classes that exhibit the trait of 'Processor'. This is to demonstrate how different implementations can exhibit the same trait (they all meet the same signature). The 'Processor' trait also inherits from traits 'CostAssessment' and 'Moniterable'. The Method lowCost is declared in both of those traits (to demonstrate a name clash).

The 'DelegatingProcessor' shows how composition works. There are also two classes of 'StringResponse' and 'StructuredResponse' that both exhibit the trait of 'ProcessingResponse'. This provides a method called 'result()'.

The Example

#!ek9
defines module introduction
  
  defines type
    List of Processor
    
  defines trait

    CostAssessment
      lowCost() as pure
        <- rtn as Boolean: true
        
    Moniterable
      available() as pure
        <- rtn as Boolean: true
      lowCost() as pure
        <- rtn as Boolean: false  
        
    Processor with trait of Moniterable, CostAssessment
      process()
        <- response as ProcessingResponse
      override lowCost() as pure
        <- rtn as Boolean: CostAssessment.lowCost()  

    ProcessingResponse allow only StringResponse, StructuredResponse
      result()
        <- rtn as String
        
  defines class

    StringResponse with trait of ProcessingResponse as open
      theResponse as String: String()
      
      StringResponse()
        -> response as String
        theResponse :=: response
        
      override result()
        <- rtn as String: theResponse
    
    StructuredResponse is StringResponse
      contentType as String: "text/plain"
      
      StructuredResponse
        ->
          response as String
          contentType as String
        super(response)
        this.contentType :=: contentType
      
      contentType() as pure
        <- rtn as String: contentType
           
    SimpleProcessor with trait of Processor
      override process()
        <- response as ProcessingResponse: StringResponse("Simple Message")
    
    DelegatingProcessor with trait of Processor by proc
      proc as Processor?
      
      DelegatingProcessor()
        proc: SimpleProcessor()
        
      DelegatingProcessor()
        -> processorToUse as Processor
        proc: processorToUse
      
      override lowCost() as pure
        <- rtn as Boolean: false
        
    XMLProcessor with trait of Processor
      override process()
        <- response as ProcessingResponse: StructuredResponse("<tag>Simple Message</tag>", "text/xml")
      override lowCost()
        <- rtn as Boolean: false
        
    JSONProcessor with trait of Processor
      override process()
        <- response as ProcessingResponse: StructuredResponse(`{"name": "John", "age": 31}`, "application/json")
    
    ResultBuilder     
      
      build() as dispatcher
        -> response as ProcessingResponse
        <- rtn as String: response.result()
        
      build()
        -> response as StringResponse
        <- rtn as String: `"${response.result()}"`
        
      build()
        -> response as StructuredResponse
        <- rtn as String: `${response.contentType()} (( ${response.result()} ))`
              
  defines program
  
    ProcessorTest
      stdout <- Stdout()
      
      processors <- [ SimpleProcessor(), XMLProcessor(), DelegatingProcessor(JSONProcessor()), JSONProcessor()] 
      
      for processor in processors
        response <- processor.process()
        stdout.println(ResultBuilder().build(response))
//EOF

The output of the program above would be:

"Simple Message"
text/xml (( <tag>Simple Message</tag> ))
application/json (( {"name": "John", "age": 31} ))
application/json (( {"name": "John", "age": 31} ))
        

The class 'ResultBuilder' is acting as a decorator (design pattern), it has the ability to access the additional method contentType() because the dispatcher has ensured the right method is called with the right type of parameter.

What has been shown in the Example

The example above is quite sophisticated in terms of structure control. Each aspect of traits is discussed below.

Constraining Extension

As outlined above; a trait can be used to control the degree to which classes may extend the trait. This can be very useful in the development of API's, library classes and sub systems. If the developer needs to ensure that the traits/classes they have developed can be used (but not extended) by users of their API; then applying constraints on a trait provides a flexible way of accomplishing this. EK9 also has the limitation of extending classes by default if the are not open.

In other languages the use of final is the only mechanism to limit extension.

See ProcessingResponse allow only StringResponse, StructuredResponse.

Providing a signature interface

This is the traditional use for interfaces/traits it really just consists of a set of method signatures. The trait can then be used in abstract way. Both 'Processor' and 'ProcessingResponse' are really just signatures, although 'ProcessingResponse' also constrains the classes that may use it as a trait.

With traits; concrete (non abstract) methods can be implemented (a little like default methods on interfaces in Java). The trait 'Processor' has such a method; lowCost demonstrates this. The class 'XMLProcessor' overrides this method to demonstrate that it is possible alter the implementation if needs be.

Trait inheritance

Inheritance/multiple inheritance of traits is also supported as shown with 'CostAssessment', 'Moniterable' and its use in 'Processor'. However there is a significant point to note here, where two methods like lowCost conflict - the trait or class where they are used together has to override that method.

The resolution of this conflict is shown in the 'Processor' trait. It overrides the lowCost method and then delegates the call to CostAssessment.lowCost() it could have used Moniterable.lowCost() or indeed just used true/false.

Composition

One of the biggest benefits of defining traits in EK9 is the ability to use automatic composition. In the example there are a number of 'Processors' these are aimed to processing data to produce a 'ProcessingResponse'. The DelegatingProcessor with trait of Processor by proc demonstrates the use of composition.

The by proc syntax is the key part here; this syntax triggers the delegation of the trait methods to the 'proc' private field object. This simple directive means that the class can now direct all calls to that delegate, but can optionally elect to override and provide some of the method implementations itself. Just take a moment and think about what that means. Delegation is now as easy as inheritance!

As you can see it is not necessary to implement all the methods that 'Processor' defines when using composition. All the other 'Processors' have to implement the abstract methods defined in 'Processor':

The additional use of by proc is the mechanism for composition; this expects a Property/Field in the 'DelegatingProcessor' called 'proc' which is of type 'Processor'. Just by using this statement (and creating 'proc' and setting it) all the method calls to the 'DelegatingProcessor' will be automatically routed to that delegate.

The construction of the 'DelegateProcessor' with DelegatingProcessor(JSONProcessor()) could have been done with an 'XMLProcessor' parameter and the behaviour would have been altered. This is composition; it is way more flexible in many ways than inheritance. It also enforces/ reinforces encapsulation and direct re-use.

Importantly it is possible to use this with any number of traits; and so compose classes out of a range of other classes with compatible traits; through the use of composition. As classes can exhibit multiple traits; class behaviour can be composed from multiple classes, in a way similar to multiple class inheritance (but much simpler and more maintainable).

It is still possible to override the methods for some parts of the 'Processor' signature in the class should you wish to. So in the example above, the method lowCost is not delegated but is re-implemented. But method available has been delegated.

Thoughts

The use of traits provides way more than just a 'set of method signatures'. Yes is can and should be used for that. It has way more power; by facilitating easy delegation and constraining the classes that can use it. It is not just an abstract class!

Please take some time to understand the trait because it is quite different to what most other programming languages offer.

Dispatcher

While not really related directly to traits the class 'ResultBuilder' has a method called build. That method is marked up as a dispatcher. Interestingly that class has two other methods also called build, but with different parameter types.

The important point of the dispatcher syntax is that it removes the need for detecting the types of classes/traits. This is done with the build method and parameter of 'ProcessingResponse'. This is just the base trait, but by marking the build method as a dispatcher; EK9 will look at the actual type of Object passed in and then automatically call the appropriate build method. This is covered in more detail in section advanced class methods.

This feature removes the need to cast Objects or use any syntax like instanceof, EK9 does not support casting or the detection of types. You could view the class 'ResultBuilder' as a sort of 'switch statement for types'. It has a default behaviour, but will attempt to find a method that accepts the type the parameter actually is - rather than just the type that was passed in.

The biggest advantage of this approach is the retention of Object Oriented approach and the avoidance of 'class cast exceptions'. Using 'casting' and 'instanceof' techniques creates brittle code that does not refactor easily.

Summary

Traits can be used in a very sophisticated and flexible manner to provide control and abstraction. They allow Objects to be viewed in different but complementary ways and facilitate design through composition rather than just inheritance.

If you ever find you need functionality from one class in another class don't just think of an inheritance structure; as this can be very limiting. Consider traits and composition by refactoring classes into smaller units and extracting method signatures out to traits.

Next Steps

There is more on this subject in the section on composition.