Composition vs. inheritance: a quick reminder for everyday .NET code

Posted: (EET/GMT+2)

 

Inheritance is one of the first object-oriented (OOP) features we learn, so it's natural that we sometimes start using it quickly .NET code, without much thought. But for most everyday scenarios, composition provides a cleaner and more flexible structure. During the years, I've learned to think about this as follows. When I'm adding a new class, I ask myself: "Am I really expressing an 'is-a' relationship, or am I just trying to reuse behaviour?".

Here's a tiny example about reporting (common enough to appear in many applications). Suppose you start with a base class and then extend from it:

public class ReportBase
{
    public void WriteHeader()
    {
        Console.WriteLine("=== Report ===");
    }
}

public class SalesReport : ReportBase
{
    public void WriteBody()
    {
        Console.WriteLine("Sales data...");
    }
}

This works, but it also locks the SalesReport class into the structure of the ReportBase base class. If you later want to change how the header works, or reuse the header logic differently, you end up fighting the inheritance chain.

With composition, the structure stays simpler:

public class ReportHeader
{
    public void Write()
    {
        Console.WriteLine("=== Report ===");
    }
}

public class SalesReport
{
    private readonly ReportHeader _header = new ReportHeader();

    public void Write()
    {
        _header.Write();
        Console.WriteLine("Sales data...");
    }
}

The pieces are now separate, easier to replace, and easier to extend. You can also swap ReportHeader for another implementation in tests or for different output formats.

(You could even use dependency injection [loose coupling] about which I wrote few months ago.)

As a quick rule of thumb for .NET code in everyday work:

  • Use inheritance when something genuinely is-a more specific version of something else.
  • Use composition when you want to build behaviour from smaller parts.

It's a tiny mindset change, but it leads to code that's easier to extend, test, and understand long after the initial implementation.