Polymorphism in Practice

Vaibhav • September 10, 2025

In earlier articles, we explored the concept of polymorphism - how objects can take many forms and how method calls are resolved at runtime based on the actual object type. Now it’s time to go beyond the theory and see how polymorphism is used in real-world C# applications. In this article, we’ll walk through practical scenarios where polymorphism shines, how it improves flexibility and maintainability, and how to design systems that take full advantage of it.

Polymorphism through interfaces in real-world design

One of the most common uses of polymorphism is through interfaces. Interfaces allow you to define a contract that multiple classes can implement in their own way. This is especially useful when you want to write code that works with a variety of types without knowing their exact implementation.

interface IExporter
{
    void Export(string data);
}

class CsvExporter : IExporter
{
    public void Export(string data)
    {
        Console.WriteLine($"Exporting data to CSV: {data}");
    }
}

class JsonExporter : IExporter
{
    public void Export(string data)
    {
        Console.WriteLine($"Exporting data to JSON: {data}");
    }
}

Both CsvExporter and JsonExporter implement IExporter. You can now write a method that works with any exporter:

void SaveReport(IExporter exporter, string reportData)
{
    exporter.Export(reportData);
}

This method doesn’t care whether the data is exported as CSV or JSON - it just calls Export(). That’s polymorphism in practice: behavior varies based on the actual object passed in.

Polymorphism in UI frameworks

Polymorphism is heavily used in UI frameworks like Windows Forms, WPF, and ASP.NET. For example, all UI controls typically inherit from a common base class like Control. This allows you to treat all controls uniformly when rendering, updating, or handling events.

abstract class Control
{
    public abstract void Render();
}

class Button : Control
{
    public override void Render()
    {
        Console.WriteLine("Rendering a button");
    }
}

class TextBox : Control
{
    public override void Render()
    {
        Console.WriteLine("Rendering a text box");
    }
}

You can now render any control using a single method:

void RenderUI(List<Control> controls)
{
    foreach (var control in controls)
    {
        control.Render();
    }
}

This pattern allows UI frameworks to be extensible - you can add new controls without changing the rendering logic.

Polymorphism in logging and diagnostics

Logging is another area where polymorphism is widely used. You might define an interface like ILogger and implement it in different ways - console logger, file logger, remote logger, etc.

interface ILogger
{
    void Log(string message);
}

class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[Console] {message}");
    }
}

class FileLogger : ILogger
{
    public void Log(string message)
    {
        File.AppendAllText("log.txt", message + Environment.NewLine);
    }
}

You can now inject any logger into your application:

class Processor
{
    private readonly ILogger _logger;

    public Processor(ILogger logger)
    {
        _logger = logger;
    }

    public void Run()
    {
        _logger.Log("Processing started...");
    }
}

This design makes your application flexible and testable. You can swap loggers without changing the Processor class.

Polymorphism in testing and mocking

Polymorphism is essential for unit testing. You can create mock implementations of interfaces to simulate behavior and verify interactions.

class MockLogger : ILogger
{
    public List<string> Messages = new List<string>();

    public void Log(string message)
    {
        Messages.Add(message);
    }
}

You can now test the Processor class without writing to the console or a file:

var mock = new MockLogger();
var processor = new Processor(mock);
processor.Run();

Console.WriteLine(mock.Messages[0]); // Output: Processing started...

This is a clean, isolated test - made possible by polymorphism.

Polymorphism in data processing pipelines

In data processing systems, you often have different steps that implement a common interface. For example, you might define a IDataTransformer interface and implement it in various ways.

interface IDataTransformer
{
    string Transform(string input);
}

class UpperCaseTransformer : IDataTransformer
{
    public string Transform(string input) => input.ToUpper();
}

class ReverseTransformer : IDataTransformer
{
    public string Transform(string input) => new string(input.Reverse().ToArray());
}

You can now build a pipeline of transformers:

string ProcessData(string input, List<IDataTransformer> steps)
{
    foreach (var step in steps)
    {
        input = step.Transform(input);
    }
    return input;
}

This design is extensible - you can add new transformers without changing the pipeline logic.

Polymorphism and the Open/Closed Principle

Polymorphism supports the Open/Closed Principle - one of the SOLID principles. This principle states that software should be open for extension but closed for modification. In other words, you should be able to add new behavior without changing existing code.

Polymorphism makes this possible. You can add new implementations of an interface or base class, and the rest of the system continues to work without modification.

Design your systems around abstractions (interfaces or base classes), not concrete implementations. This allows you to extend behavior without rewriting existing code.

Polymorphism and dependency injection

Polymorphism is the foundation of dependency injection. When you inject dependencies as interfaces, you can substitute different implementations at runtime - for example, using a mock in tests, a real service in production, or a stub in development.

interface IEmailService
{
    void Send(string to, string subject, string body);
}

class SmtpEmailService : IEmailService
{
    public void Send(string to, string subject, string body)
    {
        Console.WriteLine($"Sending email to {to}");
    }
}

You can inject IEmailService into any class that needs to send email, without tying it to a specific implementation.

Polymorphism and extensibility in plugins

Many applications support plugins - external components that extend functionality. Polymorphism makes this possible. You define a common interface, and plugins implement it.

interface IPlugin
{
    string Name { get; }
    void Execute();
}

The application loads plugins dynamically and calls Execute() on each one. This allows third-party developers to extend your app without modifying its core.

Summary

Polymorphism is not just a theoretical concept - it’s a practical tool that powers real-world C# applications. You’ve seen how it enables flexible design through interfaces and base classes, how it supports testing, dependency injection, and plugin systems, and how it helps you follow the Open/Closed Principle. By designing around abstractions and using polymorphism effectively, you can build systems that are modular, extensible, and easy to maintain.

In the next article, we’ll explore Casting and Type Checking - how to safely convert between types and verify object compatibility at runtime.