In this demo, you’ll take a quick look at the difference between procedural programming and object-oriented programming. You’ll be working with the Kotlin playground which is an interactive environment to run Kotlin code on the web.
You can find this at “play.kotlinlang.org” To get the starter code, open up the starter folder for this lesson. Copy the content of the file and use it to replace the code in the Kotlin playground.
This Playground has one import statement:
import java.util.UUID;
This library is used inside the TravelMode
class to generate a unique random ID for a travel mode. Its a Java class and you won’t actually be using it but this is just to demonstrate that Kotlin is interoperable with Java.
These are the two locations you’ll be using for this demo:
val lagos = Pair(6.465422, 3.406448)
val london = Pair(51.509865, -0.118092)
These are pairs that you’ll use to represent the coordinates. The first argument of the pair is the latitude, while the second argument is the longitude.
First up is an example of a procedural programming function that computes the travel time between two location values. I haven’t written the actual code, but it would compute the point-to-point distance between the from
and to
values and use some average speed, probably assuming the travel mode is driving. For now, the function just returns a double value of 42.0.
Scroll down to the main function. Uncomment the call to the computeTravelTime
function. Then update it to the following:
fun main() {
println(computeTravelTime(from = lagos, to = london)) // Updated code
}
Here, you pass in the locations to the from
and to
arguments of the computeTravelTime
function. For this demo, you’ll be using the println()
function when calling functions and methods so you can see their results in the Playground’s console.
Go ahead and run the code. You can see the result in the console.
Now, suppose the programmer decides or is told, to modify the function so it’s more accurate for other travel modes, like walking, cycling or public transport.
You could add some parameters for average speed and actual distance like so:
fun computeTravelTime(
from: Pair<Double, Double>,
to: Pair<Double, Double>,
averageSpeed: Double,
actualDistance: Double
): Double {
return actualDistance/averageSpeed
}
Making this change breaks every call to this function in the program and wherever it’s called. You now need to pass the calculation of actualDistance
and averageSpeed
arguments to the call. You probably need to include some branching code to set these new parameter values for the different travel modes.
Later, you might want to fine-tune the driving calculation to allow for traffic level and the average time to find parking. But these values aren’t relevant to the other travel modes, so you’d need to call a different function.
Okay, comment out the call to computeTravelTime()
.
Now, look at the TravelMode
class. TravelMode
is a very general concept that covers walking, cycling, driving, and public transport.
class TravelMode(private val mode: String, val averageSpeed: Double) {
val id = UUID.randomUUID().toString()
It has a primary constructor with two properties: mode
and averageSpeed
. A constructor is used to initialize a class. This is what you call when creating an object, as you’ll see shortly.
mode
could be walking, cycling, driving or public transport. After you create a TravelMode
object, the app’s code can’t access the mode
outside the class because it is marked as private
. You’ll check this for yourself soon.
You also supply the value of averageSpeed
when you create a TravelMode
object. And the averageSpeed
is an estimate that depends on the mode and user, or time-specific factors like how fast the user can walk or cycle, or whether the driving is on city streets or highways.
The extra requirement of the TravelMode
class is the id
property, which must be a unique value for each instance, and UUID.randomUUID()
takes care of this without any fuss. You don’t need to know what its actual value is, it’ll just do its job quietly.
Inside the main function, instantiate TravelMode
below the call to computeTravelTime
function like so:
val sammy = TravelMode(mode = "walking", averageSpeed = 4.5)
That’s 4.5 km/hr.
Now, see if you can access mode
:
println(sammy.mode)
Soon, you see an error message: Cannot access ‘mode’: it is private in ‘TravelMode’. This is an example of abstraction. Other parts of your app can interact with an object only through its public interface, and as you can see here, mode
is marked as private
in the class definition.
Comment out or delete the line with sammy.mode
.
Back in TravelMode
, look at the two methods:
fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
// use relevant map info for each specific travel mode
throw IllegalArgumentException("Implement this method for ${mode}.")
}
fun computeTravelTime(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
return actualDistance(from, to)/averageSpeed
}
computeTravelTime()
looks just like the original procedural programming function, but it uses the actualDistance()
method, which uses data that’s specific to each travel mode, and the averageSpeed
that was used to instantiate the TravelMode
object. Everything you need for computeTravelTime()
is encapsulated in the object, and the app’s code just needs to provide the from
and to
locations.
actualDistance()
is a placeholder function. Having a method like this means you shouldn’t create instances of this class. Instead, you should define a subclass and override this method to suit objects of that type. It’s a kind of back-door way of creating an abstract class. More on abstract classes in a future lesson.
Try calling it with your TravelMode
object:
sammy.actualDistance(lagos, london)
Click the run button to execute the Playground.
There’s an error flag, and, in the console, this message appears:
Exception in thread "main" java.lang.IllegalArgumentException: Implement this method for walking.
If you don’t want your class to be “sort of abstract,” you could implement actualDistance()
to return the exact point-to-point distance.
For now, comment out or delete this method call.
Leave actualDistance()
as a placeholder because you’re about to create some subclasses!
Add in a walking subclass like so:
class Walking(mode: String, averageSpeed: Double): TravelMode(mode, averageSpeed) {
}
Writing :TravelMode
after the class name means Walking
is a TravelMode
. It inherits all the properties and methods of TravelMode
. This is inheritance at play. You can see the Walking
class has the properties from the TravelMode
parent class, and it can also have its additional properties unique to walking. The only thing you need to write is the method that isn’t implemented in TravelMode
, and that’s the actualDistance()
method.
But before you do that, go ahead and run the Playground. You’ll get this error in the console:
This type is final, so it cannot be inherited from
This is because, in Kotlin, classes are final, which means they can’t be inherited. So, to open it up for inheritance, you have to mark the class and any of its members you wish to override with the open
keyword.
Let’s do that now:
open class TravelMode(private val mode: String, val averageSpeed: Double) {
//...
open fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
//...
}
//...
}
You marked both the class and the actualDistance()
method open
. You do this for the actualDistance()
method because you want to override and implement it in the Walking class. Let’s do that now.
Add in the following code:
override fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
// use map info about walking paths, low-traffic roads, hills
return 42.0
}
42.0 is only a placeholder value since you always have to return something. Notice you preceded the method’s definition with the override
keyword. Failure to do so would lead to an error because this new method would hide the one in the parent class.
Go ahead and instantiate your subclass and print it out to the console:
val tim = Walking(mode = "walking", averageSpeed = 6.0)
println(tim)
Tim is younger and taller than Sammy, so I guess he walks faster.
Then call that stubborn method that refused to run earlier:
println(tim.actualDistance(from = lagos, to = london))
Also call computeTravelTime()
:
println(tim.computeTravelTime(from = lagos, to = london))
Run the Playground. actualDistance
is 42, so travel time is 7 hours.
Also, you can see the walking object printed in the console with some strange values. That’s the hashcode of the object, and a hashcode is just a numeric representation of the contents of an object.
Now, go ahead and create one more subclass for the driving travel mode:
class Driving(mode: String, averageSpeed: Double): TravelMode(mode, averageSpeed) {
override fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
// use map info about roads, tollways
return 57.0
}
}
57.0 is another placeholder value, different from Walking’s 42.0.
Instantiate a Driving
object:
val car = Driving(mode = "driving", averageSpeed = 50.0)
50 km/hr is about right for averageSpeed
, as much of the distance is on a highway.
And compute its travel time:
val hours = car.computeTravelTime(from = lagos, to = london)
println(car)
println("Hours: "+ hours)
Run the Playground.
The travel time value actually isn’t far off, considering it’s using placeholder values, but it’s different from the Walking
object’s travel time, because it’s using its own version of actualDistance()
and averageSpeed
. That’s polymorphism in action! You could say the actualDistance()
behavior exists in different forms depending on the type of travel mode.
So far, Driving
works the same as Walking
. How do you set it up to use traffic level and parking time?
Instead of overriding computeTravelTime()
, you overload it. That is, you add parameters to create a different function signature. A Driving
object can call this version of computeTravelTime()
, but no other subclass type can.
Let’s add that in:
fun computeTravelTime(from: Pair<Double, Double>,
to: Pair<Double, Double>,
traffic: Double,
parking: Double): Double {
return actualDistance(from, to)/averageSpeed * traffic + parking
}
You can see it has a different method signature from its parent class. This is called method overloading.
Alright, let’s try it out.
Add the following code under the print statement:
val realHours = car.computeTravelTime(
from = lagos,
to = london,
traffic = 1.2,
parking = 0.5
)
println("Actual Hours: " + realHours)
Run the Playground.
As you’d expect, travel time is now more than the inherited method’s calculation due to the new factors like traffic and parking time.
Do note that the hours here are in decimal values. So for example, if you multiply the realHours
by 60, you’ll get the time in minutes.
Now, just check to see if tim
can call this method:
Start typing…
tim.c
Nope, only the TravelMode
method appears in the menu.
If you force it, by copy-pasting, then running the Playground, you get two errors that says kotlin can find the traffic and parking parameters in the method’s definition.
Go ahead and delete this call.
That ends this demo. Continue with the lesson for a summary.