Inheritance is a core part of Object Orientated programming that provides a powerful code reuse tool by grouping properties and behaviors into common classes (called base classes) and having unique properties and behaviors placed in specific classes that grow out from the base class (called child classes). The child class receives all properties and behavior from its parent class, but it also contains properties and behaviors that are unique to itself. Inheritance allows for specialization of software components when a component has a specific need, but also allows for generalization when using items common to the parent.
Let’s consider an example that is more specific. Suppose we have a Vehicle class that models some sort of vehicle that we can drive. We start by creating a class.
open class Vehicle(
private val make : String,
private val model : String,
private val year : Int) {
fun start() = println("Starting up the motor")
fun stop() = println("Turning off the engine")
fun park() = println("Parking " + toString())
open fun drive() = println("Driving " + toString())
open fun reverse() = println("Reversing " + toString())
override fun toString(): String {
return "${year} ${make} ${model}"
}
}
We know that all Vehicles have a make, model, and year. Users of a Vehicle can start and turn off the motor. They can also park, drive, or put the Vehicle in reverse. So far so good. However, later on, we need a vehicle that can tow a camper.
We could add a tow() method to Vehicle, but would that really make sense? What if our Vehicle is a Fiat? Would we really tow a camper with a Fiat? What we need is a Truck. Thanks to Inheritance, we don’t need to write Truck from scratch. We can simply create a specialized version of a Vehicle instead.
open class Truck(make: String,
model: String,
year: Int,
private val towCapacity : Int) : Vehicle(make, model, year) {
fun tow () = println("${toString()} is towing ${this.towCapacity} lbs")
}
This code creates a Truck class based off of Vehicle. As such, the Truck still has a make, model, and year. It can also start(), stop(), drive(), park(), and reverse() just like any other vehicle. However, it can also tow things and has a towCapacity property. What we have essentially done is reused all of the code from Vehicle and just changed what was needed so that we have a new Vehicle like object that also tows things.
Of course, later on, our needs change again and we decide to go camping in the mountains. It may snow in the mountains, so in addition to being able to tow things, we may want four wheel drive. Once again, not all Trucks have four wheel drive, so we don’t want to add a four wheel drive into Truck. As a matter of fact, four wheel drive isn’t even a specific behavior. What it really does is it enhances the already existing behaviors drive and reverse.
Let’s create another child class, based off of Truck, and specialize the driving behavior.
class FourWheelDrive(make: String, model: String, year: Int, towCapacity: Int) :
Truck(make, model, year, towCapacity) {
var fourByFour = false
override fun drive() {
if(fourByFour){
println("Driving ${toString()} in four wheel drive")
} else {
super.drive()
}
}
override fun reverse() {
if(fourByFour){
println("Reversing ${toString()} in four wheel drive")
} else {
super.drive()
}
}
}
In this class, we are modifing the already existant behaviors of driving and going in reverse. If the truck has four wheel drived turned on, the output of the program reflects this fact. One the other hand, if four wheel drive is turned off, the methods call super.drive(), which means use the behavior defined in Truck (which bubbles up to the original behavior in Vehicle). Thus, FourWheelDrive has specialized behaviors that were originally found in Vehicle. This is known as overriding behaviors.
Now let’s do a demonstration
fun main(args : Array<String>){
val car = Vehicle("Fiat", "500", 2012)
val truck = Truck("Chevy", "Silverado", 2017, 8000)
val fourWheelDrive = FourWheelDrive("Dodge", "Ram", 2017, 8000)
//drive() comes from Vehicle
car.drive()
println()
//There is no drive() in Truck, but it
//inherited the behavior from Vehicle
truck.drive()
println()
//FourWheelDrive override drive() to customize it
fourWheelDrive.drive()
println()
println("Turn on four wheel drive")
fourWheelDrive.fourByFour = true
fourWheelDrive.drive()
}
Output
Driving 2012 Fiat 500
Driving 2017 Chevy Silverado
Driving 2017 Dodge Ram
Turn on four wheel drive
Driving 2017 Dodge Ram in four wheel drive
Three vehicles are made at the beginning of main: car, truck, and fourWheelDrive. They are all of type Vehicle, but Truck is a specialized case of Vehicle and fourWheelDrive is a specialized case of Truck. As such, all three objects have a drive() method which we use in the example program. When fourWheelDrive turns on fourByFour and then invokes drive, the console prints out that it is driving in four wheel drive.
Sealed Class
Although Kotlin supports inheritance, it’s use is discouraged. In order to use inheritance in Kotlin, the ‘open’ keyword needs to be added in front of the ‘class’ keyword first. If the ‘open’ keyword is ommitted, the class is considered to be final and the compiler will not allow the class to be used as a parent class. Likewise, all functions in a class also have to be marked as ‘open’ (see drive and reverse in vehicle), otherwise, overriding behaviors is not permitted.
Why would Kotlin choose to do this while other languages encourage inheritence? After studying issues found with inheritance, it became clear that many developers wrote classes without considering that a class may be extended later on. When changes where made in the parent class, the child classes could potentially break as well. This ended up creating a situation called “fragile base classes”.
Kotlin designers decided that by making developers mark classes as open, it would encourage developers to think about the needs of child classes when working on a base class. Kotlin also has powerful delegation mechanisms that encourage developers to use composition and delegation as code reuse mechanisms rather than inheritance.
Example program
Here is the entire source code used in this post
//Has to be marked as open to allow inheritance
open class Vehicle(
private val make : String,
private val model : String,
private val year : Int) {
fun start() = println("Starting up the motor")
fun stop() = println("Turning off the engine")
fun park() = println("Parking " + toString())
//Has to be marked as open to allow overriding
open fun drive() = println("Driving " + toString())
//Has to be marked as open to allow overriding
open fun reverse() = println("Reversing " + toString())
override fun toString(): String {
return "${year} ${make} ${model}"
}
}
//Has to be marked as open for inheritance
open class Truck(make: String,
model: String,
year: Int,
private val towCapacity : Int) : Vehicle(make, model, year) {
fun tow () = println("${toString()} is towing ${this.towCapacity} lbs")
}
//This class is not open and therefore cannot be inherited from
class FourWheelDrive(make: String, model: String, year: Int, towCapacity: Int) :
Truck(make, model, year, towCapacity) {
var fourByFour = false
//The override keyword signals to the compiler that we are overriding
//the drive() method
override fun drive() {
if(fourByFour){
println("Driving ${toString()} in four wheel drive")
} else {
super.drive()
}
}
//The override keyword signals to the compiler that we are overriding
//the reverse() method
override fun reverse() {
if(fourByFour){
println("Reversing ${toString()} in four wheel drive")
} else {
super.drive()
}
}
}
fun main(args : Array<String>){
val car = Vehicle("Fiat", "500", 2012)
val truck = Truck("Chevy", "Silverado", 2017, 8000)
val fourWheelDrive = FourWheelDrive("Dodge", "Ram", 2017, 8000)
//drive() comes from Vehicle
car.drive()
println()
//There is no drive() in Truck, but it
//inherited the behavior from Vehicle
truck.drive()
println()
//FourWheelDrive override drive() to customize it
fourWheelDrive.drive()
println()
println("Turn on four wheel drive")
fourWheelDrive.fourByFour = true
fourWheelDrive.drive()
}
Like this:
Like Loading...