Composition over Inheritance
Part of the Object Oriented Programming section of Coddy's C# journey — lesson 41 of 70.
Inheritance creates tight coupling between classes. When a Car inherits from Vehicle, any change to Vehicle can break Car. Composition offers a more flexible alternative: instead of inheriting behavior, a class contains objects that provide that behavior.
Consider a game character that can move and attack. With inheritance, you might create a complex hierarchy. With composition, you simply give the character the components it needs:
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}The key difference is the relationship. Inheritance represents "is-a" (a Dog is an Animal), while composition represents "has-a" (a Character has movement behavior). Composition lets you swap behaviors at runtime, combine capabilities freely, and avoid the fragile base class problem.
Use inheritance when there's a genuine hierarchical relationship. Choose composition when you need flexibility, when behaviors might be shared across unrelated classes, or when you want to change behavior dynamically.
Challenge
EasyLet's build a notification system that demonstrates the power of composition over inheritance. Instead of creating a complex hierarchy of notification classes, you'll compose a NotificationSender by giving it interchangeable behavior components.
You'll organize your code across three files:
Behaviors.cs: Create two behavior classes in theNotificationsnamespace that handle different aspects of sending notifications:FormattingBehavior- has a methodFormat(string message)that returns the message wrapped with[ALERT]at the start and[END]at the endDeliveryBehavior- has a methodDeliver(string channel, string message)that returns a string in the formatSending via {channel}: {message}
NotificationSender.cs: Create aNotificationSenderclass in theNotificationsnamespace that uses composition to combine behaviors. Your sender should:- Contain instances of both
FormattingBehaviorandDeliveryBehavioras private fields - Have a
Channelproperty (string) that stores the delivery channel name - Have a constructor that accepts the channel name
- Have a
Send(string message)method that first formats the message using the formatting behavior, then delivers it using the delivery behavior, and returns the final result
- Contain instances of both
Program.cs: In your main file, create aNotificationSenderwith a channel from input, then send a message from input. Print the result to show how the composed behaviors work together.
You will receive two inputs:
- The delivery channel name (e.g.,
Email,SMS) - The message to send
Print the result of calling Send() on your notification sender.
For example, if the inputs are Email and Server is down, the output should be:
Sending via Email: [ALERT] Server is down [END]Notice how the NotificationSender doesn't inherit from anything - it simply "has" formatting and delivery behaviors. This composition approach means you could easily swap out either behavior without changing the sender class, or reuse these behaviors in completely different classes!
Cheat sheet
Composition is an alternative to inheritance that creates more flexible code by having a class contain objects that provide behavior, rather than inheriting behavior from a parent class.
Inheritance vs Composition:
- Inheritance represents "is-a" relationships (a Dog is an Animal)
- Composition represents "has-a" relationships (a Character has movement behavior)
Benefits of Composition:
- Swap behaviors at runtime
- Combine capabilities freely
- Avoid tight coupling and fragile base class problems
- Share behaviors across unrelated classes
Example with behavior components:
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}When to use each approach:
- Use inheritance for genuine hierarchical relationships
- Choose composition for flexibility, shared behaviors, or dynamic behavior changes
Try it yourself
using System;
using Notifications;
class Program
{
public static void Main(string[] args)
{
// Read input
string channel = Console.ReadLine();
string message = Console.ReadLine();
// TODO: Create a NotificationSender with the channel
// TODO: Send the message and print the result
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesNamespaces & DirectivesIntro to Classes & ObjectsThe 'this' KeywordMethods and ParametersFields vs PropertiesConstructorsObject InitializersRecap - Simple Calculator4Inheritance
Basic Inheritance (:) SyntaxThe 'base' KeywordVirtual & Override KeywordsSealed ClassesThe 'object' Base ClassRecap - Employee Hierarchy7Advanced Features
Operator OverloadingIndexers (this[])ToString() OverrideExtension MethodsRecap - Custom List2Properties & Static Members
Auto-Implemented PropertiesRead/Write-Only PropertiesStatic Fields & MethodsStatic ClassesExpression-Bodied Members5Polymorphism & Interfaces
Compile vs Runtime PolyInterface vs Abstract ClassMultiple InterfacesExplicit InterfacesUpcasting & DowncastingRecap - Shape Calculator8Advanced OOP Concepts
Composition over InheritanceGenerics (Classes & Methods)Delegates and EventsAttributes and ReflectionIDisposable & using StatementDependency Injection Basics