Object-oriented programming (OOP) is a popular programming paradigm that allows developers to create and manage complex software systems by organizing data and behavior into reusable, modular structures called classes. One of the key features of OOP is inheritance, which allows classes to inherit properties and behaviors from other classes, creating a hierarchical relationship between them.
What is Inheritance?
Inheritance is a fundamental concept in OOP, allowing a new class to be based on an existing class (or classes) and inherit its properties and behaviors. The existing class is called the superclass or base class, and the new class is called the subclass or derived class. The subclass inherits all the properties and behaviors of the superclass and can also add its own unique properties and behaviors.
data class Vehicle(val make: String, val model: String, val year: Int)
data class Car(val style: String, val vehicle: Vehicle): Vehicle(vehicle.make, vehicle.model, vehicle. Year)
Here, the Car
class extends the Vehicle
class. The Car
class inherits all the properties and behaviors of the Vehicle
class, including the make
, model
, and year
properties. The Car
class also adds its own unique property, style
.
What is Multiple Inheritance?
Multiple inheritance is a feature of some programming languages that allows a subclass to inherit from multiple superclasses. In other words, a class can have more than one direct superclass. This can be useful in certain situations, such as when creating complex hierarchies of related classes or when reusing code from multiple sources.
C++ is a programming language that supports multiple inheritance. In C++, a class can inherit from multiple superclasses using a comma-separated list of class names in the class
definition. For example:
class Vehicle {
public:
void startEngine() {
cout << "Engine started." << endl;
}
};
class Car {
public:
void drive() {
cout << "Car is being driven." << endl;
}
};
// SportsCar inherits from both Vehicle and Car
class SportsCar : public Vehicle, public Car {
public:
void turbo() {
cout << "Turbo is activated." << endl;
}
};
In this example, Vehicle
and Car
are two classes that define common behaviors of a car. The SportsCar
class inherits from both Vehicle
and Car
classes, and defines its own unique behavior, turbo()
. By inheriting from both superclasses, the SportsCar
class can access and use all the properties and methods of both classes.
Why Kotlin (and Java) don’t support Multiple Inheritance?
Kotlin (and Java) do not support multiple inheritance for several reasons:
Ambiguity
One of the biggest problems with multiple inheritance is the issue of ambiguity. If a class inherits from two or more superclasses that have the same method or property name, the subclass may not know which superclass to use. This can cause conflicts and make the code difficult to maintain.
For example, suppose we have a Shape
class with a draw()
method, and a Drawable
interface with its own draw()
method:
data class Shape(val x: Int, val y: Int) {
fun draw() {
println("Drawing a shape")
}
}
interface Drawable {
fun draw()
}
If we try to create a Rectangle
class that inherits from both Shape
and Drawable
, we will run into ambiguity when implementing the draw()
method:
data class Rectangle(val width: Int, val height: Int, val shape: Shape): Shape(shape.x, shape.y), Drawable {
override fun draw() {
// Which draw() method should we use?
}
}
Complexity
Multiple inheritance can also lead to complex and hard-to-understand code. As the number of superclasses increases, it becomes more difficult to keep track of all the properties and behaviors that the subclass inherits. This can make the code harder to read, write, and maintain.
Diamond Problem
Another issue with multiple inheritance is the diamond problem. The diamond problem occurs when a class inherits from two or more superclasses that themselves inherit from a common superclass. This creates a diamond-shaped hierarchy, which can cause conflicts and make the code difficult to maintain.
The “diamond problem” is an ambiguity that arises when two classes B and C inherit from A, and class D inherits from both B and C. If there is a method in A that B and C have overridden, and D does not override it, then which version of the method does D inherit: that of B, or that of C?
For example, suppose we have a Vehicle
class and a Machine
class that both inherit from a Transport
class:
open class Transport {
// ...
}
open class Vehicle: Transport() {
// ...
}
open class Machine: Transport() {
// ...
}
Now, suppose we want to create a new class called CarMachine
that inherits from both Vehicle
and Machine
. In a language that supports multiple inheritance, we could write:
class CarMachine: Vehicle(), Machine() {
// ...
}
However, this creates a diamond-shaped hierarchy, with CarMachine
inheriting from both Vehicle
and Machine
, which both inherit from Transport
. This can cause conflicts and make the code difficult to maintain.
Alternatives for multiple inheritance
Interface Inheritance
One alternative to multiple inheritance is interface inheritance. In Kotlin, an interface is a collection of abstract methods and properties that can be implemented by a class. By implementing multiple interfaces, a class can inherit properties and behaviors from multiple sources.
For example, suppose we have a Shape
interface with a draw()
method, and a Movable
interface with a move()
method:
interface Shape {
fun draw()
}
interface Movable {
fun move()
}
Now, suppose we want to create a new class called MovingRectangle
that is both a Shape
and a Movable
. We could implement this using interface inheritance:
data class MovingRectangle(val x: Int, val y: Int, val width: Int, val height: Int): Shape, Movable {
override fun draw() {
println("Drawing a rectangle")
}
override fun move() {
println("Moving a rectangle")
}
}
Composition
Another alternative to multiple inheritance is composition. Composition involves creating a new class that contains instances of other classes and delegates behavior to them.
For example, suppose we have a Shape
class and a Mover
class:
data class Shape(val x: Int, val y: Int
data class Mover(val x: Int, val y: Int) {
fun move() {
println("Moving to x=$x y=$y")
}
}
One way to implement composition is to define a ShapeMover
class that contains instances of both Shape
and Mover
and delegates behavior to them:
class ShapeMover(private val shape: Shape, private val mover: Mover) {
fun draw() {
shape.draw()
}
fun move() {
mover. Move()
}
}
Here, the ShapeMover
class contains instances of both Shape
and Mover
, and delegates the draw()
and move()
methods to them.
We can then create a MovingRectangle
class that uses the ShapeMover
class to combine the behavior of both:
data class MovingRectangle(val x: Int, val y: Int, val width: Int, val height: Int) {
private val shape = Shape(x, y)
private val mover = Mover(x, y)
private val shapeMover = ShapeMover(shape, mover)
fun draw() {
shapeMover.draw()
}
fun move() {
shapeMover.move()
}
}
Extension Functions
Finally, another alternative to multiple inheritance is extension functions. Extension functions allow us to add behavior to existing classes without modifying their source code.
For example, suppose we have a Shape
class:
data class Shape(val x: Int, val y: Int)
Now, suppose we want to add a draw()
method to the Shape
class without modifying its source code. We can do this using an extension function:
fun Shape.draw() {
println("Drawing a shape")
}
Here, we define an extension function called draw()
that takes a Shape
instance as a receiver, and adds behavior to it.
We can then use the draw()
method on any Shape
instance, as if it were a method defined in the Shape
class itself:
val shape = Shape(10, 20)
shape.draw() // prints "Drawing a shape"
Conclusion
While multiple inheritance can be a powerful tool, it also comes with significant drawbacks and complexities. Kotlin (and Java) avoid these issues by not supporting multiple inheritance, but offer several alternative approaches such as interface inheritance, composition, and extension functions. By using these approaches, we can write clearer, more maintainable code that avoids the pitfalls of multiple inheritance.