đź“„

Polymorphism

Intermediate 2 min read 200 words

Allow objects to take multiple forms, enabling flexible and extensible code

Polymorphism

Polymorphism means “many forms.” It allows objects of different types to be treated through a common interface, with each type providing its own implementation.

Types of Polymorphism

C# supports several types of polymorphism:

  1. Compile-time (Static): Method overloading, operator overloading
  2. Runtime (Dynamic): Method overriding, interface implementation
  3. Parametric: Generics

Compile-Time Polymorphism: Method Overloading

The compiler determines which method to call based on the method signature.

public class Calculator
{
    // Same method name, different parameter types
    public int Add(int a, int b) => a + b;
    public double Add(double a, double b) => a + b;
    public string Add(string a, string b) => a + b;

    // Same method name, different number of parameters
    public int Add(int a, int b, int c) => a + b + c;
    public int Add(params int[] numbers) => numbers.Sum();
}

// Usage - compiler picks the right method
var calc = new Calculator();
int sum1 = calc.Add(1, 2);           // Calls Add(int, int)
double sum2 = calc.Add(1.5, 2.5);    // Calls Add(double, double)
string concat = calc.Add("Hello", "World"); // Calls Add(string, string)
int sum3 = calc.Add(1, 2, 3, 4, 5);  // Calls Add(params int[])

Runtime Polymorphism: Method Overriding

The runtime determines which method to call based on the actual object type.

public abstract class Shape
{
    public string Color { get; set; }

    public abstract double GetArea();
    public abstract void Draw();
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public override double GetArea() => Math.PI * Radius * Radius;
    public override void Draw() => Console.WriteLine($"Drawing {Color} circle");
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double GetArea() => Width * Height;
    public override void Draw() => Console.WriteLine($"Drawing {Color} rectangle");
}

public class Triangle : Shape
{
    public double Base { get; set; }
    public double Height { get; set; }

    public override double GetArea() => 0.5 * Base * Height;
    public override void Draw() => Console.WriteLine($"Drawing {Color} triangle");
}

// Polymorphic usage - same method, different behavior
public class ShapeProcessor
{
    public void ProcessShapes(List<Shape> shapes)
    {
        foreach (var shape in shapes)
        {
            shape.Draw();  // Calls the correct Draw() for each shape type
            Console.WriteLine($"Area: {shape.GetArea()}");
        }
    }
}

// Usage
var shapes = new List<Shape>
{
    new Circle { Radius = 5, Color = "Red" },
    new Rectangle { Width = 4, Height = 3, Color = "Blue" },
    new Triangle { Base = 6, Height = 4, Color = "Green" }
};

var processor = new ShapeProcessor();
processor.ProcessShapes(shapes);
// Output:
// Drawing Red circle
// Area: 78.54
// Drawing Blue rectangle
// Area: 12
// Drawing Green triangle
// Area: 12

Interface Polymorphism

Different classes implement the same interface differently.

public interface IPaymentMethod
{
    Task<PaymentResult> ProcessAsync(decimal amount);
    Task RefundAsync(decimal amount);
    string PaymentType { get; }
}

public class CreditCard : IPaymentMethod
{
    public string PaymentType => "Credit Card";
    public string CardNumber { get; set; }

    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        Console.WriteLine($"Charging ${amount} to card ending in {CardNumber[^4..]}");
        return new PaymentResult { Success = true };
    }

    public async Task RefundAsync(decimal amount)
    {
        Console.WriteLine($"Refunding ${amount} to card");
    }
}

public class PayPal : IPaymentMethod
{
    public string PaymentType => "PayPal";
    public string Email { get; set; }

    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        Console.WriteLine($"Charging ${amount} via PayPal to {Email}");
        return new PaymentResult { Success = true };
    }

    public async Task RefundAsync(decimal amount)
    {
        Console.WriteLine($"Refunding ${amount} via PayPal");
    }
}

public class Cryptocurrency : IPaymentMethod
{
    public string PaymentType => "Crypto";
    public string WalletAddress { get; set; }

    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        Console.WriteLine($"Processing ${amount} crypto payment");
        return new PaymentResult { Success = true };
    }

    public async Task RefundAsync(decimal amount)
    {
        Console.WriteLine($"Refunding ${amount} to wallet");
    }
}

// Payment gateway doesn't need to know specific payment types
public class PaymentGateway
{
    public async Task ProcessPaymentAsync(IPaymentMethod method, decimal amount)
    {
        Console.WriteLine($"Processing {method.PaymentType} payment...");
        var result = await method.ProcessAsync(amount);

        if (!result.Success)
        {
            Console.WriteLine("Payment failed, initiating refund...");
            await method.RefundAsync(amount);
        }
    }
}

Generic Polymorphism (Parametric)

Write code that works with any type while maintaining type safety.

public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly DbContext _context;

    public Repository(DbContext context)
    {
        _context = context;
    }

    public async Task<T> GetByIdAsync(int id)
        => await _context.Set<T>().FindAsync(id);

    public async Task<List<T>> GetAllAsync()
        => await _context.Set<T>().ToListAsync();

    public async Task AddAsync(T entity)
    {
        await _context.Set<T>().AddAsync(entity);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(T entity)
    {
        _context.Set<T>().Update(entity);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var entity = await GetByIdAsync(id);
        if (entity != null)
        {
            _context.Set<T>().Remove(entity);
            await _context.SaveChangesAsync();
        }
    }
}

// Same repository code, different types
var userRepo = new Repository<User>(context);
var productRepo = new Repository<Product>(context);
var orderRepo = new Repository<Order>(context);

// All share the same interface
await userRepo.GetAllAsync();
await productRepo.AddAsync(new Product { Name = "Widget" });

Polymorphism in Action: Plugin Architecture

// Define a plugin interface
public interface IExportPlugin
{
    string Format { get; }
    byte[] Export(ReportData data);
}

// Different export implementations
public class PdfExporter : IExportPlugin
{
    public string Format => "PDF";
    public byte[] Export(ReportData data)
    {
        // PDF generation logic
        return GeneratePdf(data);
    }
}

public class ExcelExporter : IExportPlugin
{
    public string Format => "Excel";
    public byte[] Export(ReportData data)
    {
        // Excel generation logic
        return GenerateExcel(data);
    }
}

public class CsvExporter : IExportPlugin
{
    public string Format => "CSV";
    public byte[] Export(ReportData data)
    {
        // CSV generation logic
        return GenerateCsv(data);
    }
}

// Report service supports any export format
public class ReportService
{
    private readonly Dictionary<string, IExportPlugin> _exporters;

    public ReportService(IEnumerable<IExportPlugin> exporters)
    {
        _exporters = exporters.ToDictionary(e => e.Format);
    }

    public byte[] GenerateReport(ReportData data, string format)
    {
        if (!_exporters.TryGetValue(format, out var exporter))
            throw new NotSupportedException($"Format {format} not supported");

        return exporter.Export(data);
    }
}

// Add new formats without changing ReportService!

Benefits of Polymorphism

Benefit Description
Flexibility Easy to add new types without changing existing code
Loose Coupling Code depends on abstractions, not concrete types
Maintainability Changes isolated to specific implementations
Testability Mock interfaces for unit testing
Extensibility New implementations don’t break callers

Interview Tips

Common Questions:

  • “Explain polymorphism with an example”
  • “What’s the difference between overloading and overriding?”
  • “How does polymorphism support the Open/Closed Principle?”

Key Points:

  1. Overloading (compile-time): Same name, different parameters
  2. Overriding (runtime): Same signature, different implementation in derived class
  3. Interface polymorphism: Different classes, same interface
  4. Generics: Type-agnostic code

Interview Code Challenge:

// Classic polymorphism question: What's the output?
public class Animal { public virtual void Speak() => Console.WriteLine("Animal"); }
public class Dog : Animal { public override void Speak() => Console.WriteLine("Dog"); }
public class Cat : Animal { public override void Speak() => Console.WriteLine("Cat"); }

Animal a = new Dog();
a.Speak();  // Output: "Dog" (runtime polymorphism)

Animal[] animals = { new Dog(), new Cat(), new Animal() };
foreach (var animal in animals)
    animal.Speak();  // Output: "Dog", "Cat", "Animal"