📓 APIE: Polymorphism
We have one last principle of Object-Oriented Programming (OOP) to learn about: polymorphism! As a reminder, polymorphism is one of four principles of OOP that we can remember by the easy acronym APIE:
- Abstraction
- Polymorphism
- Inheritance
- Encapsulation
The word "polymorphism" comes from Greek and it means "many forms". In terms of programming, polymorphism happens when an object or method has multiple forms, but is identified by the same name.
Polymorphism
Let's start with an example. What follows is a Triangle
class adapted from our Shape Tracker app. This Triangle
class has an overloaded constructor so that we can create a Triangle
object with a single value, or we can create a Triangle
object with three values, one for each side:
public class Triangle
{
// Auto-Implemented Properties
public int Side1 { get; set; }
public int Side2 { get; set; }
private int Side3 { get; set; }
public Triangle(int length)
{
Side1 = length;
Side2 = length;
Side3 = length;
}
public Triangle(int length1, int length2, int length3)
{
Side1 = length1;
Side2 = length2;
Side3 = length3;
}
}
When we use an overloaded constructor, we're using polymorphism: we have two different Triangle()
methods, with just one name, but with two different behaviors.
More specifically, this is called compile-time polymorphism, because the compiler will sort out which Triangle()
method to use. So, when we call the following Triangle()
method:
class Program
{
static void Main()
{
Triangle tri = new Triangle(3);
}
}
The compiler will first search for the Triangle
class constructor methods, and then determine which is the correct version to use; if the compiler can't find one, it will throw an error.
And that's as simple as it gets: polymorphism happens when an object or method has multiple forms and behaviors.
There's also run-time polymorphism in which the exact object type or method to run is determined when our program is running. Let's look at an example.
This next example will build on the previous example and involve inheritance:
public class Shape
{
public virtual int CalculateArea()
{
return 0;
}
}
public class Triangle : Shape
{
// Auto-Implemented Properties
public int Side1 { get; set; }
public int Side2 { get; set; }
private int Side3 { get; set; }
public Triangle(int length)
{
Side1 = length;
Side2 = length;
Side3 = length;
}
public Triangle(int length1, int length2, int length3)
{
Side1 = length1;
Side2 = length2;
Side3 = length3;
}
public override int CalculateArea()
{
return Side1 * Side2 * Side3;
}
}
Now we've created a new Shape
class that extends the Triangle
class, giving it access to the virtual CalculateArea()
method.
Within the Triangle
class, we've overridden the same CalculateArea()
method. Based on what we learned in the previous lesson about the override
modifier, we know it allows us to declare a new value for an inherited virtual
method, and this is exactly what we're doing.
Now, when we invoke shp.CalculateArea();
in the following code, do you think it will return 8
or 0
? Take a moment to review the code and make a guess. Notice that we create a new Triangle
object, then a new Shape
object that is assigned tri
, the original Triangle
object, as a value.
class Program
{
static void Main()
{
Triangle tri = new Triangle(2);
Shape shp = tri;
int area = shp.CalculateArea();
// Will 'area' be equal to 8 or 0?
System.Console.WriteLine(area);
}
}
Determining whether shp.CalculateArea()
should invoke the Triangle.CalculateArea()
method and return 8
, or the Shape.CalculateArea()
method and return 0
, is an example of polymorphism. Specifically, this is an example of run-time polymorphism, where exactly which method should be called is determined while our program is running.
When we run the above code, we'll get 8
returned to us. That's because while our program is running it determined that the Triangle.CalculateArea()
method has overridden the Shape.CalculateArea()
method and because of this, the Triangle.CalculateArea()
method takes precedence.
If we did not want the original Shape.CalculateArea()
method to be overridden, we can update our Triangle.CalculateArea()
method declaration to use the new
modifier to indicate that it is a new method that is separate from the inherited Shape.CalculateArea()
method:
public class Triangle : Shape
{
... // other logic
public new int CalculateArea()
{
return Side1 * Side2 * Side3;
}
}
Now if we re-run our program code, as-is, without making any additional changes:
class Program
{
static void Main()
{
Triangle tri = new Triangle(2);
Shape shp = tri;
int area = shp.CalculateArea();
System.Console.WriteLine(area);
}
}
We'll get 0
returned to us, because at run time our program has determined that the shp.CalculateArea()
is in fact invoking the base class Shape.CalculateArea()
. This is happening because the Triangle.CalculateArea()
method is declared as new
and no longer overrides the base class Shape.CalculateArea()
method.
Is this a little confusing? Well, that's not unexpected with polymorphism since it is all about dealing with an object or method that has multiple forms, but is identified by the same name. Fortunately, it's unlikely that you'll encounter code like we saw in the last example of run time polymorphism, as it is a contrived example. As long as you structure inheritance intentionally in your apps and write tests for your code, any errors that arise from polymorphism will get identified quickly, and you'll be able to resolve those errors just as quickly.
Remember that you are not required to use polymorphism for this section's independent project. As always, we encourage you to experiment with it and try it in your code if possible.