Introduction to Object-Oriented Programming with Elan
To undertake object-oriented programming (OOP) in Elan, you should first be reasonably familiar with procedural programming in Elan.
Then, switch to the Object Oriented profile from the menu at the top of the IDE.
This still allows you to do procedural programming, but changes two things:
- The list of demo programs will now show only programs that use OOP.
A very good place to start is to look at the Snake - object-oriented demo, which is functionally identical to the (procedural) original,
but it written using OOP design and coding patterns.
We will be referring to code within this and other OOP demo programs within this text.
- It will change some of the options available on the new code menus in the editor:
- At the global level, it will offer: concrete class, abstract class, interface, and enum.
Each of these allows you to create a ‘user-defined type’ – that may then be used in a similar fashion to the standard types such
Int
int
int
Integer
int, String
str
string
String
String, or List
list
List
List
List.
(Enums - which are single-line instructions - are not directly associated with OOP, but because an enum is a very simple form of user-defined type, it is made available in the same profile.)
- Within a concrete class, abstract class, or interface, the new code menu will show an entirely new list of instructions,
collectively known as ‘members’ (plus comment) that may be defined within those three new global instructions.
If you already understand OOP but are new to coding with Elan...
Be aware that Elan imposes a set of rules on the use of OOP, and which Elan enforces for all the supported languages - even though the individual languages, when used outside Elan, might not impose those constraints.
These constraints are all good discipline: recognised by experts in OOP. So learning OOP within Elan will give you a better understanding and lead to fewer problems.
Before trying to writ
Exploring the 'Snake
In the Snake object-oriented demo you can find three examples of concrete classes:
Only the 'signature' of each class is shown here - not the instructions within the classes:
+class SnakeName? inheritance?1
+constructor(parameter definitions?)
2
new code
end constructor
+function toStringname?(parameter definitions?) returns StringType?
3
return "undefined"value or expression?4
end function
end class
+class AppleName? inheritance?5
+constructor(parameter definitions?)
6
new code
end constructor
+function toStringname?(parameter definitions?) returns StringType?
7
return "undefined"value or expression?8
end function
end class
+class SquareName? inheritance?9
+constructor(parameter definitions?)
10
new code
end constructor
+function toStringname?(parameter definitions?) returns StringType?
11
return "undefined"value or expression?12
end function
end class
+class SnakeName? inheritance? # concrete class1
+def __init__(self: Snake) -> None:
2
new code
+def toStringname?(self: Snake) -> strType?:
# function method3
return "undefined"value or expression?4
+class AppleName? inheritance? # concrete class5
+def __init__(self: Apple) -> None:
6
new code
+def toStringname?(self: Apple) -> strType?:
# function method7
return "undefined"value or expression?8
+class SquareName? inheritance? # concrete class9
+def __init__(self: Square) -> None:
10
new code
+def toStringname?(self: Square) -> strType?:
# function method11
return "undefined"value or expression?12
main()
+class SnakeName? inheritance? {1
+public Snake(parameter definitions?) {
2
new code
}
+public stringType? toStringname?(parameter definitions?) {
// function method3
return "undefined"value or expression?;4
}
}
+class AppleName? inheritance? {5
+public Apple(parameter definitions?) {
6
new code
}
+public stringType? toStringname?(parameter definitions?) {
// function method7
return "undefined"value or expression?;8
}
}
+class SquareName? inheritance? {9
+public Square(parameter definitions?) {
10
new code
}
+public stringType? toStringname?(parameter definitions?) {
// function method11
return "undefined"value or expression?;12
}
}
+Class SnakeName? inheritance? inheritance?1
+Sub New(parameter definitions?)
2
new code
End Sub
+Function toStringname?(parameter definitions?) As StringType?
3
Return "undefined"value or expression?4
End Function
End Class
+Class AppleName? inheritance? inheritance?5
+Sub New(parameter definitions?)
6
new code
End Sub
+Function toStringname?(parameter definitions?) As StringType?
7
Return "undefined"value or expression?8
End Function
End Class
+Class SquareName? inheritance? inheritance?9
+Sub New(parameter definitions?)
10
new code
End Sub
+Function toStringname?(parameter definitions?) As StringType?
11
Return "undefined"value or expression?12
End Function
End Class
+class SnakeName? inheritance? inheritance? {1
+public Snake(parameter definitions?) {
2
new code
}
+public StringType? toStringname?(parameter definitions?) {
// function method3
return "undefined"value or expression?;4
}
}
+class AppleName? inheritance? inheritance? {5
+public Apple(parameter definitions?) {
6
new code
}
+public StringType? toStringname?(parameter definitions?) {
// function method7
return "undefined"value or expression?;8
}
}
+class SquareName? inheritance? inheritance? {9
+public Square(parameter definitions?) {
10
new code
}
+public StringType? toStringname?(parameter definitions?) {
// function method11
return "undefined"value or expression?;12
}
}
This example illustrates an important principle of OOP: an object-oriented program should largely consist of the definition of types, the names and definitions of which
correspond to entities in program's 'domain'. Here the domain is a game of 'snake' - which incorporate the concepts of a snake, an apple, and many squares.
The program might included classes to represent, game, player, board, and others.
The decision as to which entities from the domain should represented in code comes with experience - based on how much value would be added to the program by
defining such a class - and some programmers will opt for more than others.
The important principle is that each class should sound familiar to someone who is familiar with the problem domain (in this case: with the game of 'snake') but
is not necessarily a programmer.
Instances
Each concrete class may be thought of as a template, from which one or more 'instances' are created.
It is valid to define a concrete class that is instantiated only once
within the program, but most classes that you define should be instantiated multiple times. When a class is instantiated, the instance is assigned to a variable,
and we can see two (of more) separate examples of this from Snake here:
variable snakename? set to new Snake()value or expression?0
snakename? = Snake()value or expression? # variable definition0
var snakename? = new Snake()value or expression?;0
Dim snakename? = New Snake()value or expression? ' variable definition0
var snakename? = new Snake()value or expression?;0
variable tailname? set to new Square(20, 15)value or expression?0
tailname? = Square(20, 15)value or expression? # variable definition0
var tailname? = new Square(20, 15)value or expression?;0
Dim tailname? = New Square(20, 15)value or expression? ' variable definition0
var tailname? = new Square(20, 15)value or expression?;0
Members
Each concrete classes contains other instructions., collectively known as the 'members' of the class. The following different kinds of members can be seen:
Property
Most classes define several properties.
A property has name (like a variable) and a type, which might be a standard simple type or data structure, but can also be a user-defined type - which indicates that
the property contains a reference to an instance of that type. (This pattern is often referred to as an 'association' between instances. Like a variable, the value of
a property may change during the program run. However, each instance holds its own value for each of the properties defined on its class.
Here are some examples of
properties being defined (the examples are taken from different classes), showing the name and type of each:
+constructor(parameter definitions?)
0
new code
end constructor
property x as Int
Code does not parse as Elan.
property x as Int
Code does not parse as Elan.
+Sub New(parameter definitions?)
0
new code
End Sub
property x as Int
Code does not parse as Elan.
taken from Square
Square
Square
Square
Square
+constructor(parameter definitions?)
0
new code
end constructor
property location as Square
Code does not parse as Elan.
property location as Square
Code does not parse as Elan.
+Sub New(parameter definitions?)
0
new code
End Sub
property location as Square
Code does not parse as Elan.
taken from Apple
Apple
Apple
Apple
Apple
+constructor(parameter definitions?)
0
new code
end constructor
private property body as List
Code does not parse as Elan.
private property body as List
Code does not parse as Elan.
+Sub New(parameter definitions?)
0
new code
End Sub
private property body as List
Code does not parse as Elan.
taken from Snake
Snake
Snake
Snake
Snake
Within the same class the value of a property may be read, or written to using the following syntax (taken from Snake
Snake
Snake
Snake
Snake):
set this.priorTailvariableName? to this.body[0]value or expression?0
self.priorTailvariableName? = self.body[0]value or expression? # re-assign variable0
this.priorTailvariableName? = this.body[0]value or expression?; // re-assign variable0
Me.priorTailvariableName? = Me.body[0]value or expression? ' re-assign variable0
this.priorTailvariableName? = this.body[0]value or expression?; // re-assign variable0
where the value of the property named priorTail
priorTail
priorTail
priorTail
priorTail is being changed to hold the first element from the list of squares held in the property named body
body
body
body
body.
In both cases the keyword this
this
this
this
this refers to the current instance (i.e. the instance to which the code is applied). It is followed by a dot and the name of the property - an example of 'dot syntax'.
Constructor
Each class must define one, and only one, constructor. The role of the constructor is to 'initialise' (set the initial values) of the properties.
The constructor is called each time an instance of a class is created.
In the constructor for Square
Square
Square
Square
Square:
constructor(x as Int, y as Int)
set this.x to x
set this.y to y
end constructor
Code does not parse as Elan.
constructor(x as Int, y as Int)
set this.x to x
set this.y to y
end constructor
Code does not parse as Elan.
constructor(x as Int, y as Int)
set this.x to x
set this.y to y
end constructor
Code does not parse as Elan.
constructor(x as Int, y as Int)
set this.x to x
set this.y to y
end constructor
Code does not parse as Elan.
constructor(x as Int, y as Int)
set this.x to x
set this.y to y
end constructor
Code does not parse as Elan.
the value of the properties x
x
x
x
x and y
y
y
y
y are set to the values defined by the two parameters. Although the parameters have the same names as the properties they
ambiguity is avoided because the property is always prefixed by this
this
this
this
this.
The constructor for Snake
Snake
Snake
Snake
Snake does not define any parameters, because a snake is instantiated only once within each game, and always to the same size and initial position:
constructor()
constructor()
constructor()
constructor()
constructor()
Note, however, that within the constructor, it creates a new instance of Square
Square
Square
Square
Square, passing in the arguments 20
20
20
20
20 and 15
15
15
15
15, which the square's constructor requires.
It also makes used of getAdjacentSquare
which is an example of using a method - which we shall explore next.
Methods
The properties on a class define the state of an instance; the methods define its behaviour. Methods on a concrete class have two distinct forms: function method and procedure method.
These mirror the distinction between a (global) function and procedure. A function is a 'query' that returns information based on the parameters that are supplied to it,
but in the case of a function method (defined within a class), in addition to the parameter values, it has access to the property values of the instance - which is legitimate because it is only ever called
via that instance. Thus, in the example above, getAdjacentSquare is invoked on the variable tail
tail
tail
tail
tail (an instance of Square
Square
Square
Square
Square), and is passed
the value of currentDir
currentDir
currentDir
currentDir
currentDir as an argument. Looking at the definition of getAdjacentSquare within the class Square
Square
Square
Square
Square:
+function getAdjacentSquarename?(d as Directionparameter definitions?) returns SquareType?
1
variable newXname? set to this.xvalue or expression?2
variable newYname? set to this.yvalue or expression?3
+if d is Direction.leftcondition? then
4
set newXvariableName? to this.x - 1value or expression?5
elif d is Direction.rightcondition? then6
set newXvariableName? to this.x + 1value or expression?7
elif d is Direction.upcondition? then8
set newYvariableName? to this.y - 1value or expression?9
elif d is Direction.downcondition? then10
set newYvariableName? to this.y + 1value or expression?11
end if
return new Square(newX, newY)value or expression?12
end function
+def getAdjacentSquarename?(d: Directionparameter definitions?) -> SquareType?:
# function1
newXname? = self.xvalue or expression? # variable definition2
newYname? = self.yvalue or expression? # variable definition3
+if d == Direction.leftcondition?:
4
newXvariableName? = self.x - 1value or expression? # re-assign variable5
elif d == Direction.rightcondition?: # else if6
newXvariableName? = self.x + 1value or expression? # re-assign variable7
elif d == Direction.upcondition?: # else if8
newYvariableName? = self.y - 1value or expression? # re-assign variable9
elif d == Direction.downcondition?: # else if10
newYvariableName? = self.y + 1value or expression? # re-assign variable11
return Square(newX, newY)value or expression?12
main()
+static SquareType? getAdjacentSquarename?(Direction dparameter definitions?) {
// function1
var newXname? = this.xvalue or expression?;2
var newYname? = this.yvalue or expression?;3
+if (d == Direction.leftcondition?) {
4
newXvariableName? = this.x - 1value or expression?; // re-assign variable5
} else if (d == Direction.rightcondition?) {6
newXvariableName? = this.x + 1value or expression?; // re-assign variable7
} else if (d == Direction.upcondition?) {8
newYvariableName? = this.y - 1value or expression?; // re-assign variable9
} else if (d == Direction.downcondition?) {10
newYvariableName? = this.y + 1value or expression?; // re-assign variable11
}
return new Square(newX, newY)value or expression?;12
}
+Function getAdjacentSquarename?(d As Directionparameter definitions?) As SquareType?
1
Dim newXname? = Me.xvalue or expression? ' variable definition2
Dim newYname? = Me.yvalue or expression? ' variable definition3
+If d = Direction.leftcondition? Then
4
newXvariableName? = Me.x - 1value or expression? ' re-assign variable5
ElseIf d = Direction.rightcondition? Then6
newXvariableName? = Me.x + 1value or expression? ' re-assign variable7
ElseIf d = Direction.upcondition? Then8
newYvariableName? = Me.y - 1value or expression? ' re-assign variable9
ElseIf d = Direction.downcondition? Then10
newYvariableName? = Me.y + 1value or expression? ' re-assign variable11
End If
Return New Square(newX, newY)value or expression?12
End Function
+static SquareType? getAdjacentSquarename?(Direction dparameter definitions?) {
// function1
var newXname? = this.xvalue or expression?;2
var newYname? = this.yvalue or expression?;3
+if (d == Direction.leftcondition?) {
4
newXvariableName? = this.x - 1value or expression?; // re-assign variable5
} else if (d == Direction.rightcondition?) {6
newXvariableName? = this.x + 1value or expression?; // re-assign variable7
} else if (d == Direction.upcondition?) {8
newYvariableName? = this.y - 1value or expression?; // re-assign variable9
} else if (d == Direction.downcondition?) {10
newYvariableName? = this.y + 1value or expression?; // re-assign variable11
}
return new Square(newX, newY)value or expression?;12
}
we see that the direction (assigned to the parameter d
d
d
d
d) is used together with the square's x
x
x
x
x and y
y
y
y
y properties to identify the coordinates
newX
newX
newX
newX
newX and newY
newY
newY
newY
newY for the adjacent square in the specified direction, and use those to create and return a new instance of Square
Square
Square
Square
Square with
those two new coordinates.
Similarly a procedure method is like a global procedure except that it is defined within a class, is accessed only via an instance of that class, using dot syntax, and also
has internal access to the properties of that instance. Both kinds of procedure may be thought of as 'commands': they make changes to the state of the system.
A common use of procedure methods is to change the values of one or more properties and/or to make changes outside the object.
The method newRandomPosition defined on the class Apple
Apple
Apple
Apple
Apple:
+procedure newRandomPositionname?(snake as Snakeparameter definitions?)
1
variable changePositionname? set to truevalue or expression?2
+while changePositioncondition?
3
variable ranXname? set to randint(0, 39)value or expression?4
variable ranYname? set to randint(0, 29)value or expression?5
set this.locationvariableName? to new Square(ranX, ranY)value or expression?6
+if not snake.bodyCovers(this.location)condition? then
7
set changePositionvariableName? to falsevalue or expression?8
end if
end while
end procedure
+def newRandomPositionname?(snake: Snakeparameter definitions?) -> None:
# procedure1
changePositionname? = Truevalue or expression? # variable definition2
+while changePositioncondition?:
3
ranXname? = randint(0, 39)value or expression? # variable definition4
ranYname? = randint(0, 29)value or expression? # variable definition5
self.locationvariableName? = Square(ranX, ranY)value or expression? # re-assign variable6
+if not snake.bodyCovers(self.location)condition?:
7
changePositionvariableName? = Falsevalue or expression? # re-assign variable8
main()
+static void newRandomPositionname?(Snake snakeparameter definitions?) {
// procedure1
var changePositionname? = truevalue or expression?;2
+while (changePositioncondition?) {
3
var ranXname? = randint(0, 39)value or expression?;4
var ranYname? = randint(0, 29)value or expression?;5
this.locationvariableName? = new Square(ranX, ranY)value or expression?; // re-assign variable6
+if (!snake.bodyCovers(this.location)condition?) {
7
changePositionvariableName? = falsevalue or expression?; // re-assign variable8
}
}
}
+Sub newRandomPositionname?(snake As Snakeparameter definitions?)
' procedure1
Dim changePositionname? = Truevalue or expression? ' variable definition2
+While changePositioncondition?
3
Dim ranXname? = randint(0, 39)value or expression? ' variable definition4
Dim ranYname? = randint(0, 29)value or expression? ' variable definition5
Me.locationvariableName? = New Square(ranX, ranY)value or expression? ' re-assign variable6
+If Not snake.bodyCovers(Me.location)condition? Then
7
changePositionvariableName? = Falsevalue or expression? ' re-assign variable8
End If
End While
End Sub
+static void newRandomPositionname?(Snake snakeparameter definitions?) {
// procedure1
var changePositionname? = truevalue or expression?;2
+while (changePositioncondition?) {
3
var ranXname? = randint(0, 39)value or expression?;4
var ranYname? = randint(0, 29)value or expression?;5
this.locationvariableName? = new Square(ranX, ranY)value or expression?; // re-assign variable6
+if (!snake.bodyCovers(this.location)condition?) {
7
changePositionvariableName? = falsevalue or expression?; // re-assign variable8
}
}
}
changes the location
location
location
location
location to a new Square
Square
Square
Square
Square - which requires it to be a procedure method.
(A second reason is that it uses the system method randint - which depends upon, and changes, data held in the system.)
Note that this procedure method makes use of the function method bodyCovers
bodyCovers
bodyCovers
bodyCovers
bodyCovers, defined on Snake
Snake
Snake
Snake
Snake.
A function method may also use other function methods and global functions, but may not call any procedure, whether global or procedure method.
toString method
Every class must define a function method named toString, which defines no parameters and returns a String
str
string
String
String. The returned string is considered to be a representation of the instance:
toString is called automatically when an instance printed, or is used within a field in an interpolated string. The string might be as generic as 'a Person', use one or
more properties to identify the instance as e.g 'Charlie Smith', or even list, or summarise all the data held by the object.
Creating a new concrete class
A new concrete class is added into your code, at global level, with the concrete class instruction.
The template includes an empty constructor, and a default implementation of toString:
+class NameName? inheritance?1
+constructor(parameter definitionsparameter definitions?)
2
new code
end constructor
+function toStringname?(parameter definitionsparameter definitions?) returns StringType?
3
return "undefined"value or expression?4
end function
end class
+class NameName? inheritance? # concrete class1
+def __init__(self: Name) -> None:
2
new code
+def toStringname?(self: Name) -> strType?:
# function method3
return "undefined"value or expression?4
main()
+class NameName? inheritance? {1
+public Name(parameter definitionsparameter definitions?) {
2
new code
}
+public stringType? toStringname?(parameter definitionsparameter definitions?) {
// function method3
return "undefined"value or expression?;4
}
}
+Class NameName? inheritance? inheritance?1
+Sub New(parameter definitionsparameter definitions?)
2
new code
End Sub
+Function toStringname?(parameter definitionsparameter definitions?) As StringType?
3
Return "undefined"value or expression?4
End Function
End Class
+class NameName? inheritance? inheritance? {1
+public Name(parameter definitionsparameter definitions?) {
2
new code
}
+public StringType? toStringname?(parameter definitionsparameter definitions?) {
// function method3
return "undefined"value or expression?;4
}
}
You must complete the template, by giving the class a name, which must start with a capital letter (because a class defines a type), followed by any alphabetic or numeric characters, or underlines.
In toString you should, at some point, replace "undefined"
"undefined"
"undefined"
"undefined"
"undefined" with something more specific.
Then via the new code prompt, which will offer you the appropriate options, add properties and methods. All properties must be initialised in the constructor,
except those of types Int
int
int
Integer
int, Float
float
double
Double
double, which take on the default values of the type - although such properties may optionally be initialised to different values.
Note in particular that properties of type String
str
string
String
String must be initialised, even if only to an empty string ""
""
""
""
"".
Inheritance
A concrete class may inherit from a single abstract class, and/or from one or more interfaces. It may not inherit from more than one abstract class. Nor may it inherit from another concrete class.
Abstract class
An abstract class may not be instantiate directly, and hence neither needs, nor allows, a constructor.
It may define the following types of member
- Property, Function method, Procedure method
- Abstract Property, abstract function, abstract
Intro
What is OOP and its advantages
Allows you to work with custom data structures that represent the domain of your application in a way that it easier to read, write, and modify.
Example: Snake OOP vs. Snake Procedural
Particularly well suited to simulations and games, graphics, or complex business applications – give examples of objects.
You can define and use objects where needed within a procedural programming, but the OOP paradigm means using the ‘object model’ as the primary design pattern.
Class and instance
The example programs
We will be referring to the following demo programs: …
Class defines type and may be thought of as a template, used to create one or more instances.
Class is defined in one place
And instantiated, and subsequently used, in other places
Example of Snake OOP
Snake
Defined here (the code within the definition is not shown here)
Instantiated here and assigned to variable named snake
Apple
Defined here (the code within the definition is not shown here)
Instantiated here and assigned to the variable named apple
Square
Instantiated many times to show each segment of the snake’s body and the apple. Whenever the snake moves a new instance of Square is added to the front (the ‘head’) and the one at the tail end is discarded.
Class definition
Adding a new class
Select class from new code menu – at global (file) level show
You must give it a name. Because a class defines a type the name must start with a capital letter. Examples of valid class names including use of camel case.
Within the class, two members are defined in the template (because every class must have them both) : a constructor and a toString method. If you look at each of the classes in Snake_OOP you will see that each has these somewhere, but with some additions/modifications.
Properties
Almost all classes define one or more properties – which define the data items. Each has a name and type. See examples
Properties are like variables, except that they are associated separately with each instance. Thus the class X might define a property y, which will contain z1 for one instance, and z2 for another.
Properties are defined within a class using the the property instruction. Example
A property of a numeric type (float or integer) will automatically be initialised to the value 0, and a Boolean type to false. All other properties must be explicitly initialised within the constructor. Whenever a value is specified for a property, it is done with a change variable instruction. In the following example the property x is initialised to ….
Or you can allow value(s to be specified as parameters in the constructor, and then these values are used within the constructor to initialise one or more properties, for example:
If the constructor defines parameters (as above) then appropriate values must be provided when you create a new instance. For example:
Then, you can read the properties of an object instance that is held in
Using property values within the toString method
ToString is a function method that returns a string, but takes no parameters. However, like other methods it can make use of the instance’s properties
To string is used:
- When printing an object e.g.
- Referencing an object within an interpolated string e.g.
By default the toString returns ‘undefined’. You can edit this so that it returns `a student` but it is often more useful more useful to return a string that is derived from one or more of the instances properties, for example:
- Returning a single identifier
- Returning type plus identifier
- Summarising all the properties
Updating properties of an object
Although you can read the value of an ordinary property from code that it outside the class (for example in main) you cannot directly update a property from outside the class. For example, if you try to write this code:
You will get the error Xxx
If you want to be able to update an property from code outside you need to write a method
Private property
What this looks like
What it means
Encapsulation and information hiding
Associations
Basic types e.g. numbers, strings, Boolean
Single association
Other UDT (class) or less-commonly another instance of the same type e.g. Best friend
Multiple association
List of value types, or of associated objects - referrerd
Optional association
Methods
A method (more completely called an ‘instance method’) is a function or procedure that is defined within a class.
These methods (unless defined as private) can be
Function methods
Think of them as queries on the object. They can return information from object’s
Procedure methods
Make changes to the object and/or depend upon the system. So they f