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. Classes can also be created by using dynamic classes; these can provide a lightweight implementation of traits, classes are revisited in that section.

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. They are not designed to be your primary development focus.

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.

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

As mentioned in the operators section; EK9 can provide one off implementations for some operators when they are not marked abstract and no body/return value is implemented. This is shown below in the Coordinate class together with a default constructor with no body.

In this case a default constructor for Coordinate is made publicly available. If the developer wished to prevent default object construction of a Coordinate then the constructor could have been marked as private.

#!ek9
defines module introduction

  defines function

    doDrawing() as abstract
      -> message as String

    doSimpleDrawing() is doDrawing
      -> message as String
      Stdout().println(message.lowerCase())

    doFancyDrawing() extends 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

      //Sort of lombok like, no implementation needed.
      default Coordinate()

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

        x :=: initialX
        y :=: initialY

      x()
        <- rtn as Float: x

      y()
        <- rtn as Float: y

      operator + as pure
        -> 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 - as pure
        -> 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()

      //Instruction to add in additional operators where these can be auto generated.
      default operator

    Shape as abstract
      draw() as abstract
        <- drawn as Boolean?

    Ellipse extends Shape as open
      override draw()
        <- drawn as Boolean: true
        doDraw("Draw Ellipse")

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

    Circle is Ellipse
      override draw()
        <- drawn as Boolean: true
        doDraw("Draw Circle")

    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 shapes")

      shapes <- [
        Circle(),
        Square(),
        Ellipse(),
        Rectangle(),
        Triangle(),
        Square(doSimpleDrawing),
        Square(doFancyDrawing),
        Rectangle(false)
        ]

      //Procedural
      for shape in shapes
        shape.draw()

      //Functional
      drawnShapes <- cat shapes | map with drawShape | collect as Bits
      stdout.println("Shapes drawn is: " + $drawnShapes)

      //You could have also done this
      shapeInfo <- cat [
        Circle(),
        Square(),
        Ellipse(),
        Rectangle(),
        Triangle(),
        Square(doSimpleDrawing),
        Square(doFancyDrawing),
        Rectangle(false)
        ]
        | map with drawShape
        | collect as Bits

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

Constructors

With default constructors (constructors that take no parameters) it is possible to declare the constructor with no body (as shown above with Coordinate). Furthermore, it is possible to make these constructor protected/private rather than just being public.

You may wonder why EK9 has this feature; it enables the developer to quickly and easily define and control the visibility of the class default constructor; thereby limiting/controlling its use.

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 be 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 enforces encapsulation.

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.

As with records; classes support the default operator syntax; see operators for more details on this. This capability has been provided so that operators can be provided with a default implementation (if the solution is logical and simple). But, EK9 also enables the developer to override and provide their own implementations of each of the operators as well. If the developer provides their own implementations for some operators; EK9 will add in those operators it can with default implementations if the default operator syntax is used after all operators implementations.

Summary

The example above shows classes and also 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.