© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
V. SarcarSimple and Efficient Programming with C# https://doi.org/10.1007/978-1-4842-8737-8_1

1. Flexible Code Using Polymorphism

Vaskaran Sarcar1  
(1)
Kolkata, West Bengal, India
 

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

Here is the complete demonstration:
Console.WriteLine("***Sounds of the different animals.***");
Tiger tiger = new();
tiger.Sound();
Dog dog = new();
dog.Sound();
Monkey monkey = new();
monkey.Sound();
Console.ReadKey();
class Tiger
{
    public void Sound()
    {
        Console.WriteLine("Tigers roar.");
    }
}
class Dog
{
    public void Sound()
    {
        Console.WriteLine("Dogs bark.");
    }
}
class Monkey
{
    public void Sound()
    {
        Console.WriteLine("Monkeys whoop.");
    }
}

Output

Here is the output:
***Sounds of the different animals.***
Tigers roar.
Dogs bark.
Monkeys whoop.

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.

Understand that when you use Tiger tiger = new Tiger();, we say that you are programming to an implementation. Notice that in this case the reference and object both are of the same type. You can improve this program using the concept of polymorphism. In the upcoming implementation, I show you such an example. I use an interface in this example. Before I show you the example, let me remind you of a few important points:
  • 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.

Here is a code fragment that shows the IAnimal interface. It also gives you an idea of how to override its Sound() method in the Tiger class. The Monkey and Dog class can do the same thing.
    interface IAnimal
    {
        void Sound();
    }
    class Tiger : IAnimal
    {
        public void Sound()
        {
            Console.WriteLine("Tigers roar.");
        }
    }
Programming to a supertype gives you more flexibility. It allows you to use a reference variable polymorphically. The following code segment demonstrates such a usage:
IAnimal animal = new Tiger();
animal.Sound();
animal = new Dog();
animal.Sound();
// The remaining code skipped

Better Program

Now I rewrite this program which produces the same output. Let’s take a look at the following demonstration.

Demonstration 2

Here is demonstration 2. It is a modified version of demonstration 1.
Console.WriteLine("***Sounds of the different animals.***");
IAnimal animal = new Tiger();
animal.Sound();
animal = new Dog();
animal.Sound();
animal = new Monkey();
animal.Sound();
interface IAnimal
{
    void Sound();
}
class Tiger : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Tigers roar.");
    }
}
class Dog : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Dogs bark.");
    }
}
class Monkey : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Monkeys whoop.");
    }
}

Analysis

Have you noticed the difference? This time I used the superclass reference animal to refer to different derived class objects.

Following this approach, not only do you type less, but you also use a program that is more flexible and easier to maintain. If needed, now you can iterate over a list too. For example, you can replace the following code segment inside Main():
IAnimal animal = new Tiger();
animal.Sound();
animal = new Dog();
animal.Sound();
animal = new Monkey();
animal.Sound();
with the following code:
List<IAnimal> animals = new List<IAnimal>
{
     new Tiger(),
     new Dog(),
     new Monkey()
};
foreach (IAnimal animal in animals)
     animal.Sound();

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.

This discussion is not over yet. Here I have used one of the simplest forms of polymorphism. In this case, a question may come to mind: we know a supertype reference can refer to a subtype object in C#. So, when I use the following lines:
IAnimal animal = new Tiger();
animal.Sound();

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.

Let us assume that you create a subtype based on some runtime random number generator (or user input). In this case, you cannot predict the output in advance. For example, see the following lines of code:
 IAnimal animal = AnimalProducer.GetAnimal();
 animal.Sound();
What is the difference? Anyone who sees this code segment can assume that GetAnimal() of the AnimalProducer class returns an animal that can make some sound. How can you achieve this? It is pretty simple: let me rewrite the program. Notice the changes in bold:
Console.WriteLine("***Sounds of the different
 animals.***");
IAnimal animal = AnimalProducer.GetAnimal();
animal.Sound();
animal = AnimalProducer.GetAnimal();
animal.Sound();
animal = AnimalProducer.GetAnimal();
animal.Sound();
interface IAnimal
{
    void Sound();
}
class Tiger : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Tigers roar.");
    }
}
class Dog : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Dogs bark.");
    }
}
class Monkey : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Monkeys whoop.");
    }
}
class AnimalProducer
{
    internal static IAnimal GetAnimal()
    {
        IAnimal animal;
        Random random = new Random();
        // Get a number between 0 and 3(exclusive)
        int temp = random.Next(0, 3);
        if (temp == 0)
        {
            animal = new Tiger();
        }
        else if (temp == 1)
        {
            animal = new Dog();
        }
        else
        {
            animal = new Monkey();
        }
        return animal;
    }
}
Run this application now and notice the output. Here is the sample output that I got on the various runs:
First Run:
***Sounds of the different animals.***
Monkeys whoop.
Dogs bark.
Monkeys whoop.
Second Run:
***Sounds of the different animals.***
Dogs bark.
Dogs bark.
Tigers roar.
Third Run:
***Sounds of the different animals.***
Tigers roar.
Monkeys whoop.
Dogs bark.

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.

POINTS TO REMEMBER
If you like to shorten this code, instead of using the if-else chain, you can use the switch expression as follows:
animal =
temp switch
{
  0 => new Tiger(),
  1 => new Dog(),
  _ => new Monkey()
};

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.

Now I’ll show you some code that helps you understand and use polymorphic code in an alternative way. You can replace animal.Sound(); with the following code:
AnimalProducer.MakeSound(animal);
MakeSound() is defined inside the AnimalProducer class as follows:
internal static void MakeSound(IAnimal animal)
{
  animal.Sound();
}
Why am I showing this to you? Following this approach, you can pass a supertype reference to this method to invoke the appropriate subtype method. This also gives you flexibility and helps you write better, more readable code. Here is an alternative version of the program that we have just discussed:
Console.WriteLine("***Sounds of the different
 animals.***");
IAnimal animal = AnimalProducer.GetAnimal();
AnimalProducer.MakeSound(animal);
animal = AnimalProducer.GetAnimal();
AnimalProducer.MakeSound(animal);
animal = AnimalProducer.GetAnimal();
AnimalProducer.MakeSound(animal);
interface IAnimal
{
    void Sound();
}
class Tiger : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Tigers roar.");
    }
}
class Dog : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Dogs bark.");
    }
}
class Monkey : IAnimal
{
    public void Sound()
    {
        Console.WriteLine("Monkeys whoop.");
    }
}
class AnimalProducer
{
    internal static IAnimal GetAnimal()
    {
        IAnimal animal;
        Random random = new Random();
        // Get a number between 0 and 3(exclusive)
        int temp = random.Next(0, 3);
        animal =
        temp switch
        {
            0 => new Tiger(),
            1 => new Dog(),
            _ => new Monkey()
        };
        return animal;
    }
   internal static void MakeSound(IAnimal animal)
    {
        animal.Sound();
    }
}
Note

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.

To implement a polymorphic behavior, I started with an interface. I could achieve the same result using an abstract class. In this case, you’d use the abstract and override keywords in the respective code segments. Here is a sample:
abstract class Animal
{
    public abstract void Sound();
}
class Tiger : Animal
{
    public override void Sound()
    {
        Console.WriteLine("Tigers roar.");
    }
}
But when you use a concrete parent class and want its derived classes to redefine its method(s), you see the use of virtual keywords in the parent class. Here is a sample:
class Animal
{
    public virtual void Sound()
    {
        Console.WriteLine("I make sounds.");
    }
}
class Tiger : Animal
{
    public override void Sound()
    {
        Console.WriteLine("Tigers roar.");
    }
}

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.

Note

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.

In this chapter, I discussed the code examples using C# interfaces. So, I’d like to point out one important change that came in C# 11. Consider the following code:
Console.WriteLine("Testing a C#11 feature");
public interface ISample
{
    static abstract void ShowInterfaceName();
}
If you run this code in C# 10, you’ll see the compile-time error. Here is a sample:
CS8703 The modifier 'abstract' is not valid for this item in C# 10.0. Please use language version 'preview' or greater.
This message is self-explanatory. So, you need to understand that this feature was planned for C# 11. At the same time, you also need to remember that only interface members that aren’t fields can be static abstract. So, if you write something like the following:
public interface ISample
{
    static abstract int SomeFlag; // ERROR CS0681
}

you’ll see the following error: CS0681 The modifier 'abstract' is not valid on fields. Try using a property instead.

In short, you can refer to the following code segment and keep the supporting comments in mind when you use a C# interface in your program:
public interface ISample
{
    //static abstract int SomeFlag1; // ERROR CS0681
    static int SomeFlag2=1; // OK, but warning message
                            // (CA 2211)
    // Interfaces cannot contain instance fields
    int _someFlag3; // ERROR CS0525
    const int SomeFlag3 = 3; // OK
    static abstract void ShowInterfaceName1(); // OK in C#11
    void ShowInterfaceName2(); // OK
}

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.

When you write the following:
Tiger tiger = new Tiger();
tiger.Sound();

you are programming to concrete implementation.

When you write the following:
IAnimal animal = new Tiger();
animal.Sound();

you are programming to a supertype. It is often referred to as programming to an interface.

Note

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.

You can follow a better approach when you write something like this:
IAnimal animal = AnimalProducer.GetAnimal();
animal.Sound();

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.

In short, this chapter answered the following questions:
  • 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”?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.92.199