Polymorphism is the ability of different types to be treated uniformly through a common interface. It allows objects of different classes to be used interchangeably when they share a common contract.
Core Concept
Polymorphism means “many forms” - one interface, multiple implementations.
Key Insight: Code can work with abstractions without knowing the concrete types at compile time.
// Polymorphic code - works with any IShape
public decimal CalculateTotalArea(IEnumerable<IShape> shapes)
{
return shapes.Sum(shape => shape.Area()); // Don't need to know concrete type
}
// Works with circles, rectangles, triangles, etc.
var shapes = new List<IShape>
{
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4, 5)
};
var total = CalculateTotalArea(shapes);Types of Polymorphism
1. Subtype Polymorphism (Runtime Polymorphism)
Objects of derived classes can be used where base type is expected:
public abstract class Animal
{
public abstract string MakeSound();
}
public class Dog : Animal
{
public override string MakeSound() => "Woof!";
}
public class Cat : Animal
{
public override string MakeSound() => "Meow!";
}
// Polymorphic usage
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Console.WriteLine(animal1.MakeSound()); // "Woof!"
Console.WriteLine(animal2.MakeSound()); // "Meow!"2. Interface Polymorphism
Different classes implement the same interface:
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
}
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
// Credit card processing logic
}
}
public class PayPalPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
// PayPal processing logic
}
}
// Polymorphic usage
public class PaymentProcessor
{
public void Process(IPaymentMethod method, decimal amount)
{
method.ProcessPayment(amount); // Works with any payment method
}
}3. Parametric Polymorphism (Generics)
Code works with type parameters:
// Generic class works with any type
public class Repository<T>
{
private List<T> _items = new();
public void Add(T item) => _items.Add(item);
public T Get(int id) => _items[id];
}
// Can be used with any type
var userRepo = new Repository<User>();
var orderRepo = new Repository<Order>();4. Ad-hoc Polymorphism (Method Overloading)
Same method name, different parameter types:
public class Printer
{
public void Print(int value) => Console.WriteLine($"Integer: {value}");
public void Print(string value) => Console.WriteLine($"String: {value}");
public void Print(DateTime value) => Console.WriteLine($"Date: {value:yyyy-MM-dd}");
}Benefits of Polymorphism
Open/Closed Principle
Closed Principle - extend behavior without modification:
// BAD: Not polymorphic - must modify to add shapes
public class AreaCalculator
{
public decimal Calculate(object shape)
{
if (shape is Circle circle)
return Math.PI * circle.Radius * circle.Radius;
else if (shape is Rectangle rect)
return rect.Width * rect.Height;
// Must add new else-if for each shape!
}
}
// GOOD: Polymorphic - extend by adding new types
public interface IShape
{
decimal Area();
}
public class Circle : IShape
{
public decimal Radius { get; }
public decimal Area() => (decimal)Math.PI * Radius * Radius;
}
public class Rectangle : IShape
{
public decimal Width { get; }
public decimal Height { get; }
public decimal Area() => Width * Height;
}
// No modification needed to add new shapes
public class AreaCalculator
{
public decimal Calculate(IShape shape) => shape.Area();
}Dependency Inversion
Dependency Inversion - depend on abstractions:
// BAD: Depends on concrete class
public class OrderService
{
private readonly SqlDatabase _database = new SqlDatabase();
public void Save(Order order)
{
_database.Save(order);
}
}
// GOOD: Depends on abstraction (polymorphic)
public class OrderService
{
private readonly IDatabase _database;
public OrderService(IDatabase database)
{
_database = database;
}
public void Save(Order order)
{
_database.Save(order); // Works with any IDatabase implementation
}
}Testability
Polymorphism enables testing through test doubles:
// Production interface
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
// Production implementation
public class SmtpEmailService : IEmailService
{
public void SendEmail(string to, string subject, string body)
{
// Real SMTP logic
}
}
// Test double
public class FakeEmailService : IEmailService
{
public List<string> SentEmails { get; } = new();
public void SendEmail(string to, string subject, string body)
{
SentEmails.Add($"{to}: {subject}");
}
}
// Test
[Test]
public void OrderCompletion_SendsConfirmationEmail()
{
var fakeEmail = new FakeEmailService();
var orderService = new OrderService(fakeEmail);
orderService.CompleteOrder(order);
Assert.That(fakeEmail.SentEmails, Has.Count.EqualTo(1));
}Liskov Substitution Principle
Liskov Substitution Principle - subtypes must be substitutable:
// BAD: Violates LSP - Square can't substitute Rectangle
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area() => Width * Height;
}
public class Square : Rectangle
{
public override int Width
{
get => base.Width;
set { base.Width = value; base.Height = value; }
}
public override int Height
{
get => base.Height;
set { base.Width = value; base.Height = value; }
}
}
// This breaks!
void ResizeRectangle(Rectangle rect)
{
rect.Width = 5;
rect.Height = 4;
Assert.That(rect.Area(), Is.EqualTo(20)); // Fails for Square!
}
// GOOD: Proper abstraction
public interface IShape
{
int Area();
}
public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Area() => Width * Height;
}
public class Square : IShape
{
public int SideLength { get; set; }
public int Area() => SideLength * SideLength;
}Design Patterns Using Polymorphism
Strategy Pattern
Strategy Pattern - encapsulate algorithms:
public interface ISortStrategy
{
void Sort(int[] array);
}
public class QuickSort : ISortStrategy
{
public void Sort(int[] array) { /* QuickSort logic */ }
}
public class MergeSort : ISortStrategy
{
public void Sort(int[] array) { /* MergeSort logic */ }
}
public class Sorter
{
public void Sort(int[] array, ISortStrategy strategy)
{
strategy.Sort(array); // Polymorphic call
}
}Template Method Pattern
Define algorithm structure, let subclasses override steps:
public abstract class DataProcessor
{
public void Process()
{
LoadData();
ProcessData();
SaveData();
}
protected abstract void LoadData();
protected abstract void ProcessData();
protected abstract void SaveData();
}
public class CsvDataProcessor : DataProcessor
{
protected override void LoadData() { /* Load CSV */ }
protected override void ProcessData() { /* Process */ }
protected override void SaveData() { /* Save CSV */ }
}Factory Pattern
Create objects polymorphically:
public interface INotification
{
void Send(string message);
}
public class NotificationFactory
{
public INotification Create(string type)
{
return type switch
{
"email" => new EmailNotification(),
"sms" => new SmsNotification(),
"push" => new PushNotification(),
_ => throw new ArgumentException("Unknown type")
};
}
}Common Pitfalls
Inappropriate Inheritance
Using inheritance when composition is better:
// BAD: Inheritance for code reuse
public class Stack : List<int>
{
public void Push(int value) => Add(value);
public int Pop()
{
int value = this[Count - 1];
RemoveAt(Count - 1);
return value;
}
}
// Problem: Exposes all List methods (Clear, Sort, etc.)
// GOOD: Composition
public class Stack
{
private List<int> _items = new();
public void Push(int value) => _items.Add(value);
public int Pop()
{
int value = _items[Count - 1];
_items.RemoveAt(Count - 1);
return value;
}
}Violating LSP
Creating subtypes that break contracts:
// BAD: Derived class violates expectations
public class Bird
{
public virtual void Fly() { /* Flying logic */ }
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Penguins can't fly");
}
}
// GOOD: Better abstraction
public interface IBird { }
public interface IFlyingBird : IBird
{
void Fly();
}
public class Eagle : IFlyingBird
{
public void Fly() { /* Flying logic */ }
}
public class Penguin : IBird
{
public void Swim() { /* Swimming logic */ }
}Over-Abstraction
Creating unnecessary abstractions:
// TOO MUCH: Over-engineered
public interface IInteger
{
int GetValue();
}
public class Integer : IInteger
{
private readonly int _value;
public int GetValue() => _value;
}
// JUST RIGHT: Use primitives when appropriate
int value = 42;Duck Typing (Dynamic Languages)
In dynamic languages, polymorphism through duck typing:
# Python example - no explicit interface needed
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
def make_animal_speak(animal):
print(animal.speak()) # Works if animal has speak()
make_animal_speak(Dog()) # "Woof!"
make_animal_speak(Cat()) # "Meow!"When to Use Polymorphism
Use polymorphism when:
- Multiple implementations of the same behavior exist
- Behavior needs to be selected at runtime
- You want to extend behavior without modifying existing code
- Testing requires substituting dependencies
Don’t use polymorphism when:
- There’s only one implementation and none expected
- Behavior is static and known at compile time
- The abstraction doesn’t have a clear, stable contract
- Simplicity is more important than flexibility
Resources
See Also
- Closed Principle - Enabled by polymorphism
- Liskov Substitution Principle - Rules for proper polymorphism
- Dependency Inversion - Depend on abstractions
- Composition over Inheritance - Alternative to inheritance polymorphism
- Design Patterns - Many patterns use polymorphism
- Object-Oriented Design
- Design Concepts
- Good Software Practices