Dive into Object-Oriented Programming with Kotlin
When learning to write Kotlin for the first time, you aren’t just learning how to string together complex chains of seemingly arcane symbols, you are actually learning how to represent problems in a way for the computer to understand. Yet, people need to understand the code as well. Yet, what is “good” code?
Throughout the years, certain patterns and techniques have evolved in the developer community. Some of these concepts have been incorporated directly into a language while other techniques and best practices are used in conjunction with these language features. For this reason, understanding how to structure and write your code is just as important as learning the syntax and keywords.
In the following excerpt, Emmanuel Okiche covers the concepts of abstract classes and interfaces in Kotlin. You’ll learn how and why to use these language constructs in your own code. In the process, you’ll gain a preview of Kodeco’s Object-Orientated Programming with Kotlin course.
Abstract Classes
Sometimes, you may want to prevent a class from being instantiated but still be able to be inherited from. This will let you define properties and behavior common to all subclasses. Such a parent class is called an abstract class. These classes can’t be instantiated, meaning you can’t create an object of an abstract class. You can think of these classes as templates for other classes: just base style, configurations, and functionality guidelines for a specific design. The template can’t run directly in your app. Instead, your app can make use of the template.
Classes declared with the abstract
keyword are open
by default and can be inherited from. In abstract classes, you can also declare abstract methods marked with abstract
that have no body. The abstract methods must be overridden in subclasses. Since the main reason for abstract classes is for other classes to extend them, they can’t be private
or final
. Though, their methods and properties are final by default, unless you make them abstract
, which makes them open
for overriding.
Take a look at this:
abstract class Animal { abstract val name: String // Abstract Property } abstract class Mammal(val birthDate: String): Animal() { // Non-Abstract Property (birthDate) abstract fun consumeFood() // Abstract Method abstract val furColor: List<String> // Abstract Property // Non-Abstract Method fun someMammalMethod() { println("Non abstract function") } } class Human(birthDate: String): Mammal(birthDate) { // Abstract Property (Must be overridden by Subclasses) override val name = "Human" // Abstract Property (Must be overridden by Subclasses) override val furColor = listOf("brown", "black") // Abstract Method (Must be implemented by Subclasses) override fun consumeFood() { // ... } // Member method created by this class (Not Inherited) fun createBirthCertificate() { // ... } }
Here, you have Animal and Mammal classes, which are both abstract, and the Mammal class inherits from Animal. We also have the Human class which inherits from Mammal.
It might look like a lot is happening in the code above, but it’s simpler than you think. Here’s the breakdown:
- The Animal class is an abstract class that has one abstract property; name. This means that the subclasses must override it.
- Next, you have the Mammal abstract class that extends the Animal class, which means that Mammal is-a Animal.
- It has a mixture of both abstract and non-abstract members. Abstract classes can have non-abstract members.
- The name property from the Animal parent class isn’t overridden here. But that’s ok—Mammal is an abstract class too, so it just means that name must be implemented somewhere down the line in the inheritance tree. Otherwise, you’ll get an error.
- The Human class extends the Mammal class, which means that Human is-a Mammal.
- It overrides the name property from the Animal class, which was passed down by Mammal.
- It also overrides Mammal abstract members and creates its own
createBirthCertificate()
method.
Now, see what happens when you try to create an instance of each of these:
val human = Human("1/1/2000") val mammal = Mammal("1/1/2000") // Error: Cannot create an instance of an abstract class
Remember, abstract classes can’t be instantiated, and that’s why trying to instantiate Mammal causes an error.
Now, abstract classes are cool, but Kotlin doesn’t support multiple inheritance. This means that a class can only extend one parent class. So, a class can only have one is-a relationship. This can be a bit limiting depending on what you want to achieve. This leads us to the next construct, “Interfaces.”
Using Interfaces
So far, you’ve been working with the custom type, Class. You’ve learned about inheritance and how a class can extend an abstract and non-abstract class that are related. Another very useful custom type is Interfaces.
Interfaces simply create a contract that other classes can implement. Remember, you imagined abstract classes as website or mobile templates above, and this means we can’t use more than one template for the app at the same time. Interfaces can be seen as plugins or add-ons which add a feature or behavior to the app. An app can have only one template but can have multiple plugins connected to it.
A class can implement multiple interfaces, but the classes that implement them must not be related. You could say that interfaces exhibit the is relationship rather than the is-a relationship. Another thing to note is that most interfaces are named as adjectives, although this is not a rule. For example, Pluggable, Comparable, Drivable. So you could say a Television class is Pluggable or a Car class is Drivable. Remember, a class can implement multiple interfaces, so the Car class can be Drivable and at the same time Chargeable if it’s an electric car. Same thing with a Phone is Chargeable even though Car and Phone are unrelated.
Now, imagine you have two classes Microwave and WashingMachine. These are different electrical appliances, but they have one thing in common, they both need to be connected to electricity to function. Devices that connect to electricity always have some important things in common. Let’s push these commonalities to an interface.
Take a look at how you could do this:
interface Pluggable { // properties in interfaces cannot maintain state val neededWattToWork: Int // this won't work. would result in an error because of the reason above // val neededWattToWork: Int = 40 //Measured in Watt fun electricityConsumed(wattLimit: Int) : Int fun turnOff() fun turnOn() } class Microwave : Pluggable { override val neededWattToWork = 15 override fun electricityConsumed(wattLimit: Int): Int { return if (neededWattToWork > wattLimit) { turnOff() 0 } else { turnOn() neededWattToWork } } override fun turnOff() { println("Microwave Turning off...") } override fun turnOn() { println("Microwave Turning on...") } } class WashingMachine : Pluggable { override val neededWattToWork = 60 override fun electricityConsumed(wattLimit: Int): Int { return if (neededWattToWork > wattLimit) { turnOff() 0 } else { turnOn() neededWattToWork } } override fun turnOff() { println("WashingMachine Turning off...") } override fun turnOn() { println("WashingMachine Turning on...") } }
You can see that the Pluggable interface creates a contract that all classes implementing it must follow. The members of the interface are abstract by default, so they must be overridden by subclasses.
Note: Properties in interfaces can’t maintain their state, so initializing it would result in an error.
Also, interfaces can have default method implementation. So turnOn could have a body like so:
fun turnOn() { println("Turning on...") }
Let’s say the WashingMachine subclass doesn’t override it. Then you have something like this:
val washingMachine = WashingMachine() washingMachine.turnOn() // Turning on...
The output will be “Turning on…” because it was not overridden in the WashingMachine class.
When an interface defines a default implementation, you can still override the implementation in a class that implements the interface.