In this article, I want to explore the differences between two common object-oriented programming concepts: inheritance and composition. I will also discuss when to use each one and how to avoid some common pitfalls.
Understanding Inheritance
Inheritance is a fundamental object-oriented programming (OOP) concept that is a way of creating new classes from existing ones. The new class inherits all the properties and methods of the parent class, and can also add its own. Inheritance is useful when we want to model a "is-a" relationship between classes. For example, a Dog class can inherit from an Animal class, because a dog is an animal.
Here is a C# example of inheritance:
// Define a base class
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void Eat()
{
Console.WriteLine($"{Name} is eating.");
}
public void Sleep()
{
Console.WriteLine($"{Name} is sleeping.");
}
}
// Define a derived class
public class Dog : Animal
{
public string Breed { get; set; }
public void Bark()
{
Console.WriteLine($"{Name} is barking.");
}
}
// Create an instance of the derived class
Dog dog = new Dog();
dog.Name = "Rex";
dog.Age = 3;
dog.Breed = "German Shepherd";
// Call methods from both classes
dog.Eat();
dog.Sleep();
dog.Bark();
The output of this code is:
Rex is eating.
Rex is sleeping.
Rex is barking.
We can see that the Dog class inherits the Name, Age, Eat and Sleep properties and methods from the Animal class, and also adds its own Breed and Bark properties and methods.
When to Use and Not to Use Inheritance
Inheritance is a powerful way of reusing code and creating hierarchies of classes. So, Use inheritance when you want to model an "is-a" relationship, where one class is a specialized version of another. For example, a Dog is a specific type of Animal. In such cases, inheritance can lead to clean and intuitive code. However, it also has some drawbacks. One of them is that inheritance creates a tight coupling between classes, which means that any change in the parent class can affect the child classes. Another drawback is that inheritance can lead to over-generalization and confusion. For example, what if we want to create a Bird class that inherits from Animal? Should we also inherit the Eat and Sleep methods? What if we want to create a Penguin class that inherits from Bird? Should we also inherit the Fly method?
Understanding Composition
Composition is another OOP concept where a class is constructed by combining several objects from other classes. Composition is based on the idea of "has-a" relationship between classes. For example, a Car class has an Engine class, a Wheel class, a Door class, etc. Composition is useful when we want to model complex objects that are composed of simpler objects.
Here is a C# example of composition:
// Define a simple class
public class Engine
{
public int HorsePower { get; set; }
public int Cylinders { get; set; }
public void Start()
{
Console.WriteLine("Engine started.");
}
public void Stop()
{
Console.WriteLine("Engine stopped.");
}
}
// Define another simple class
public class Wheel
{
public int Size { get; set; }
public string Type { get; set; }
public void Rotate()
{
Console.WriteLine("Wheel rotating.");
}
public void Brake()
{
Console.WriteLine("Wheel braking.");
}
}
// Define a complex class that uses composition
public class Car
{
// Declare fields that hold references to other objects
private Engine engine;
private Wheel[] wheels;
// Use constructor to initialize the fields with new objects
public Car(int horsePower, int cylinders, int wheelSize, string wheelType)
{
engine = new Engine();
engine.HorsePower = horsePower;
engine.Cylinders = cylinders;
wheels = new Wheel[4];
for (int i = 0; i < 4; i++)
{
wheels[i] = new Wheel();
wheels[i].Size = wheelSize;
wheels[i].Type = wheelType;
}
}
// Define methods that delegate the behavior to the composed objects
public void Start()
{
engine.Start();
foreach (Wheel wheel in wheels)
{
wheel.Rotate();
}
Console.WriteLine("Car started.");
}
public void Stop()
{
foreach (Wheel wheel in wheels)
{
wheel.Brake();
}
engine.Stop();
Console.WriteLine("Car stopped.");
}
}
// Create an instance of the complex class
Car car = new Car(200, 4, 17, "All-season");
car.Start();
car.Stop();
The output of this code is:
Engine started.
Wheel rotating.
Wheel rotating.
Wheel rotating.
Wheel rotating.
Car started.
Wheel braking.
Wheel braking.
Wheel braking.
Wheel braking.
Engine stopped.
Car stopped.
We can see that the Car class is composed of an Engine object and an array of Wheel objects. The Car class does not inherit any properties or methods from the Engine or Wheel classes, but rather uses them as components. The Car class also defines its own methods that delegate the behavior to the composed objects.
When to Use and Not to Use Composition
Composition is a flexible way of creating new classes from existing ones, Use composition when you want to model a "has-a" relationship. It allows us to create complex objects that are loosely coupled and easy to modify. However, it also has some drawbacks. One of them is that composition can lead to code duplication and complexity. For example, what if we want to create a Truck class that is similar to the Car class, but has more wheels and a different engine? Should we copy and paste the code from the Car class and modify it? What if we want to create a Motorcycle class that has only two wheels and a different engine? Should we create a new class from scratch?
To avoid these problems, we can use a combination of inheritance and composition. For example, we can create an abstract Vehicle class that inherits from the Object class and defines some common properties and methods for all vehicles. Then, we can create subclasses of Vehicle, such as Car, Truck, Motorcycle, etc., that inherit the common properties and methods from Vehicle and also use composition to define their specific components.
Here is a C# example of using both inheritance and composition:
// Define an abstract base class
public abstract class Vehicle
{
// Declare some common properties
public string Color { get; set; }
public int Speed { get; set; }
// Declare some abstract methods
public abstract void Start();
public abstract void Stop();
public abstract void Accelerate();
public abstract void Decelerate();
// Define some common methods
public void Honk()
{
Console.WriteLine("Beep beep!");
}
public void Paint(string newColor)
{
Color = newColor;
Console.WriteLine($"Vehicle painted {newColor}.");
}
}
// Define a simple Composition class
public class Engine
{
public int HorsePower { get; set; }
public int Cylinders { get; set; }
public void Start()
{
Console.WriteLine("Engine started.");
}
public void Stop()
{
Console.WriteLine("Engine stopped.");
}
}
// Define another simple Composition class
public class Wheel
{
public int Size { get; set; }
public string Type { get; set; }
public void Rotate()
{
Console.WriteLine("Wheel rotating.");
}
public void Brake()
{
Console.WriteLine("Wheel braking.");
}
}
// Define a derived class that uses composition
public class Car : Vehicle
{
// Declare fields that hold references to other objects
private Engine engine;
private Wheel[] wheels;
// Use constructor to initialize the fields with new objects
public Car(int horsePower, int cylinders, int wheelSize, string wheelType)
{
engine = new Engine();
engine.HorsePower = horsePower;
engine.Cylinders = cylinders;
wheels = new Wheel[4];
for (int i = 0; i < 4; i++)
{
wheels[i] = new Wheel();
wheels[i].Size = wheelSize;
wheels[i].Type = wheelType;
}
}
// Override the abstract methods from the base class
public override void Start()
{
engine.Start();
foreach (Wheel wheel in wheels)
{
wheel.Rotate();
}
Console.WriteLine("Car started.");
}
public override void Stop()
{
foreach (Wheel wheel in wheels)
{
wheel.Brake();
}
engine.Stop();
Console.WriteLine("Car stopped.");
}
public override void Accelerate()
{
Speed += 10;
Console.WriteLine($"Car accelerated to {Speed} km/h.");
}
public override void Decelerate()
{
Speed -= 10;
Console.WriteLine($"Car decelerated to {Speed} km/h.");
}
}
// Define another derived class that uses composition
public class Motorcycle : Vehicle
{
// Declare fields that hold references to other objects
private Engine engine;
private Wheel[] wheels;
// Use constructor to initialize the fields with new objects
public Motorcycle(int horsePower, int cylinders, int wheelSize, string wheelType)
{
engine = new Engine();
engine.HorsePower = horsePower;
engine.Cylinders = cylinders;
wheels = new Wheel[2];
for (int i = 0; i < 2; i++)
{
wheels[i] = new Wheel();
wheels[i].Size = wheelSize;
wheels[i].Type = wheelType;
}
}
// Override the abstract methods from the base class
public override void Start()
{
engine.Start();
foreach (Wheel wheel in wheels)
{
wheel.Rotate();
}
Console.WriteLine("Motorcycle started.");
}
public override void Stop()
{
foreach (Wheel wheel in wheels)
{
wheel.Brake();
}
engine.Stop();
Console.WriteLine("Motorcycle stopped.");
}
public override void Accelerate()
{
Speed += 20;
Console.WriteLine($"Motorcycle accelerated to {Speed} km/h.");
}
public override void Decelerate()
{
Speed -= 20;
Console.WriteLine($"Motorcycle decelerated to {Speed} km/h.");
}
}
// Create instances of the derived classes
Car car = new Car(200, 4, 17, "All-season");
car.Speed = 20;
car.Color = "Blue";
car.Start();
car.Paint("Red");
car.Accelerate();
car.Decelerate();
car.Honk();
car.Stop();
Motorcycle motorcycle = new Motorcycle(80, 1, 15, "All-season");
motorcycle.Speed = 20;
motorcycle.Color = "Blue";
motorcycle.Start();
motorcycle.Paint("Red");
motorcycle.Accelerate();
motorcycle.Decelerate();
motorcycle.Honk();
motorcycle.Stop();
The output of this code is:
Engine started.
Wheel rotating.
Wheel rotating.
Wheel rotating.
Wheel rotating.
Car started.
Vehicle painted Red.
Car accelerated to 30 km/h.
Car decelerated to 20 km/h.
Beep beep!
Wheel braking.
Wheel braking.
Wheel braking.
Wheel braking.
Engine stopped.
Car stopped.
Engine started.
Wheel rotating.
Wheel rotating.
Motorcycle started.
Vehicle painted Red.
Motorcycle accelerated to 40 km/h.
Motorcycle decelerated to 20 km/h.
Beep beep!
Wheel braking.
Wheel braking.
Engine stopped.
Motorcycle stopped.
Conclusion
Inheritance and composition are both valuable tools in the programmer's toolkit. Knowing when to use each is essential for designing clean and maintainable code. Inheritance is suitable for "is-a" relationships, while composition is ideal for "has-a" relationships and can help avoid some of the pitfalls associated with inheritance. Choose your approach wisely to create robust and flexible software systems.
Top comments (2)
Well written article, good job. You brought facts, that's already a huge step.
As you said, both techniques are really good, and I would add to your conclusion, this:
Inheritance is best when the classes you're dealing with represent objects that are fundamentally the same and share a large amount of behavior. Do you want to make sure you reuse stuff. :)
Composition is best when you need to assemble various abilities or behaviors dynamically or you want to keep your classes as loosely coupled as possible.
I think the major drawbacks are If you misuse inheritance, you may find yourself dealing with fragile and tightly coupled code, or if you misuse composition, that can lead to a system that's difficult to understand and manage due to its fragmented nature.
I have a good tendency to always look for composition and sometimes aggregation as well, and leave it for last inheritance.
Good job again, keep it up!
Great job! You are absolutely right. Your conclusion perfectly complements my article. Thank you so much for your contribution.