Classes in EK9

The 'Class' was briefly outlined in the class section in 'structure'. This section details the simple use of classes and inheritance. Generic/Template classes can also be developed but are slightly more complex and so are covered in more detail separately. Additional classes can also be created by using dynamic classes these can provide a light weight implementation of a trait. Classes are also revisited in the section on traits.

Uses for Classes

A Class in an Object Oriented language is the main construct of development. In EK9 classes are still important but can be augmented with other constructs such as functions, records and components.

The main structural parts of a class are:

The properties/fields are always private. The methods can be overloaded (i.e take different combinations of parameters) but use the same name.

It is possible to create classes that are abstract i.e can only be extended and cannot be instantiated directly. Furthermore non abstract classes are by default 'closed' meaning they cannot be extended. If the developer wishes to enable the class to be extended then they must 'open' the class up.

This last point will probably polarize developers. The main reason for this it to encourage wider use of traits and composition. This is in contrast to always looking to inheritance as the solution. This 'pain' has been deliberately placed here for that reason.

Shape Example

The example below shows a traditional class hierarchy, there is a mix of abstract, open and closed classes.

One of the advantages of creating a class hierarchy like this; is the ability to use the abstract concept of a shape with a collection and then use that to trigger some processing that is applicable to all sub classes of shape, the same sort of reasoning also applies to traits.

The program 'DrawShapes' demonstrates two different ways to trigger the drawing of each type of shape. Firstly, note the construction of a List of Shape; EK9 looks at each of the types in the definition of the List and finds the common type, it can then infer what 'shapes' is a list of.

In this case it is a List of Shape. This is then used in a for loop and then a processing pipeline to show the two different ways the shapes in the list can be used.

Note that mathematically a Square is a Rectangle, but it not modelled this way. See Liskov substitution principle for more details on this. The only method is draw and all this does is print out the fact it would draw the particular shape. If the example was to be extended to actually define widths, heights centre points, etc then an appropriate class structure would be needed. Indeed it probably would be the case that while from a mathematical point of view a Square is a Rectangle.

It would not be modelled this way as it would mean that methods such as setWidth and setHeight on the Rectangle would be inherited by Square and this would not actually make sense (for a Square). So the expected behaviour of those methods would not really conform to the Liskov substitution principle.

#!ek9
defines module introduction

  defines type
    List of Shape
  
  defines function
    doDrawing() as abstract
      -> message as String
    
    doSimpleDrawing() is doDrawing
      -> message as String
      Stdout().println(message.lowerCase())

    doFancyDrawing() is doDrawing
      -> message as String
      Stdout().println(message.upperCase())
              
    drawShape()
      -> shape as Shape
      <- drawn as Bits: Bits(shape.draw())
              
  defines class

    Coordinate
      x Float: 0.0
      y Float: 0.0

      Coordinate()
        x: 0.0
        y: 0.0

      //Constructor
      Coordinate()
        ->
          initialX as Float
          initialY as Float

        x: initialX
        y: initialY

      x()
        <- rtn as Float: x

      y()
        <- rtn as Float: y

      operator +
        -> coord as Coordinate
        <- rtn as Coordinate: Coordinate(this.x() + coord.x(), this.y() + coord.y())

      operator +=
        -> coord as Coordinate
        this.x += coord.x()
        this.y += coord.y()

      operator -
        -> coord as Coordinate
        <- rtn as Coordinate: Coordinate(this.x() - coord.x(), this.y() - coord.y())

      operator -=
        -> coord as Coordinate
        this.x -= coord.x()
        this.y -= coord.y()

    Shape as abstract      
      draw() as abstract
        <- drawn as Boolean
        
    Ellipse extends Shape
      override draw()
        <- drawn as Boolean: true
        doDraw("Draw Ellipse")        
    
      protected doDraw()
        -> message as String
        Stdout().println(message)
        
    Circle is Shape
      override draw()
        <- drawn as Boolean: true
        doDraw("Draw Circle")

      protected doDraw()
        -> message as String
        Stdout().println(message)

    Rectangle is Shape
      shouldDraw as Boolean: true
      
      Rectangle()
        -> doDraw as Boolean
        shouldDraw: doDraw
      
      override draw()
        <- drawn as Boolean: shouldDraw
        if shouldDraw
          Stdout().println("Draw Rectangle")        
       
    Triangle is Shape
      override draw()
        <- drawn as Boolean: true
        Stdout().println("Draw Triangle")
    
    Square is Shape
      drawingFunction as doDrawing
      message as String: "Draw Square"
      
      Square()
        -> functionToDraw as doDrawing
        this.drawingFunction = functionToDraw
        
      override draw()
        <- drawn as Boolean: true
        if drawingFunction?
          drawingFunction(message)
        else
          Stdout().println(message)
                   
  defines program
    DrawShapes()
      stdout <- Stdout()
      stdout.println("Will draw some shapes")    
     
      shapes <- [Square(), Ellipse(), Rectangle(false), Triangle(), Square(doSimpleDrawing), Square(doFancyDrawing)]
      
      //Procedural      
      for shape in shapes
        shape.draw()
      
      //Functional
      drawnShapes <- cat shapes | map with drawShape | collect as Bits  
      stdout.println("Shapes drawn is: " + $drawnShapes) 
//EOF

When defining a class that is not abstract it can be made extendable by using the open modifier. See composition for open usage specifically for class 'SalaryPolicy' and structure.

The modifier keyword override must be used Whenever methods in a base class are overridden in a sub class (as in all of the cases for draw above).

By default all methods on classes are public (there is no need to declare them public). In the case of the doDraw method in the example above the modifier keyword protected has been used. This means that only 'Ellipse' and 'Circle' can call that method - it is not publicly available. If there were classes that extended 'Circle' or 'Ellipse' they would have to be marked as open and then those classes could call doDraw indeed they could also override that method.

Properties defined on classes are all private and cannot be shared other than though method accessors. The private property/field shouldDraw on in the 'Rectangle' class is only visible in that class.

Methods

Some languages support the idea of static methods/variables on the class itself rather than just methods/properties on Object instances. EK9 does not support this. All methods must be on an Object instance, though they do not have to mutate the Object or even use the Object's state. An example of this would be Clock - time() specifically SystemClock().time(). So in EK9 creating an Object instance and using a method like this is acceptable/necessary. In the example of 'Clock' which is a trait alternative implementations to SystemClock can be developed and substituted where necessary (testing for example).

Extending Functionality

Clearly functionality can been added through inheritance. But 'Square' has had functionality and behaviour modified without inheritance. This has been accomplished by using a function delegate. See traits for additional ways to extend functionality without just using inheritance. Composition is a capability built into the EK9 language and this increases reuse and removes the need for multiple class inheritance.

The program 'DrawShapes' creates three different instances of 'Square'; one without a delegate and the other two with different delegates. When the method draw is called on 'Square' a check is made to see if a function delegate has been configured. If so the delegate is called, if not the standard functionality is triggered.

This is an alternative for altering behaviour without inheritance (EK9 does not support 'mixins'), but EK9 does support delegation to other classes where they are compatible and also function delegates. The only issue with delegation is access to private state variables is not possible. This is actually a good thing as it ensures object are encapsulated.

Operators

The 'Coordinate' class has been used to demonstrate some the operators that can also be added to classes. All operators can be implemented on classes if necessary.

Summary

The above is a simple example of using classes, but also with a function delegate. In general classes should have state and behaviour. If you find your classes seem to only hold state then consider using records, if on the other hand you find you have classes with many methods that don't really relate to that class consider pulling them out to functions. There will be times when you need some functionality that seems to need to be in two different classes. These can make good candidates for a function delegate.

Next Steps

There is much more on classes yet to come in methods, traits, extension by composition, inheritance, dynamic classes and advanced class methods.