Abstraction is one of the major components of OOP. When we abstract, we are hiding the internal working details of something from its user. The user only cares about the controls that operate an object, but how the object acts on the controls are of no concern to the user.
A common everyday abstraction that people use daily can be found in a smart phone’s operating system. When a user wishes to make a phone call, they do not worry about how the phone makes a call. All the user cares about is using the keypad to dail a phone number and then pressing the call button. The details of connecting to the cell phone tower and then routing the phone call through the phone network are of no concern to the user. Those details have been abstracted.
Kotlin provides a variety of ways to provide abstraction. In the example below, I used the interface feature to model a Vehicle
interface Vehicle { fun park() fun drive() fun reverse() fun start() fun shutDown() }
This code defines an abstraction point for all Vehicles. It guarantees that all classes that implement Vehicle have the following behaviors: park, drive, reverse, start, and shutDown. However, what we do not have is details as to how the Vehicle drives, parks, etc. As a matter of fact, the function bodies of all of the methods inside of vehicle are left empty (they are called abstract methods).
We may wish to take our vehicle for a drive. When we drive our vehicle, we are only really concerned with what the vehicle can do. We don’t care how it parks or goes in reverse. Let’s see this example in terms of code.
fun takeForDrive(v : Vehicle){ with(v){ //How we start is abstracted. We only care that the vehicle starts, but //we don't care about how it starts. start() //Likewise, we only care that it goes in reverse(). How it goes in reverse //is irrelevant here. reverse() //And so on... drive() park() shutDown() } }
Notice how the takeForDrive function calls all five of our behaviors on the supplied Vehicle object. It doesn’t even know what kind of a vehicle it is driving. The Vehicle could be a car, Truck, airplane, boat, etc. None of that matters to the takeForDrive function. The details are hidden behind the Vehicle interface (in other words, abstracted).
One of the reasons abstraction is so important is that it promotes code reusability and maintainability. For example, now that we have this takeForDrive function, we can use any object that implements Vehicle. So for example, we can create a Truck class that implements Vehicle.
class Truck : Vehicle { override fun park() = println("Truck is parking") override fun drive() = println("Truck is driving") override fun reverse() = println("Truck is in reverse") override fun start() = println("Truck is starting") override fun shutDown() = println("Truck is shutting down") }
and now we can take the Truck for a drive.
val truck = Truck() takeForDrive(truck)
The price of gas may spike later one and we may choose to drive something that is more efficient. As long as our new mode of transportation implements the Vehicle interface, we can take it for a drive. Here is a car class that impelements Vehicle.
class Car : Vehicle{ override fun park() = println("Car is parking") override fun drive() = println("Car is driving") override fun reverse() = println("Car is in reverse") override fun start() = println("Car is starting") override fun shutDown() = println("Car is shutting down") }
Just like with truck, we can drive the car.
val car = Car() takeForDrive(car)
Since Vehicle provides an abstraction point, any code that accepts Vehicle as a parameter can use Truck or Car. The function takeForDrive can be said to be loosely coupled to Truck and Car because it indirectly accepts Trucks or Cars using the Vehicle interface. This makes the takeForDrive function highly reusable to other components that may need to get developed in the future.
Example Program
Here is a fully working Kotlin program that ties everything together.
package ch1 //This defines our public interface for all vehicles interface Vehicle { fun park() fun drive() fun reverse() fun start() fun shutDown() } //Our Truck class provides an implementation of Vehicle class Truck : Vehicle { override fun park() = println("Truck is parking") override fun drive() = println("Truck is driving") override fun reverse() = println("Truck is in reverse") override fun start() = println("Truck is starting") override fun shutDown() = println("Truck is shutting down") } //Car provides an alternative implementation of Vehicle class Car : Vehicle{ override fun park() = println("Car is parking") override fun drive() = println("Car is driving") override fun reverse() = println("Car is in reverse") override fun start() = println("Car is starting") override fun shutDown() = println("Car is shutting down") } /** * This function demonstrates Abstraction. Notice how it accepts a Vehicle object but * makes no distinction if it's a Truck or a Car. The details of how the vehicle parks, * drives, reverses, starts, or shuts down are abstracted from this function. In the end, we are * only concerned with what the Vehicle object does, not how it does it. */ fun takeForDrive(v : Vehicle){ with(v){ //How we start is abstracted. We only care that the vehicle starts, but //we don't care about how it starts. start() //Likewise, we only care that it goes in reverse(). How it goes in reverse //is irrelevant here. reverse() //And so on... drive() park() shutDown() } } fun main(args : Array<String>){ //Create a new Truck and take it for a drive. It works because Truck //implements the Vehicle Interface which abstracts the truck's details from //the takeForDrive function takeForDrive(Truck()) //Likewise, we can also take a car for a drive. The car class also implements //Vehicle so takeForDrive can also use cars. takeForDrive(Car()) }
When run, the program prints
Truck is starting Truck is in reverse Truck is driving Truck is parking Truck is shutting down Car is starting Car is in reverse Car is driving Car is parking Car is shutting down