by Richard Russell, February 2015
Version 3.00 of LB Booster (LBB) incorporates some extensions to support Object Oriented Programming (OOP). I should emphasise that the use of these extensions is entirely optional, and their presence should not impair compatibility with non-OOP Liberty BASIC programs. But please don't stop reading now: OOP has a lot to offer!
Many people find the whole idea of Object Oriented Programming unfamiliar, new-fangled and frightening. They tend to flinch at the very mention of it! This is unfortunate because OOP is intended to make programming easier and result in more reliable and easy-to-maintain code.
The basic concept behind OOP is that of encapsulation. This is one of those long words which can seem rather intimidating, but all it really means is keeping related things together. The idea is that programs consist of code which operates on data - the code only makes sense in association with the data on which it operates, and the data is only useful in association with the code which operates on it.
In traditional programming languages there is usually no way of 'bundling together' related code and data: the data (typically numbers, strings and arrays) are declared somewhere near the start of the program - or worse, scattered throughout it - and the subroutines and functions which work on that data are somewhere near the end. The programmer knows which subroutine/function is related to what data (at least, he once did!) but this may not be at all obvious to somebody else trying to understand how the program works.
Object Oriented Programming has the concept of a class. A class is just a bundled collection of data along with the code which operates on it (the data are often referred to as the properties of the class and the code, consisting of subroutines and functions, as the methods of the class). Not only does the class bundle the related items together, it (typically) isolates the data from being accessed from outside the class - rather as variables (other than GLOBALS) referenced inside a function or subroutine are inaccessible from outside.
Hopefully you can see how this is helpful. It is obvious at a glance what data is related to what code, and the 'methods' can only operate on the 'properties' in the same class (see later for how classes can be combined). This makes the code more understandable and more reliable, and allows modifications to be made with more confidence.
But this is all rather theoretical, and it may be easier to understand by way of example. Imagine that in a program we want to represent a vehicle; all vehicles have certain properties in common, these might be things like the speed with which it is moving. We also want to be able to perform certain operations on that vehicle, such as increase its speed (accelerate) or decrease its speed (decelerate).
In Object Oriented Liberty BASIC (OOLB) we can define a class to represent a vehicle as follows:
CLASS Vehicle DIM speed SUB accelerate rate speed += rate END SUB SUB decelerate rate speed -= rate END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS
The properties of the class are listed in one or more DIM statements; they may include numbers, strings and arrays (we will see later that it's also possible to include 'child' classes). The methods of the class provide the means to manipulate those properties, and a means to discover the value of a property (if it needs to be known outside the class).
So we have a simple class which represents a vehicle. But in the program there may be several different vehicles, with different speeds and other properties. This is where the real power of Objects comes in. The 'class' is like a template: it describes a vehicle in general. An 'object' is a specific case of that class (it's usually called an instance of the class) and describes a particular vehicle.
In OOLB we create an object (an instance of a class) as follows:
NEW MyVehicle1 AS Vehicle
The object MyVehicle1 has a speed (and other properties, if any) independent of any other objects in the program. Let's put all this together into our first complete Object Oriented Liberty BASIC program:
NEW MyVehicle1 AS Vehicle CALL MyVehicle1::accelerate 10 PRINT MyVehicle1::get.speed() CALL MyVehicle1::accelerate 5 PRINT MyVehicle1::get.speed() CALL MyVehicle1::decelerate 10 PRINT MyVehicle1::get.speed() DISCARD MyVehicle1 END CLASS Vehicle DIM speed SUB accelerate rate speed += rate END SUB SUB decelerate rate speed -= rate END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS
Here we have introduced a couple of things you haven't seen before. Firstly, to call a method (subroutine or function) in an object we use the familiar LB syntax but the name consists of the name of the object followed by two colons (this is called the scope resolution operator) and then the name of the method within that object, i.e. Object::Method.
Secondly, when we have finished with an object we DISCARD it. This frees the memory resources which were allocated to the object with NEW.
Some methods may be intended to be called only from other methods within the same class, rather than from outside the class. It is possible to specify such methods as PRIVATE in which case an attempt to call them from outside the class will fail at run-time. Here is an example:
NEW MyVehicle1 AS Vehicle CALL MyVehicle1::accelerate 10 PRINT MyVehicle1::get.speed() CALL MyVehicle1::accelerate 5 PRINT MyVehicle1::get.speed() CALL MyVehicle1::decelerate 10 PRINT MyVehicle1::get.speed() DISCARD MyVehicle1 END CLASS Vehicle DIM speed SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS
Here, instead of the accelerate and decelerate methods altering the speed value directly, they each call a PRIVATE method adjust which does so. Note the syntax for calling a method in the same class: specify the object name as this.
You will notice that in the class Vehicle the value of speed is not initialised. Just as with ordinary LB variables and arrays, the value is assumed to be zero or an empty string if not previously set. Of course we could have provided an explicit method for initialising the speed, but there is another approach.
It is possible to incorporate a special method called the constructor. The constructor takes the form of an ordinary subroutine but instead of being deliberately executed using CALL it is automatically called when the object is created. The constructor is distinguished by having a name which is the same as the name of the class. Let's modify our program accordingly:
NEW MyVehicle1 AS Vehicle CALL MyVehicle1::accelerate 10 PRINT MyVehicle1::get.speed() CALL MyVehicle1::accelerate 5 PRINT MyVehicle1::get.speed() CALL MyVehicle1::decelerate 10 PRINT MyVehicle1::get.speed() DISCARD MyVehicle1 END CLASS Vehicle DIM speed SUB Vehicle ' constructor speed = 2 END SUB SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS
Here the speed is automatically initialised to 2 when the object is created.
Suppose we want to be able to initialise the speed to a different value for each instance of the class. Again we could do that by calling a method, but it's also possible to pass one or more parameters to the constructor:
NEW MyVehicle1 AS Vehicle 3 ' parameter passed to constructor CALL MyVehicle1::accelerate 10 PRINT MyVehicle1::get.speed() CALL MyVehicle1::accelerate 5 PRINT MyVehicle1::get.speed() CALL MyVehicle1::decelerate 10 PRINT MyVehicle1::get.speed() DISCARD MyVehicle1 END CLASS Vehicle DIM speed SUB Vehicle init ' constructor with parameter speed = init END SUB SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS
You probably won't be surprised to learn that as well as a constructor, which is called automatically when an object is created, we can also have a destructor which is called automatically when the object is discarded. A destructor has a name consisting of a tilde (~) followed by the name of the class; in this case there is nothing useful for it to do so we will just print a message:
NEW MyVehicle1 AS Vehicle 3 ' parameter passed to constructor CALL MyVehicle1::accelerate 10 PRINT MyVehicle1::get.speed() CALL MyVehicle1::accelerate 5 PRINT MyVehicle1::get.speed() CALL MyVehicle1::decelerate 10 PRINT MyVehicle1::get.speed() DISCARD MyVehicle1 END CLASS Vehicle DIM speed SUB Vehicle init ' constructor with parameter speed = init END SUB SUB ~Vehicle ' destructor print "Destructor called" END SUB SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS
We have seen how to create a class which represents a vehicle, but there are different kinds of vehicles (cars, bicycles etc.). Suppose we want to create a class to represent a bicycle; we could start from scratch and define the properties and methods that a bicycle needs. But a better way is to recognise that a bicycle is a kind of vehicle, so any property or method relevant to a vehicle should also be relevant to a bicycle - although a bicycle may have properties and methods of its own.
We can do that using a variation of the CLASS statement as follows:
CLASS Bicycle INHERITS Vehicle
The INHERITS keyword specifies that the class Bicycle inherits all the properties and methods of the class Vehicle. You can then specify additional properties and additional methods which are needed by a bicycle but not by vehicles in general. Here I have chosen the current gear and the gear ratios:
NEW MyBicycle1 AS Bicycle 2,3,5,7,10 ' parameters passed to constructor CALL MyBicycle1::change.up PRINT MyBicycle1::get.ratio() CALL MyBicycle1::accelerate 10 PRINT MyBicycle1::get.speed() DISCARD MyBicycle1 END CLASS Vehicle DIM speed SUB Vehicle init ' constructor with parameter speed = init END SUB SUB ~Vehicle ' destructor print "Destructor called" END SUB SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS CLASS Bicycle INHERITS Vehicle DIM gear, ratio(5) SUB Bicycle r1, r2, r3, r4, r5 ' constructor with five parameters ratio(1) = r1 ratio(2) = r2 ratio(3) = r3 ratio(4) = r4 ratio(5) = r5 gear = 1 speed = 3 END SUB SUB change.up IF gear < 5 THEN gear += 1 END SUB SUB change.down IF gear > 1 THEN gear -= 1 END SUB FUNCTION get.ratio() get.ratio = ratio(gear) END FUNCTION END CLASS
The order of declaration is important: ancestor classes must be declared before their descendant classes.
As we have seen, when a class INHERITS another class it acquires the properties and methods of its ancestor class. However it is still possible to declare a method in the descendant class which has the same name as one in the ancestor class. In that case the declaration in the descendant class takes precedence. For example:
NEW MyBicycle1 AS Bicycle 2,3,5,7,10 ' parameters passed to constructor CALL MyBicycle1::change.up PRINT MyBicycle1::get.ratio() CALL MyBicycle1::accelerate 10 PRINT MyBicycle1::get.speed() DISCARD MyBicycle1 END CLASS Vehicle DIM speed SUB Vehicle init ' constructor with parameter speed = init END SUB SUB ~Vehicle ' destructor print "Destructor called" END SUB SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS CLASS Bicycle INHERITS Vehicle DIM gear, ratio(5) SUB Bicycle r1, r2, r3, r4, r5 ' constructor with five parameters ratio(1) = r1 ratio(2) = r2 ratio(3) = r3 ratio(4) = r4 ratio(5) = r5 gear = 1 speed = 3 END SUB SUB change.up IF gear < 5 THEN gear += 1 END SUB SUB change.down IF gear > 1 THEN gear -= 1 END SUB SUB accelerate rate speed += rate * ratio(gear) END SUB FUNCTION get.ratio() get.ratio = ratio(gear) END FUNCTION END CLASS
Here I have re-defined the accelerate method so that it takes account of the gear ratio.
As described in the previous section, inheritance represents an 'is a' relationship: a bicycle is a vehicle. A related concept is that of containment, which represents the 'has a' relationship. For example a bicycle has wheels. A class can only inherit from one ancestor class, but it can contain several child classes.
Here is an example. We will first declare a new class Wheel which has a property diameter, a method to set its value, and a method to get its value:
CLASS Wheel DIM diameter SUB set.diameter d diameter = d END SUB FUNCTION get.diameter get.diameter = diameter END FUNCTION END CLASS
Note that a method which sets the value of a property is sometimes called a setter and a method which gets the value of a property a getter; together they are known as accessors.
We can now add some wheels to our class Bicycle:
NEW MyBicycle1 AS Bicycle 2,3,5,7,10 ' parameters passed to constructor CALL MyBicycle1::change.up CALL MyBicycle1::accelerate 10 PRINT MyBicycle1::get.speed() PRINT MyBicycle1::get.wheel.diameter(1) DISCARD MyBicycle1 END CLASS Vehicle DIM speed SUB Vehicle init ' constructor with parameter speed = init END SUB SUB ~Vehicle ' destructor print "Destructor called" END SUB SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS CLASS Wheel DIM diameter SUB set.diameter d diameter = d END SUB FUNCTION get.diameter get.diameter = diameter END FUNCTION END CLASS CLASS Bicycle INHERITS Vehicle DIM gear, ratio(5), Front AS Wheel, Rear AS Wheel SUB Bicycle r1, r2, r3, r4, r5 ' constructor with five parameters ratio(1) = r1 ratio(2) = r2 ratio(3) = r3 ratio(4) = r4 ratio(5) = r5 gear = 1 speed = 3 CALL Front::set.diameter 12 CALL Rear::set.diameter 12 END SUB SUB change.up IF gear < 5 THEN gear += 1 END SUB SUB change.down IF gear > 1 THEN gear -= 1 END SUB SUB accelerate rate speed += rate * ratio(gear) END SUB FUNCTION get.ratio() get.ratio = ratio(gear) END FUNCTION FUNCTION get.wheel.diameter(wheel) SELECT CASE wheel CASE 1: get.wheel.diameter = Front::get.diameter() CASE 2: get.wheel.diameter = Rear::get.diameter() END SELECT END FUNCTION END CLASS
Note that to call a method in a contained class the scope resolution operator is once again used, i.e. Child::Method.
Up to now we have instantiated objects individually, but it is possible to instantiate an array of objects. For example if we have five bicycles we can use an array for them:
NEW MyBikes(4) AS Bicycle CALL MyBikes(2)::change.up CALL MyBikes(2)::accelerate 10 PRINT MyBikes(2)::get.speed() PRINT MyBikes(2)::get.wheel.diameter(1) DISCARD MyBikes() END CLASS Vehicle DIM speed SUB Vehicle init ' constructor with parameter speed = init END SUB SUB ~Vehicle ' destructor print "Destructor called" END SUB SUB accelerate rate CALL this::adjust rate END SUB SUB decelerate rate CALL this::adjust -rate END SUB PRIVATE SUB adjust adj speed += adj END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS CLASS Wheel DIM diameter SUB set.diameter d diameter = d END SUB FUNCTION get.diameter get.diameter = diameter END FUNCTION END CLASS CLASS Bicycle INHERITS Vehicle DIM gear, ratio(5), Front AS Wheel, Rear AS Wheel SUB Bicycle ' default constructor ratio(1) = 2 ratio(2) = 3 ratio(3) = 5 ratio(4) = 7 ratio(5) = 10 gear = 1 speed = 3 CALL Front::set.diameter 12 CALL Rear::set.diameter 12 END SUB SUB change.up IF gear < 5 THEN gear += 1 END SUB SUB change.down IF gear > 1 THEN gear -= 1 END SUB SUB accelerate rate speed += rate * ratio(gear) END SUB FUNCTION get.ratio() get.ratio = ratio(gear) END FUNCTION FUNCTION get.wheel.diameter(wheel) SELECT CASE wheel CASE 1: get.wheel.diameter = Front::get.diameter() CASE 2: get.wheel.diameter = Rear::get.diameter() END SELECT END FUNCTION END CLASS
There is one limitation of this technique: you cannot pass parameters to the constructor. The default constructor (a constructor with no parameters) will still be called however, and in the above program the necessary initialisation has been done there.
OOP languages commonly allow you to have multiple methods with the same name but with different signatures; in this context a 'signature' means the number of parameters and their types. LBB supports this too, although only the number of parameters is distinguished. One use for this facility is to have multiple constructors; which constructor is called will depend on how many parameters are specified in the NEW statement:
NEW MyVehicle1 AS Vehicle NEW MyVehicle2 AS Vehicle 5 PRINT MyVehicle1::get.speed() PRINT MyVehicle2::get.speed() DISCARD MyVehicle1 DISCARD MyVehicle2 END CLASS Vehicle DIM speed SUB Vehicle ' constructor with no parameters speed = 2 END SUB SUB Vehicle s ' constructor with one parameter speed = s END SUB SUB accelerate rate speed += rate END SUB SUB decelerate rate speed -= rate END SUB FUNCTION get.speed get.speed = speed END FUNCTION END CLASS
Note that if you supply a default constructor, which takes no parameters, this will always be called, even if another constructor is called as well.