Kotlin Open & Abstract Classes and How Inheritance Works

In object-oriented programminginheritance enables new objects to take on the properties of existing objects. We can have class that can inherit properties and methods from the parent class, when we inherit classes, we do not need to specify the method and properties again, in the new class (child class or sub class). In essence it allow us to avoid code duplication (important) and make our code more flexible and maintainable (also very important).

By default, classes in kotlin are final i.e they cannot be inherited from. This makes sense because not all classes created will need to be inherited or opened

Why do we Inherit classes?

Let's say, we wanted to create a class for Student and Employee.

What do student and employee have in common?
  1. they both need to eat
  2. they both sleep
  3. they both move
  4. they both need to bath
  5. they both have a name
  6. the both have hair (no matter how small)
  7. they both breath air
  8. they both have birthdays
  9. they both ... You get the idea?
now, our code will be too cumbersome and hard to maintain if we defined all of those common functionalities in individual Student and Employee classes.

[box type="info" align="" class="" width=""]When coding always follow the DRY (Don't Repeat Yourself) principle. A rule of thumb is to create a class to define the common attributes and functions of the Objects (in this case Student and Employee) your trying to model and then inherit from it[/box]

Differences between a Student and an Employee
  • An Employee gets paid, a student is not
  • An Employee doesn't need to study for exam, a student has to
  • An Employee can buy Expensive things, a student, probably not
  • A student has a Matriculation or ID number, an Employee? probably not
  • ....
The difference between the objects (student and employee) is what we should include in our individual classes (good practice).

Instead of writing our code like this :
class Example{ 
  fun main (args: Array<String { 
    val employee = Employee("Joe", 32) 
    println(employee.yearOfBirth())
    val student = Student("Jack", 22) 
    println(student.yearOfBirth()) 
  } 
}
//a lot of duplicate code
class Student(val name: String, var age: Int) {
    fun move() {
        println("$name moved!")
    }
    fun greet(name : String){
     println("Good Morning $name)
    }
    fun yearOfBirth() = Calendar.getInstance().get(Calendar.YEAR) - age
    fun birthday() {
        age++
    }
}
class Employee(val name: String, var age: Int) {
    fun move() {
        println("$name moved!")
    }
    fun greet(name : String){
     println("Good Morning $name)
    } 
    fun yearOfBirth() = Calendar.getInstance().get(Calendar.YEAR) - age
    fun birthday() {
        age++
    }
}
[box type="warning" align="" class="" width=""]the above code is hard to maintain and can not scale because there are too many duplicate codes which perform the exact same function. If we need to modify something, we would need to modify it in both classes. the situation gets worse if we have  many functions and variables[/box]
we write like this:
open class Person(open val name: String, open var age: Int){ 
// open declares our class as a class we can inherit from
    fun move() {
        println("$name moved!")
    }
    fun greet(name : String){
        println("Good Morning $name")
    }
    fun yearOfBirth() = Calendar.getInstance().get(Calendar.YEAR) - age
    fun birthday() {
        age++
    }
}
class Employee(override val name: String, override var age: Int) : Person(name, age){
// inheriting is done after the colon
// we also have to call the constructor of the parent class
}
class Student(override val name: String, override var age: Int) : Person(name, age){
// inheriting is done after the colon
}
class Example{
    fun main (args: Array<String>){
        val employee = Employee("Joe", 32)
        println(employee.yearOfBirth())
        val student = Student("Jack", 22)
        println(student.yearOfBirth())
    }
}

As you can see from above we've eliminated a lot of spaghetti (duplicate) code and saved our self heart ache of maintainability and scaling.

we overrode the variables name and age (in the constructor), so that the name and age in the constructor of super class (Person) can be unified. such when we change age for example it will reflect in both the child class and parent class. The code below demonstrate the concept succinctly.
class Example{
 fun main(args: Array<String>){
 val student = StudentEx("Carter", 17)
 student.print() // Name = Carter, Age = 17
 student.studentAge = 23 // age changed in the StudentEx class but print() function is in PersonEx class
 student.print()// Name = Carter, Age = 17
 student.printStudentAge() // 'Student Age = 23'
 student.age = 23 //age changed in the PersonEx class and print() function is in PersonEx class
 student.print() // Name = Carter, Age = 23

 val employee = EmployeeEx("Anthony", 28)
 employee.print() // Name = Anthony, Age = 28
 employee.employeeAge = 31 // age changed in the EmployeeEx class but print() function is in Person class
 employee.print() // 'Name = Anthony, Age = 28' values remain unchanged
 employee.printEmployeeAge() // 'Employee Age = 31'
 employee.age = 31 //age changed in the PersonEx class and print() function is in PersonEx class
 employee.print() // 'Name = Anthony, Age = 31'
 employee.printEmployeeAge()
 }
}
class StudentEx(val studentName :String, var studentAge: Int): PersonEx(studentName, studentAge){
 fun printStudentAge(){
 println("Student Age = $studentAge")
 }
}
class EmployeeEx(val employeeName :String, var employeeAge: Int): PersonEx(employeeName, employeeAge){
 fun printEmployeeAge(){
 println("Employee Age = $employeeAge")
 }
}

open class PersonEx (open val name: String, open var age:Int){
 fun print() {
 println("Name = $name, Age = $age")
 }
}

 // Utilizing open and override keywords eliminates boiler plate code
class Example{
    fun main(args: Array){
        val student = StudentEx("Carter", 17)
        student.print() // Name = Carter, Age = 17
        student.age = 23 // age changed in both StudentEx and Person class
        student.print()// Name = Carter, Age = 23
        student.printStudentAge() // 'Student Age = 23'
        student.age = 24 //age changed in both StudentEx and Person class
        student.print() // Name = Carter, Age = 24

        val employee = EmployeeEx("Anthony", 28)
        employee.print() // Name = Anthony, Age = 28
        employee.age = 31 // age changed in both EmployeeEx and Person class
        employee.print() // 'Name = Anthony, Age = 31' 
        employee.printEmployeeAge() // 'Employee Age = 31'
        employee.age = 32 // age changed in both EmployeeEx and Person class
        employee.print() // 'Name = Anthony, Age = 32'
        employee.printEmployeeAge()
    }
}
class StudentEx(override val name :String, override var age: Int): PersonEx(name, age){
    fun printStudentAge(){
        println("Student Age = $age")
    }
}
class EmployeeEx(override val name :String, override var age: Int): PersonEx(name, age){
    fun printEmployeeAge(){
        println("Employee Age = $age")
    }
}
open class PersonEx (open val name: String, open var age:Int){
    fun print() {
        println("Name = $name, Age = $age")
    }
}



Keywords:
  1. 'open' Keyword is used on either variable or class we want to inherit
  2. 'override' let us override properties in our parent class
Note:
  1. In order to inherit a class, you must declare the class as open or abstract
  2. each property we may want to override should be declared open or abstract
  3. condition  2 goes for methods too.
class InheritanceExample {
    fun main(args: Array<String>) {
        val studentObject = Student("Romeo", 20, 167289)
        val employeeObject = Employee("Joe", 31, "Quality Assurance Officer")
        
        employeeObject.move() // method in parent class
        studentObject.move() // method in parent class
        studentObject.writeExam() // method in child class
        employeeObject.getPaid() // method in child class
    }
}

class Student(override val name: String,
              override var age: Int,
              val matricNo: Long) : Person(name, age) { 
// A student has an identification number
    fun writeExam() { // A student has to write exams
        println("Boy, this exam is simple")
    }
}

class Employee(override val name: String, override var age: Int, var jobTitle: String) : Person(name, age) {
// A job usually has a title    
    fun getPaid() { // An employee get's paid
        println("Yay it's pay day")
    }
}
open class Person(open val name: String, open var age: Int) {
    fun move() {
        println("$name just moved!")
    }
    fun eat() {
        println("$name is eating!")
    }
    fun sleep() {
        println("$name is sleeping!")
    }
    fun yearOfBirth() = Calendar.getInstance().get(Calendar.YEAR) - age

    fun birthday() {
        age++
    }
}
open keyword with Methods

the reason we might want to override a method ( using the open keyword) is that there may be some similarities between the functions of a student and an Employee, for example a student can change college level while an employee can get promoted, these are similarities but they are manifested a little differently. An Employee might get a double promotion while a student must go through a level at a time. For that reason we can simply create a function incrementLevel and override it in our child class and define the behavior we want for the object.

Methods declared open have a default implementation (i.e they have a method body), which would be used if the method was not overridden. In essence overriding a method declared open is optional.
class InheritanceExample {
 fun main(args: Array<String>) {
 val studentObject = Student("Romeo", 20, 167289, 1)
 studentObject.incrementLevel() // Yay! Student Romeo with ID 167289 is now in Year 2
 val employeeObject = Employee("Joe", 31, "Quality Assurance Officer", 5)
 employeeObject.incrementLevel() // Romeo Level has changed
 }
}

class Student(override val name: String,
              override var age: Int,
              val matricNo: Long,
              var academicYear: Int) : Person(name, age) {
    fun writeExam() {
        println("Boy, this exam is Simple!")
    }
    override fun incrementLevel() {
        println("Yay! Student $name with ID $matricNo is now in Year ${academicYear++} ")
    }
}

class Employee(override val name: String, override var age: Int, var jobTitle: String, var jobLevel: Int) : Person(name, age) {
    fun getPaid() {
        println("Yay it's pay day")
    }

    override fun incrementLevel() {
        println("Yay! $name has been promoted to Level ${jobLevel++} ")
    }
}

open class Person(open val name: String, open var age: Int) {
    fun move() {
        println("$name moved!")
    }

    fun yearOfBirth() = Calendar.getInstance().get(Calendar.YEAR) - age

    open fun incrementLevel() {
        println("$name Level has changed") // This will be executed if method is not overrode
    }
}
Abstract Methods

1 - Abstract methods on the other hand do not have a method body at all.

2 - Abstract methods demand that their classes be declared abstract.

3 - Classes declared 'abstract' can be inherited but must implement abstract method in sub class

4 - Abstract methods must be overridden in any class that inherits its class.

An example of abstract method is a method called study. A student needs to study to pass an exam while an Employee needs to study where he/she works, to improve it's efficiency, productivity and bring revenue. Even though they are both named study they perform wildly different implementations. For this we can use abstract methods. Since study is crucial to the success of both of them (they both must study or get FIRED! ) in their fields, abstract method is the way to go.
class InheritanceExample {
    fun main(args: Array<String>) {
        val studentObject = Student("Romeo", 20, 167289, 1)
        studentObject.study() // This's where we Implement what Study means to a Student
        val employeeObject = Employee("Joe", 31, "Quality Assurance Officer", 5)
        employeeObject.study() // Here's where we Implement what Study means to an Employee
    }
}
class Student(override val name: String,
              override var age: Int,
              val matricNo: Long,
              var academicYear: Int) : Person(name, age) {

    override fun study() {
        println("This's where we Implement what Study means to a Student")
    }
}

class Employee(override val name: String, override var age: Int, var jobTitle: String, var jobLevel: Int) : Person(name, age) {
    override fun study() {
        println("Here's where we Implement what Study means to an Employee")
    }
}

abstract class Person(open val name: String, open var age: Int) {
    fun move() {
        println("$name moved!")
    }

    open fun incrementLevel() {
        println("$name Level has changed")
    }

    abstract fun study() // abstract method, no method body 
}

Conclusion

putting it all together :
class InheritanceExample {
    fun main(args: Array<String>) {
        //instatiating object
        val studentObject = Student("Romeo", 20, 167289, 1) 
        val employeeObject = Employee("Joe", 31, "Quality Assurance Officer", 5)
        
        studentObject.study() // This's where we Implement what Study means to a Student
        studentObject.incrementLevel() // prints 'Yay! Student Joe with ID 167289 is now in Year 1' from Student class
        studentObject.writeExam() // method in child class
        employeeObject.study() // Here's where we Implement what Study means to an Employee
        employeeObject.getPaid() // method in child class
        employeeObject.incrementLevel() // prints 'Joe level has changed' from the parent class of Employee
    }
}
class Student(override val name: String,
              override var age: Int,
              val matricNo: Long,
              var academicYear: Int) : Person(name, age) {
    fun writeExam() { // A student has to write exams
        println("Boy, this exam is simple")
    }
    override fun incrementLevel() {
        println("Yay! Student $name with ID $matricNo is now in Year ${academicYear++}")
    }
    override fun study() {
        println("This's where we Implement what Study means to a Student")
    }
}

class Employee(override val name: String, override var age: Int, var jobTitle: String, var jobLevel: Int) : Person(name, age) {
    override fun study() {
        println("Here's where we Implement what Study means to an Employee")
    }
    fun getPaid() {
        println("Yay it's pay day")
    }
}

abstract class Person(open val name: String, open var age: Int) {
    fun move() {
        println("$name just moved!")
    }
    fun eat() {
        println("$name is eating!")
    }
    fun sleep() {
        println("$name is sleeping!")
    }
    fun yearOfBirth() = Calendar.getInstance().get(Calendar.YEAR) - age

    fun birthday() = age++

    open fun incrementLevel() {
        println("$name Level has changed")
    }
    
    // abstract method, no method body
    abstract fun study() 
}
[box type="note" align="" class="" width=""]Hey there! Thanks for reading through! Please use the comment section below to post your suggestions, comments or reviews. You noticed an error? please let me know! am in the process of becoming a better writer and developer. Thanks, Love You![/box]

Comments

Popular posts from this blog

How to select Multiple Item in RecyclerView in Android

Android Clipboard Listener - How to Know when User copies a text in Android

Kotlin ArrayList and Loops Example