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:
- An optional super class; only single inheritance is supported
- Optional trait(s)
- Any number of properties/fields to hold state on the class
- Any number of methods that can be public, protected or private
- Implementation of any of the standard operators
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.
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.
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).
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.
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.
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.
There is much more on classes yet to come in methods, traits, extension by composition, inheritance, dynamic classes and advanced class methods.