Inheritance says "I am a..." — Composition says "I have a..."
Imagine you're building a car. You could use inheritance: "A Car is a Vehicle" — that works. But a Car also has an engine, wheels, and a steering wheel. If you tried to model those with inheritance, you'd need a messy hierarchy: "Car is a Engine"? No, that's nonsense.
Inheritance is a powerful tool, but it's often overused. When you inherit, you tie your subclass tightly to the parent. That's fine for "is-a" relationships (like Dog is an Animal), but for "has-a" relationships (like a Car has an Engine), composition is the better choice.
In this lesson, you'll learn when to use inheritance, when to use composition, and why composition is often preferred in modern software design.
Inheritance is the "is-a" relationship. A Dog is an Animal. A Square is a Shape. The subclass inherits behavior from the base class.
Composition is the "has-a" relationship. A Car has an Engine. A House has a Door. The class contains instances of other classes to provide functionality.
Inheritance (in OOP) is a mechanism where a class derives from another class, gaining its members and behavior. In C#, inheritance is single (a class can inherit from only one base class) but allows method overriding and polymorphism.
Composition is a design principle where a class contains objects of other classes as fields or properties. It delegates work to those contained objects, effectively "composing" behavior from multiple parts.
virtual/override)Inheritance sounds great — "reuse code!" — but it comes with hidden costs:
Composition solves these issues:
The principle "Favor composition over inheritance" is one of the most important design guidelines in object-oriented programming.
Animal ← Mammal ← Dog
Car ⤳ EngineCar ⤳ WheelCar ⤳ SteeringWheel
public class Animal
{
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
}
Dog inherits all members of AnimalAnimal animal = new Dog();
animal.Speak(); // "Woof!"
public class Engine
{
public void Start() => Console.WriteLine("Engine started");
}
public class Car
{
private Engine _engine = new Engine();
public void Start() => _engine.Start();
}
Car contains an Engine (has-a)public class Shape
{
public virtual double Area() => 0;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area() => Width * Height;
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Math.PI * Radius * Radius;
}
Meaning: A Rectangle is a Shape. Circle is a Shape. They share a common base and can be used polymorphically.
public class Logger
{
public void Log(string message) => Console.WriteLine($"Log: {message}");
}
public class OrderService
{
private Logger _logger; // composition
public OrderService(Logger logger) => _logger = logger;
public void ProcessOrder() => _logger.Log("Order processed");
}
Meaning: OrderService has a Logger. It doesn't inherit from Logger; it uses it. This makes OrderService flexible — you can swap loggers or mock them for testing.
Consider a notification system that can send emails, SMS, and push notifications. With inheritance, you might create a base Notifier and derive EmailNotifier, SmsNotifier, etc. But if you need to combine behaviors (e.g., email + SMS), inheritance fails.
With composition, you can create small, focused classes and combine them flexibly.
// ─── Parts (composition) ───
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message) => Console.WriteLine($"Email: {message}");
}
public class SmsSender : IMessageSender
{
public void Send(string message) => Console.WriteLine($"SMS: {message}");
}
public class PushSender : IMessageSender
{
public void Send(string message) => Console.WriteLine($"Push: {message}");
}
// ─── Composed Notification Service ───
public class NotificationService
{
private readonly List<IMessageSender> _senders;
public NotificationService(params IMessageSender[] senders)
{
_senders = senders.ToList();
}
public void Notify(string message)
{
foreach (var sender in _senders)
sender.Send(message);
}
}
// ─── Usage ───
var service = new NotificationService(
new EmailSender(),
new SmsSender()
);
service.Notify("Order shipped!"); // Sends both email and SMS
Why this is better than inheritance:
SlackSender) doesn't require changing existing code.Inheritance is like a pre‑assembled toy — you get a specific shape, and you can't easily change the parts. If you want a different toy, you have to buy a new one (create a new subclass). The structure is fixed.
Composition is like LEGO bricks — you have small, reusable pieces that you can combine in countless ways. You can swap a red brick for a blue one, or add a wheel to a car. The possibilities are endless, and you can change the configuration at any time.
In software, composition gives you the same flexibility: you build complex behavior by combining small, focused objects, rather than locking yourself into a rigid hierarchy.
Every class that uses another object is using composition. The principle is about preferring composition over inheritance when designing relationships. It's not just a tactic; it's a mindset to favor loose coupling.
Inheritance is great for genuine "is-a" relationships where the behavior is truly shared and unlikely to change. For example, a List<T> inherits from Collection<T> because a list is a collection. Use inheritance when the relationship is stable and you want to reuse code and enable polymorphism.
Composition often requires writing more code (delegating methods), but the code is simpler, more testable, and more maintainable. The extra lines are a small price for flexibility.
Creating a base class just to share a few helper methods, even when there's no logical "is-a" relationship.
public class UtilityBase
{
public void Log(string msg) { ... }
}
public class OrderService : UtilityBase { } // Not an "is-a"
Use composition: inject a logger instead.
Composing everything into tiny pieces, making the design overly complex for simple problems.
Use the right tool for the job. For trivial cases, a simple inheritance might be fine.
Hard-coding concrete components inside a class, making it hard to swap or test.
public class OrderService
{
private Logger _logger = new Logger(); // tight coupling
}
Depend on an interface: private readonly ILogger _logger; and inject it.
You've learned the difference between inheritance and composition, and when to use each. Let's see if you can apply these concepts.
1. Which of the following is an example of an "is-a" relationship, where inheritance is appropriate?
Correct: B
Why B is correct: A Square is a Shape — this is a classic inheritance relationship. The behavior and properties of a Shape can be shared and overridden by Square.
Why A, C, D are incorrect: All of these are "has-a" relationships. A Car has an Engine, OrderService has a Logger, House has a Door. These are better modeled with composition.
Reinforcement: Inheritance models "is-a"; composition models "has-a".
2. What is a major drawback of using inheritance excessively?
Correct: B
Why B is correct: Inheritance couples the subclass to the base class. Changes to the base class can break subclasses unexpectedly (fragile base class problem). This is one of the main reasons to prefer composition.
Why A is incorrect: Inheritance actually promotes code reuse (that's one of its purposes).
Why C is incorrect: Inheritance generally has minimal performance impact; method calls are fast.
Why D is incorrect: Inheritance and interfaces can be used together; they are not mutually exclusive.
Reinforcement: Tight coupling and fragility are the primary drawbacks of overusing inheritance.
3. Which of the following best describes the principle "Favor composition over inheritance"?
Correct: B
Why B is correct: The principle is a guideline, not an absolute rule. It suggests that composition often leads to more flexible and maintainable designs, but inheritance still has its place for "is-a" relationships.
Why A is incorrect: It says "favor," not "always." Inheritance is still useful in appropriate scenarios.
Why C is incorrect: Inheritance and interfaces are different concepts; the principle applies to class inheritance.
Why D is incorrect: Composition is a general design principle, not limited to any specific domain.
Reinforcement: Favor composition means prefer it when it makes sense, not that it's always the only choice.
4. You are building a PaymentProcessor that needs to log each transaction. You already have a ILogger interface with multiple implementations (ConsoleLogger, FileLogger). Which approach is better?
Correct: C
Why C is correct: PaymentProcessor has a logger (composition). Injecting the logger via the constructor allows you to swap implementations easily, and makes testing simpler (you can mock ILogger). This follows the dependency inversion principle.
Why A and B are incorrect: PaymentProcessor is not a type of logger; inheriting would be a misuse of inheritance (not an "is-a" relationship).
Why D is incorrect: Copy-pasting code is a maintenance nightmare and violates DRY.
Reinforcement: For cross-cutting concerns like logging, composition (dependency injection) is the standard pattern.
5. Which of the following is a benefit of composition over inheritance?
Correct: B
Why B is correct: With composition, you can swap out components (e.g., change the engine of a car) at runtime, which is not possible with inheritance (the relationship is fixed at compile time).
Why A is incorrect: Composition does not provide multiple inheritance of classes (C# still has single inheritance), but it does allow combining behavior from many objects, which is a form of multiple inheritance of behavior.
Why C is incorrect: Interfaces are often used in conjunction with composition to define contracts for the parts.
Why D is incorrect: Inheritance method calls can be slightly faster, but the difference is negligible; flexibility is the primary benefit.
Reinforcement: Runtime flexibility is one of the biggest advantages of composition.
You now understand the trade-offs between inheritance and composition — you can design more flexible, maintainable systems!
dotnetmadeeasy.com — Learn C# and .NET, the right way.