Introduction

One of the foundational concepts of object-oriented programming is Inheritance. It aids in creating a "is-a" relationship between things. For instance, a dog "is-a" mammal and a car "is-a"  vehicle. On the other hand, Composition is an idea in object-oriented programming that aids in creating a "has-a" relationship. As implied by the name, it combines instances of the same object. For instance, a dog "has-a" barking capacity, just as a car "has-a" engine. The same issue of code repetition is addressed by both Inheritance and Composition, albeit in different ways. While Composition begins with little specialized classes that are joined to make a general class, Inheritance begins with a large generic class that is then broken up into smaller specialized classes.

Limitations of Inheritance

Let us take a look at an example showcasing the limitation of Inheritance. Suppose we have a Mammal class representing mammals. It can have methods such as BreastFeed(). We create sub-classes for each mammal, such as Cat, Dog, etc., and then there comes the Platypus, an egg-laying mammal - an outlier. Our Mammal class doesn't have any method to lay an egg. If we add such a method to the base class, all other mammals would also have to lay an egg. One could say that we can add a method specific to that Platypus class to lay an egg, but remember, there are other faunas with egg-laying capabilities, such as Reptiles and Birds, which would have egg-laying methods defined in their classes. In the end, we will end with code duplication, the very problem inheritance was supposed to solve.

class Mammal {
    public void BreastFeed() { }
}

class Reptile {
    public void LayEgg() { }
}

class Bird {
    public void LayEgg() { }
    
    public void Fly() { }
}

class Platypus : Mammal {
    public void LayEgg() { }	
}

class Bat : Mammal {
    public void Fly() { }
}
Code repetition in Inheritance

Looking at the example above, we can see how there still is some level of code repetition when using Inheritance.

Now let's see some of the major flaws of Inheritance.

Code Bloating

When a child class inherits from an existing class, a contract is created that the child class must abide by, which requires it to copy all of the parent class's available attributes, regardless of whether they apply to the child class.

Tight Coupling

After creating a flawless object hierarchy, a little modification to a base class—let's say, the addition or deletion of a method or property—will affect all of our child classes, their child classes, and so on. We'll have to rewrite a sizable portion of the code. Changes are not helpful to Inheritance, especially when things are scaled.

Harder to Test

Tight coupling between the parent and child classes can make it harder to test since we now have to better understand the implementation of the parent class.

Rigidity

Inheritance creates a fixed hierarchy of classes. Once finalized, we will find it hard to change the existing implementations.

Why Composition?

Now that we're done blasting Inheritance let's consider why Composition is a better alternative. Let's continue with our Platypus example. Instead of Inheritance, let's use Composition to define the animals.

interface IBreastFeeder {
    void BreastFeed();
}

interface IReproducer {
    void Reproduce();
}

interface IFlyer {
    void Fly();
}

public class Reproducer : IReproducer
{
    void Reproduce(string name)
    {
        Console.WriteLine($"{name} is giving birth");
    }
}

public class EggLayer : IReproducer
{
    public void Reproduce(string name)
    {
        Console.WriteLine($"{name} is laying egg");
    }
}

public class Flyer : IFlyer
{
    public void Fly(string name)
    {
        Console.WriteLine($"{name} is flying");
    }
}

public class BreastFeeder : IBreastFeeder
{
    public void BreastFeed(string name)
    {
        Console.WriteLine($"{name} is breastfeeding");
    }
}

public class Platypus
{
    private IBreastFeeder BreastFeeder { get; }
    private IReproducer EggLayer { get; }
    private String Name { get; } = "Platypus";

    public Platypus(
        IBreastFeeder breastFeeder,
        IReproducer eggLayer
    )
    {
        this.BreastFeeder = breastFeeder;
        this.EggLayer = eggLayer;
    }

    public void LayEgg()
    {
        this.EggLayer.Reproduce(this.Name);
    }

    public void BreastFeed()
    {
        this.BreastFeeder.BreastFeed(this.Name);
    }
}

public class Bat
{
    private IFlyer Flyer { get; }
    private IBreastFeeder BreastFeeder { get; }
    private IReproducer Reproducer { get; }
    private String Name { get; } = "Bat";

    public Bat(
        IFlyer flyer,
        IBreastFeeder breastFeeder,
        IReproducer reproducer
    )
    {
        this.Flyer = flyer;
        this.BreastFeeder = breastFeeder;
        this.Reproducer = reproducer;
    }

    public void Fly()
    {
        this.Flyer.Fly(this.Name);
    }

    public void BreastFeed()
    {
        this.BreastFeeder.BreastFeed(this.Name);
    }

    public void GiveBirth()
    {
        this.Reproducer.Reproduce(this.Name);
    }
}

public class Program
{
    public static void Main(String[] args)
    {
        var platypus = new Platypus(
            new BreastFeeder(),
            new EggLayer()
        );
        platypus.LayEgg();
        platypus.BreastFeed();

        var bat = new Bat(
            new Flyer(),
            new BreastFeeder(),
            new Reproducer()
        );
        bat.Fly();
        bat.BreastFeed();
        bat.GiveBirth();
    }
}
Solving the Mammal example given above using Composition

In the above example, we described the interface for individual behaviour. Then, we wrote classes that implement those interfaces. We can see that EggLayer and Reproducer classes both implement IReproducer interfaces but have different implementations. Interfaces have allowed us to swap implementations as and when needed. Now, let us look at the Platypus class. I composed the class with different behaviours that the Platypus can have. The constructor accepts implementations for the class. We can use different implementations as we need with this approach. There's a special term for what we did in the constructor. It's called a Dependency Injection. Then, we implemented class-specific methods LayEgg and BreastFeed for the platypus class, which actually is an abstraction over the actual implementation. Similarly, we composed the Bat class with its specific behaviour.

Initially, it might look like we wrote more code than we would write when using Inheritance, but in the long run, we will have to change a lesser amount of code when changes need to be made. Suppose we need to change an implementation for the code for a fly; we would just write a new one and swap it out with the old one without changing any other piece of code.

Composition brings the following advantages to the table:

Flexibility

You don't have to worry about breaking something when you change something in your code. Implementations can be swapped without major changes.

Easy to Test

Since you can swap implementations in and out, you can write mock implementations anytime when you are writing tests for your code.

Loose Coupling

Any changes you make are bound to that class, and will have fewer chances to break other classes. Changing core implementations might break dependent classes, but you would not be doing that very often.

Should You Stop Using Inheritance For Composition?

The answer to this query is ambiguous. Composition offers flexibility, but it also has a lot of boilerplate. For small-scale applications where little modifications are necessary, Inheritance can be acceptable. It might even be the most effective strategy for the given task. However, I believe Composition to be a preferable strategy for scalability. You should research the ideal strategy before beginning a project.

Conclusion

In conclusion, Composition provides flexibility and loose coupling when designing complex systems. It should be preferred over Inheritance when systems are prone to changes. It also makes testing code easier since you can swap out implementations when needed.