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.