Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Interface

Interface is a class that contains only abstract class members.

Abstract members are those data members and member functions which are just declared but not defined. In other words, implementation of member functions is absent. Because the implementation is absent, they are known to provide abstraction.

Let’s see in what scenarios we might need such abstraction.

Hierarchy with similar structure

The simplest use case is where an entity has multiple types and each type has similar structure but different implementation.

Ex. - 2D Shapes

Let’s understand this use case with the help of 2D shapes. We can define two basic quantities for every 2D shape - area & perimeter. Different shapes like Square, Rectangle, Circle have different dimension attributes and formulas to compute area & perimeter.

Let’s write an OOPs program to represent such 2D shapes (Square, Rectangle, Circle) as objects. Each object should have the following capabilities :

  • Store the dimensions of the shape
  • Compute area & perimeter of the shape
  • Provide a summary of the shape in the format Shape(dimensions): Area = area, Perimeter = perimeter

Inheritance appoach

We can define a base class - Shape :

 open class Shape {
    open fun area() = 0
    open fun perimeter() = 0
    open fun summary() = ""
}
 

Note :

  • We cannot define fields to save the dimensions because it will be different for each shape.
  • We can declare the functions - area() and perimeter(), but can’t define them because formula for each shape is different. So, let’s return 0 for now. Similarly, we declare a dummy summary() function that returns empty string.
  • Class & its functions are defined as open for inheritance

Inheriting Shape class in Square class :

 class Square(
    val side: Int
): Shape() {

    override fun area() = side * side
    override fun perimeter() = 4 * side

    override fun summary(): String {
        return buildString {
            append("Square(side = $side)")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}
 

Representing a square using an instance of Square class :

 val square = Square(5)
println(square.summary()) // Prints "Square(side = 5): Area = 25, Perimeter = 20"
 

Problem with Inheritance

We have defined Shape as a class, so we can instantiate Shape also. Which doesn’t make any sense because it has no dimensions defined. Moreover, area() & perimeter() always return 0 and summary() returns empty string.

 // This shouldn't be allowed :
val shape = Shape()
println(shape.summary()) // Prints ""
println(shape.area()) // Prints "0"
 

So, this is a Bad design. This is where we can use Interface. Implementation (definition) of area(), perimeter() & summary() is irrelevant in Shape class, but they are declared in it just to provide a common structure to derived classes.

Interface approach

Shape Interface can be defined as :

 interface Shape {
    fun area(): Int
    fun perimeter(): Int
    fun summary(): String
}
 

Defining the Rectangle class by implementing Shape interface :

 class Rectangle(
    val l: Int, val b: Int
): Shape {

    override fun area() = l * b
    override fun perimeter() = 2 * (l + b)

    override fun summary(): String {
        return buildString {
            append("Rectangle")
            append("l = $l, b = $b")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}
 

Note that Interfaces do not have a constructor, so we don’t invoke it while implementing one :

 // Implementing Interface
class Rectangle: Shape

// Invalid :
class Rectangle : Shape()
 

Creating an instance of Rectangle class :

 val r1 = Rectangle(2, 3)
println(r1.summary()) // Prints "Rectangle(l = 2, b = 3): Area = 6, Perimeter = 10"
 

Because Interfaces do not contain implementation of class members, we can’t instantiate them like classes :

 val shape = Shape() // ERROR! This is invalid
 

This is a Good design. We have defined a basic structure of 2D Shape using Shape interface and implemented it in Rectangle class. We can instantiate Rectangle class but not Shape interface. Similar to Rectangle class, we can have more implementations:

 class Square(
    val side: Int
): Shape {

    override fun area() = side * side
    override fun perimeter() = 4 * side

    override fun summary(): String {
        return buildString {
            append("Square")
            append("(side = $side)")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}

class Circle(
    val r: Int
): Shape {

    override fun area() = (Math.PI * r * r).toInt()
    override fun perimeter() = (2 * Math.PI * r).toInt()

    override fun summary(): String {
        return buildString {
            append("Circle")
            append("(r = $r)")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}

class Triangle(
    val sides: List<Int>,
    val base: Int,
    val height: Int
): Shape {

    override fun area() = (1/2f * base * height).toInt()
    override fun perimeter() = sides.sum()

    override fun summary(): String {
        return buildString {
            append("Triangle")
            append("(sides = $sides, base = $base, height = $height)")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}
 

All implementations of Shape interface have a similar structure, so we can easily define a List<Shape> that can accomodate any implementation of Shape :

 val shapes = listOf(
    Rectangle(2, 3),
    Square(6),
    Circle(7),
    Circle(14),
    Triangle(
        sides = listOf(2, 3, 6),
        base = 6,
        height = 1
    )
)

shapes.forEach {
    println(it.summary())
}

/* Output :

Rectangle(l = 2, b = 3): Area = 6, Perimeter = 10
Square(side = 6): Area = 36, Perimeter = 24
Circle(r = 7): Area = 153, Perimeter = 43
Circle(r = 14): Area = 615, Perimeter = 87
Triangle(sides = [2, 3, 6], base = 6, height = 1): Area = 3, Perimeter = 11
 */
 

Note that creating such common list wouldn’t have been possible without a common base class or interface.

Need of a common structure

We were able to define List<Shape> and invoke its element’s summary() function only because of a common structure defined by the Shape interface.

A common structure is needed in such heirarchy of classes to perform such collective operations.

We could have eliminated Shape class altogether, but that would make things complicated when invoking summary() for elements of List<Shape>. Here is an oversimplified example to illustrate this :

 class Rectangle {
    fun summary() = "<Rectangle summary>"
}

class Square {
    fun summary() = "<Square summary>"
}

fun main() {
    val shapes = listOf(
        Rectangle(), Square()
    )

    shapes.forEach {
        println(it.summary()) // ERROR! summary() is not resolved

        // Following will work :
        println(
            when (it) {
                is Rectangle -> it.summary()
                is Square -> it.summary()
                else -> "Not a shape!"
            }
        )
    }
}
 

Conclusion

Interface provides a way to define a common structure for classes that are similar. In this example, Shape interface defines a common structure for all 2D shapes.

Basics

Defining

We can define an interface using the interface keyword :

 interface /* name */ {
		// Members
}

// Example :
interface Animal {
		fun makeSound()
}
 

Implementing

Can we create an instance of the interface and invoke its functions? Absolutely not, how can we invoke a function that’s not defined! To define the abstract members of an interface, we need to implement the interface in another class. Interfaces are always open, so we can directly implement (inherit) them :

 class /* name */ : /* InterfaceName */

// Example :
class Lion: Animal {
		override fun makeSound() {
				println("Roar...")
		}
}
 

Implementing all members of interface is compulsory. Partial implementation will not allow to code to compile.

Note that Interfaces do not have a constructor, so we don’t invoke it while implementing one :

 // Implementing Interface
class Lion: Animal

// Invalid :
class Lion: Animal()
 

Abstract data members

Similar to functions, we can also have abstract data members. Data members can only be declared and not initialized in an interface :

 // Declaring abstract data members
interface Animal {
		val name: String // Declaration only
}

// Assigning a value is not allowed!
interface Animal {
		val name: String = "Unknown" // Invalid
}
 

To implement the abstract data members in derived class, we have two options:

  • Assign values to the abstract data from constructor :

     class Tiger(
    		override val name: String
    ): Animal
    
    // Example :
    val t1 = Tiger("T-24")
     
  • Assign a value directly :

     class Tiger: Animal {
        override val name = "T-24"
    }
    
    // Example :
    fun main() {
        val t1 = Tiger()
        println(t1.name) // Prints "T-24"
    }
     

Implementing Multiple Interface

In a single class, we can not inherit from multiple classes but we can implement multiple interface.

 interface /* name */ : /* interface names separated by "," */

// Example :
interface Animal {
    fun makeSound()
}

interface Organism {
    fun eat()
}

// Tiger class implements 2 interfaces - Animal & Organism
class Tiger: Animal, Organism {
    override fun eat() {
        println("Eating meat...")
    }

    override fun makeSound() {
        println("Roar...")
    }
}
 

More use cases

Dependency Injection & IoC principle

In later modules, we will see how Interfaces help follow Inversion of Control (IoC) principle (one of the SOLID principles of Software Development) in Dependency Injection. These might seem as alien concepts. But we will discuss them in great detail in later modules.

Callbacks

In Coroutines module, we will see a legacy way of passing callbacks using Interfaces.