I have read many tutorials on Polymorphism and all of them show examples of Polymorphism used in arrays
The arrays themselves are not really the point, the key thing is that polymorphism lets you treat different objects in the same way. An array just provides an easy/obvious example of that; e.g., if you loop over the elements, the code in the loop body is the same for every object, and that's possible because that code has no idea what the actual type of the object is. The interface1 of Animal
allows you to write it in such a way so that it doesn't have to know. All it requires is that all of the objects support speak()
.
I think what you're really asking is in what circumstances (other than arrays) such a situation arises. You'll find more opportunities to use polymorphism as you get more experienced.
A motivating example
One scenario that's common enough is when you have to support two pieces of functionality that both have the same overall logic, but differ in certain details. E.g., you might want to process some data, and have the ability to save the result in a couple of different file formats - say JSON and XML. You could, in principle, write the code twice, but you'll end up having a lot of repetition, and if you have to change something, you'll have to remember to do it in two places. You could also clean up the code a bit, and separate the part that saves the result into an if statement that saves in one or the other format. Maybe your method would accept some kind of a flag or an enumeration, and the if statement then decides what to do based on that:
ProcessDataAndExport(ExportFormats.JSON)
But, if you need to support another format, you'll need to edit the code again. And then again if you need another one. And again, and again.
Instead, you could separate out the saving logic to a different class. To use polymorphism, you come up with an abstraction - a base class, say Exporter
, that provides the abstract interface1 that ProcessDataAndExport
will use. BTW, "come up with an abstraction" just means that you define some type that lets you write the code of ProcessDataAndExport
in a way that doesn't require you to know about the concrete details of the export process (it's not unusual to not get this right on the first try - you may need to refine the Exporter class as you add support for new formats, but after 3 or 4 of them, it should become fairly stable).
class Exporter {
public abstract Export(MyDataType myData, string outputPath);
}
And then you can change ProcessDataAndExport to accept an exporter instead:
public ProcessDataAndExport(Exporter exporter) {
// process data (omitted)...
// no if anymore; the choice is not made in this method
exporter.Export(data, outputPath);
}
Then you can call it like this:
Exporter jsonExporter = new JsonExporter();
ProcessDataAndExport(jsonExporter);
Or like this:
Exporter xmlExporter = new XmlExporter();
ProcessDataAndExport(xmlExporter);
Or like this:
Exporter userSelectedExporer = ShowSelectExportFormatDialog();
ProcessDataAndExport(userSelectedExporer);
To support new formats, all you need to do is define another class deriving from exporter.
But wait, there's more! Here's something interesting; the concrete exporters don't even have to be defined in the same library (say, same EXE, DLL, or JAR, or whatever) as your ProcessDataAndExport
function. They could come from a plugin (or a number of plugins) that the application has loaded. Because ProcessDataAndExport
doesn't hardcode the formats, it can work with any of them.
TheShowSelectExportFormatDialog
has a way of discovering all the exporters that come from plugins and creating instances of them, but the code in ProcessDataAndExport
doesn't have to worry about that - it can treat all the exporters in the same way, just like the loop body in the array example.
With this, you can add support for new kinds of exporters without having to recompile/redeploy the library that has ProcessDataAndExport
in it (e.g., your users can just download the new plugins).
Discussion
Now, this whole thing is partially contrived, as in some languages, instead of an exporter, you may use something called a Serializer
. It also essentially converts your data/objects into some output format, it just works a little bit differently (e.g., it might use reflection to discover properties defined on your object, and write them out automatically).
But the thing is, in much the same way as described above, you can write your own serializer, for your own custom type, by creating a derived class. And that's possible even though the base type from which you're deriving is defined in a separate library that comes with the language. Furthermore, say you are using some 3rd-party framework that provides some operation that accepts a serializer. You can easily pass your own custom serializer to it, and it will polymorphically call it. The developers of that framework couldn't have predicted what kinds of formats you'd want; in fact, they'll probably never know about your class. Even so, their code works with your code - because it's written in a way that lets it treat all serializers, known and unknown, in the same way, again, just like the loop body in the array example.
Here's an example you might be familiar with; you may have used a GUI framework before. Often, you derive your own MyWindow
or MyForm
from a framework-defined base class (Window
, Form
). That framework has no idea about the details of your window class. But, if you override an inherited method, the framework will just call it polymorphically, and that will cause your version to run.
Another common use for polymorphism is to isolate objects for testing; e.g., when you write a test for some class, and you want to focus only on a certain aspect of it, you might want to pass in a fake object (that either does nothing or something test-specific) instead of the real one you'd use in production. Or, if the class makes a call to something that's inconvenient in the testing scenario (e.g., to a network service or a database), you can use polymorphism to prevent that, and maybe return some fake data.
There are all kinds of examples like this. Library functions that accept some interface type use polymorphism. Library functions that accept lambdas also use polymorphism in much the same way (their code doesn't know the details of the concrete function it accepts). As another answer mentioned, many design patterns rely on a combination of polymorphism and composition to achieve some design goal.
P.S. Also, if one day it turns out that you need to support different data processing algorithms, you may end up with something like this instead:
ProcessDataAndExport(dataProcessor, exporter);
Now ProcessDataAndExport
just makes polymorphic calls to the two objects and coordinates the overall flow. Concrete DataProcessor
is concerned with the specific processing algorithm, and it returns some result, that ProcessDataAndExport
then passes on to a concrete Exporter
, that's in turn responsible for producing the output.
1 By "interface" I just mean the set of public methods/properties on the base type (this is the general meaning of the word); I don't mean specifically the thing declared by interface
keyword in languages like C# and Java (although that defines an interface in this sense as well).