Ask a developer the following question: “What are the fundamental characteristics of object-oriented programming (OOP)?” You will hear an immediate reply saying that classes (and objects), inheritance, abstraction, encapsulation, and polymorphism are the most important characteristics of OOP. In addition, when you analyze OOP-based enterprise code, you’ll find different forms of polymorphism. But the truth is that a novice programmer rarely uses the power of polymorphism. Why? It is said that object-oriented programmers pass through three important stages. In the first stage, they become familiar with non-object-oriented constructs. In this stage, they use decision statements, looping constructs, etc. In the second stage, they start creating classes and objects and use the inheritance mechanism. Finally, in the third stage, they use polymorphism to achieve late binding and make their programs flexible. But writing the polymorphic is not always easy. Honestly, it is a little bit tough compared to the other features of OOP. Using some simple but powerful code examples, this chapter will make this concept easy for you to understand.
Recap of Polymorphism
Polymorphism simply means there is one name with many forms. In the real world, it is a common phenomenon. Consider the behavior of your pet dog: when it sees an unknown person, it starts barking. But when it sees you, it makes different noises and behaves differently. In both cases, this dog sees with its eyes, but based on the observation, the dog behaves differently.
You can relate this concept to other areas as well. For example, consider the customer support departments in different organizations. They each provide support to the customers in their own way. Similarly, each of the search engine providers such as Google, Yahoo, or Microsoft Bing searches the Internet following its own algorithm.
OOP likes to mimic real-world scenarios, and conceptually, the polymorphic code works in the same way. In C#, a class can have methods (or properties). Optionally, you can provide implementations for them. C# also allows the derived classes to override those implementations as per their needs. As a result, these related types can have methods with the same name, but they can show different behaviors. This is the key concept to understand before you deal with the polymorphic code.
Initial Program
The importance of a feature is often realized in the absence of it. So, I start with a program that does not use the concept of polymorphism. This program compiles and runs successfully. Here you have three different types of animals—tigers, dogs, and monkeys. Each of them can produce a different sound. So, I made classes with their corresponding names, and in each class, you see a Sound() method. Check whether you can improve this program.
Demonstration 1
Output
Analysis
I have used the simplified new expressions here. For example, the line Tiger tiger = new(); is the simplified version of Tiger tiger = new Tiger(); Starting with C# 9.0, you can use this form. It says that during the constructor invocation if the target type of an expression is known, you can omit the type name.
When you use Tiger tiger = new Tiger();, the tiger is a reference to an object that is based on the Tiger class. This reference refers to the object, but it does not contain the object data itself. Even Tiger tiger; is a valid line of code that creates an object reference without creating the actual object.
I could achieve the same effect using an abstract class. When you use an abstract class or an interface, the first thing that comes to mind is inheritance. How do you know whether you are correctly using inheritance? The simple answer is that you do an IS-A test. For example, a rectangle IS-A shape, but the reverse is not necessarily true. Take another example: a monkey IS-An animal, but not all animals are monkeys. Notice that the IS-A test is unidirectional.
In programming, if you inherit class B from class A, you say that B is the subclass and A is the parent class or base class. But most importantly, you can say B is a type of A. So, if you derive a Tiger class or a Dog class from a base class called Animal (or an interface say IAnimal), you can say that Dog IS-An Animal (or IAnimal) or Tiger IS-An Animal (or IAnimal). Similarly, a rectangle IS-A special type of shape. A square IS-A special type of rectangle. So, a square IS-A shape too.
If you have an inheritance tree, this IS-A test can be applied anywhere in the tree.
Let us assume that I represent rectangles and shapes using the Rectangle and Shape classes, respectively. Now when I say Rectangle IS-A Shape, programmatically I tell that a Rectangle instance can invoke the methods that a Shape instance can invoke. But, if needed, a Rectangle class can include some specific methods that are absent in the Shape class. To invoke these specific methods, you need to use a Rectangle instance only; since the Shape class does not include those methods, the Shape instances cannot call them.
In C#, a parent (or base) class reference can refer to a subclass object. Since each tiger, dog, or monkey is an animal, you can introduce a parent type and inherit all these concrete classes from it. I told you that I am going to use a C# interface now. Following the C# naming convention, let’s name the supertype as IAnimal.
Better Program
Now I rewrite this program which produces the same output. Let’s take a look at the following demonstration.
Demonstration 2
Analysis
Have you noticed the difference? This time I used the superclass reference animal to refer to different derived class objects.
If you run the program again with these changes, you see the same output.
Notice that in demonstration 1, when a client reads the line dog.Sound(), they can assume that the Sound() method from the Dog class will be invoked.
But in demonstration 2, when the client reads the line animal.Sound(), it is not obvious which subtype of IAnimal will invoke the Sound(). Why is this important? As a programmer, you do not provide every possible detail to your clients.
you can surely predict that the Sound() method of Tiger class will be used. So, it appears that you know the output in advance and you doubt the concept of polymorphism. If this is the case, let us further dig into this.
It is now clear that no one can predict the output of this program in advance. You can see the effective use of polymorphism in this example.
One more point: you can use a simplified new expression again. For example, the line Random random = new Random(); can be shortened if you use Random random = new();. When you download the source code from the Apress website, refer to the folder Demo3_Polymorphism inside Chapter1 to see the complete program.
You should not assume that the GetAnimal() and MakeSound(...) methods need to be static only. You can use them as instance methods too. When you download the source code from the Apress website, refer to the folder Demo4_Polymorphism inside Chapter1 to see this modified program.
Useful Notes
Before I finish this chapter, let me point out some important information for your immediate reference.
C# types including the user-defined types are polymorphic because they inherit from Object.
In short, a base class can define (or implement) virtual methods, and if needed, the derived classes override them as per their needs. As a result, at runtime, when client code calls the method, the common language runtime (CLR) can invoke the appropriate method based on the runtime type of the object. These are the key things to understand in the polymorphic code.
C# primarily supports OOP. But being hybrid in nature, it can support functional programming (FP) too. FP prefers immutability and pure functions. A function is pure if it returns the same value for the same input. Otherwise, it is an impure function. So, from a FP developer’s point of view, you may consider the dynamic behavior of a method as a problem instead of an advantage. But I remind you that our focus is on OOP in this book, but not on FP. So, you should not be confused.
you’ll see the following error: CS0681 The modifier 'abstract' is not valid on fields. Try using a property instead.
Summary
To implement polymorphic behavior, I started with an interface. I could achieve the same effect using an abstract class. There are situations when an interface is a better choice over an abstract class, and vice versa. You will see a discussion about this in Chapter 2.
When you code to a parent type (it can be an interface, an abstract class, or simply a parent class), the code can work with any new classes implementing the interface. This helps you to adjust to lots of new changes in the future, and you can adopt those requirements easily. This is the power of polymorphism. But if you use only concrete classes in your program, it is likely that you may need to change your existing code in the future such as when you add a new concrete class. This approach does not follow the Open/Closed principle, which says your code should be open for extension but closed for modification.
I have shown you the advantages of polymorphism. But it’s not always easy to write polymorphic code, and you need to be careful when you use it. You’ll get a better idea about this when I discuss SOLID principles in Chapter 4.
Everything in this chapter may not be new to you, but I believe that you have a better idea about polymorphism now. Before you move on to the next chapters, let me make sure that we agree on the following points.
you are programming to concrete implementation.
you are programming to a supertype. It is often referred to as programming to an interface.
When we say programming to an interface, it does not necessarily mean that you use a C# interface only. It can be an abstract class or a parent/base class too.
In this case, by merely reading the code, no one can predict the output in advance. In simple terms, this code segment implies that you announce to the outside world that you get an animal through the GetAnimal() method and this animal can make a sound.
How can you perform an IS-A test?
How can you write a polymorphic code for your application, and why is it better?
How can you iterate over a list when you write the polymorphic code?
How can you write a better polymorphic code?
How do experts differentiate between “programming to an implementation” and “programming to an interface”?