There are different kinds of polymorphism, the one of interest is usually runtime polymorphism/dynamic dispatch.
A very high-level description of runtime polymorphism is that a method call does different things depending on the runtime type of its arguments: the object itself is responsible for resolving a method call. This allows for a huge amount of flexibility.
One of the most common ways to use this flexibility is for dependency injection, e.g. so that I can switch between different implementations or to inject mock objects for testing. If I know in advance that there will only be a limited number of possible choices I could try to hardcode them with conditionals, e.g.:
void foo() {
if (isTesting) {
... // do mock stuff
} else {
... // do normal stuff
}
}
This makes the code hard to follow. The alternative is to introduce an interface for that foo-operation and write a normal implementation and a mock implementation of that interface, and “injecting” to desired implementation at runtime. “Dependency injection” is a complicated term for “passing the correct object as an argument”.
As a real-world example, I am currently working on a kind machine-learning problem. I have an algorithm that requires a prediction model. But I want to try out different machine learning algorithms. So I defined an interface. What do I need from my prediction model? Given some input sample, the prediction and its errors:
interface Model {
def predict(sample) -> (prediction: float, std: float);
}
My algorithm takes a factory function that trains a model:
def my_algorithm(..., train_model: (observations) -> Model, ...) {
...
Model model = train_model(observations);
...
y, std = model.predict(x)
...
}
I now have various implementations of the model interface and can benchmark them against each other. One of these implementations actually takes two other models and combines them into a boosted model. So thanks to this interface:
- my algorithm doesn't need to know about specific models in advance,
- I can easily swap out models, and
- I have a lot of flexibility in implementing my models.
A classic use case of polymorphism is in GUIs. In a GUI framework like Java AWT/Swing/… there are different components. The component interface/base class describes actions such as painting itself to the screen, or reacting to mouse clicks. Many components are containers that manage sub-components. How might such a container draw itself?
void paint(Graphics g) {
super.paint(g);
for (Component child : this.subComponents)
child.paint(g);
}
Here, the container doesn't need to know about the exact types of the subcomponents in advance – as long as they conform to the Component
interface the container can simply call the polymorphic paint()
method. This gives me the freedom to extend the AWT class hierarchy with arbitrary new components.
There are many recurring problems throughout software development that can be solved by applying polymorphism as a technique. These recurring problem–solution pairs are called design patterns, and some of them are collected in the book of the same name. In the terms of that book, my injected machine learning model would be a strategy that I use to “define a family of algorithms, encapsulate each one, and make them interchangeable”. The Java-AWT example where a component can contain sub-components is an example of a composite.
But not every design needs to use polymorphism (beyond enabling dependency injection for unit testing, which is a really good use case). Most problems are otherwise very static. As a consequence, classes and methods are often not used for polymorphism, but simply as convenient namespaces and for the pretty method call syntax. E.g. many developers prefer method calls like account.getBalance()
over a largely equivalent function call Account_getBalance(account)
. That's a perfectly fine approach, it's just that many “method” calls have nothing to do with polymorphism.
if(x is SomeType) DoSomething()
often, it may be worth using polymorphism. For me polymorphism is a decision similar to when to make a separate method, if I found that I repeated the code a few times I usually refactor it into a method, and if I find that I'm either makingif object is this type do this
code often, it might be worth refactoring and adding an interface or class.