In this post I present the two types of polymorphism supported by C#: Static and Dynamic Polymorphism. C#’s language features including inheritance, abstract base classes, interfaces, virtual methods, method overriding, method overloading, operator overloading, and generics, offers you a rich pallate from which you can design and create robust application architectures that support the SOLID design principles and allow you to think polymorphically.
Don’t Feel Like Reading? Watch the Video Instead!
The material discussed in this post comes from my book C# For Artists: The Art, Philosophy, And Science Of Object-Oriented Programming, 2nd Edition, with material from the forthcoming 3rd Edition.
C# supports two types of polymorphism: Static and Dynamic. Static polymorphism, otherwise, known as compile-time polymorphism, is enabled by the language features operator overloading, method overloading, and generics. Dynamic, or Runtime polymorphism, is all about method overriding and the language features that enable this are inheritance, abstract base classes, and interfaces.
The Purpose of Types
An object’s type dictates the range of allowed operations. For example, 1 + 2 = 3. That’s an (int + int). The + operator knows how to work on two integer objects. 1.5 + 2 = 3.5. That’s a (double + int), and the compiler knows how to convert the integer to a double and return a double. The next example is interesting: “1.5” + 2 results in a new string “1.52”. The + operator in this case is overloaded to perform string concatenation. In the final example, the + operator is being applied against two Person objects, p1 + p2, and will throw an exception unless the Person class overloads the + operator and defines exactly what it means in the context of two objects of type person. C# is a strongly typed language, which means the compiler needs to know the type of objects with which it’s dealing.
Dynamic Polymorphism
Dynamic polymorphism, also referred to as runtime polymorphism, simply means using derived class objects where base class objects are specified. It relies on the following language features: inheritance, abstract base classes, and interfaces. Inheritance is baked into the .NET Framework. You have the System.Object class, which sits at the top of the inheritance hierarchy of all .NET Framework types. Abstract base classes are used where you only want to specify an interface or provide a partial implementation. Interfaces are used to specify the interface to an object only with no implementation. The implementation of interface methods is left to the class implementing the interface.
The difference between an interface and a class in C# is that a class can extend at most one other class, but it can implement as many interfaces as required. Note that manually checking objects for their type is not polymorphic programming.
Understanding dynamic polymorphism leads to a deeper understanding of SOLID Design Principles. These include: the Single Responsibility Principle, the Open-Closed Principle, the Liskov Substitution Principle, the Interface Segregation Principle, and the Dependency Inversion Principle.
Single Responsibility Principle
A class should have at most one reason to change and one responsibility.
Open-Closed Principle
A software entity should be open for extension but closed for modification.
Liskov Substitution Principle (Also see Bertrand Meyer’s Design by Contract)
Subclasses should be directly substitutable for their base classes. Or stated another way, subtypes should fulfill the contract specified by their base type’s interface and pull no surprises.
Interface Segregation Principle
Favor client-specific interfaces over one general purpose interface.
Dependency Inversion Principle
Depend upon abstractions — not upon concretions.
Fleet Simulation Architecture
The fleet simulation architecture implements a lot of the SOLID design principles. It provides three abstract base classes: Weapon, Vessel, and PowerPlant. Vessel is a composite which comprises a Weapon and a PowerPlant. There are two Vessel subclasses: Surface Ship and Submarine. There are three Weapon subclasses: FiveInchGun, CWIS, and Torpedo, and finally there are three PowerPlant subclasses: Nuclear, GasTurbine, and Steam.
The abstract base classes form an abstraction layer, and once their interfaces have settled down, there is rarely a need to modify them. This leads to the Open-Closed Principle (OCP). Take the Weapon class for example. To create a new weapon type with different behavior and characteristics one need only extend the Weapon class to create a new subclass and provide the required implementation. In this way, the design is closed for modification but open to extension, with the notion being that modification should be avoided because of unknown system impacts and modifying code introduces new bugs. Anywhere a Weapon, Vessel, or PowerPlant is specified in the architecture, a substitution can be provided of the appropriate subtype. For example, if a method takes a Weapon as an argument, a CIWS object can be provided. The code targets the Weapon interface and CIWS is a Weapon. This is an example of the Liskov Substitution Principle (LSP) (or Bertrand Meyer’s Design by Contract). Note that a Weapon does not attempt to act like a PowerPlant. This is an example of the Interface Segregation Principle (ISP). You find many examples of the ISP throughout the .NET platform.
Person-Employee Architecture
This is an architecture for a Person-Employee application. At the top of the inheritance hierarchy sits three primary entities: A concrete class Person, an interface IPayable, and an abstract base class Employee. You can create instances of Person if you needed to. IPayable is just an interface and specifies one method Pay() which must be implemented by some class further down the inheritance hierarchy. Employee extends Person and implements IPayable, but defers the implementation of the Pay() method and leaves it to the concrete subclasses HourlyEmployee and SalariedEmployee.
The takeaway from this architecture is how you would achieve dynamic polymorphism. If you have a reference of type Person that points to an object of type HourlyEmployee, you can only manipulate that object using the interface specified by the Person class. If you have a reference of type IPayable that points to an HourlyEmployee object, you can only call the Pay() method, as that is the interface specified by the IPayable interface. A reference of type Employee offers the most benefit, as it combines Person and IPayable, plus any additional interface members required of an Employee.
Static Polymorphism
Static polymorphism, also referred to as compile time polymorphism, includes operator overloading, method overloading, and generics.
Operator Overloading
Operator overloading simply means defining how one of the overloadable operators behave in the context of your user-defined types. You saw earlier how the + operator was overloaded to work with different numeric types as well as serve as the string concatenation operator.
Method Overloading
Method overloading means reusing a method name to work on different types and number of arguments. You see this all the time with the System.WriteLine() method. You can overload constructor methods as well as ordinary methods.
Generics
You first encounter generics in the System.Collections.Generic namespace. Example, List<T>. The ‘T’ is a type parameter. You can define generic classes and methods using type parameters. Operations on the substituted objects target the System.Object interface unless a type constraint is specified. For example:
class MyClass<T> { … } //Can’t assume anything other than System.Object class MyClass<T> where T: Employee {…} //Ah! Expect Employee objects!
Bottom Line
Designing C# application architectures with polymorphism in mind at the very start leads to more robust, reusable, and extensible code. A deep understanding of how to achieve polymorphic behavior in all its forms is a powerful tool to add to your programmer’s toolbelt. Watch the YouTube video posted at the top of this article for coding demonstrations on both static and dynamic polymorphism in C#.