Why Kotlin Ditched Multiple Inheritance: A Look at Alternative Approaches

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.

Share with your friends

Leave a Reply

Your email address will not be published. Required fields are marked *