The simplest use case is where an entity has multiple types and each type has similar structure but different implementation.
Ex. - 2D Shapes
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
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()
andperimeter()
, but can’t define them because formula for each shape is different. So, let’s return0
for now. Similarly, we declare a dummysummary()
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
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
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
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
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.