Object-oriented: Procedural polymorphism
About polymorphism in object-oriented design
When we talk about polymorphism, we mean that objects of different classes are capable of responding to syntactically the same messages (same signature) regardless of their internal behavior. Depending on the language in which we find ourselves, polymorphism will be implemented in a certain way, such as through interfaces.
A simple example can be the response to asking about the area of objects whose classes represent the geometric figures of a circle, a square and a triangle:
interface Figure {
float area();
}
class Circle implements Figure {
...
float area() {
return Math.PI * Math.pow(this.radius, 2);
}
}
class Square() implements Figure {
...
float area() {
return Math.pow(this.side, 2);
}
}
class Triangle() implements Figure {
...
float area() {
return this.base * this.high / 2;
}
}
In this way, the objects that communicate with those generated by these classes will only need to know that they are talking to geometric figures, thus promoting a more cohesive design.
Procedural polymorphism
We can understand procedural polymorphism as the lack in our design of an object-oriented polymorphism where there is a clear opportunity to apply it.
Sometimes we may come across a set of data in which one of them indicates a type. This can occur for various reasons such as:
- The data comes from the persistence layer or from outside the system, since its source may not work with the object-oriented paradigm.
- The design has been evolving and that opportunity for polymorphism has gone unnoticed.
- The project was in a premature phase and at that time we were not sure enough that polymorphism was the correct tool to apply. Let us remember that every time we introduce a new abstraction we are adding complexity to the design, which is why said abstraction needs to fulfill an improvement purpose (communicate, avoid duplication of intention, etc.) that justifies its use.
The following case is a clear example of procedural polymorphism.
Example
We have a role-playing game and in our initial design there were only two character types: warrior and archer. We didn’t know how the game would evolve and that’s why we hadn’t applied object-oriented polymorphism yet, leaving us with code like this:
class Character {
private string type;
...
float power() {
return (this.type.equals("warrior"))
? this.level * this.strength
: this.level * this.speed;
}
}
Clearly we are conditioning the behavior of each instance of our Character
class based on the value of its type
property.
Now let’s imagine that a new requirement has appeared and we need to evolve the design to support a new set of character types. At this point we clearly see that this condition in our code is replacing an object-oriented polymorphism.
The steps to replace procedural polymorphism are:
- Take each possible result of the current condition to a new class with a method of the same signature, making the existence of the
type
property unnecessary.
class Warrior {
...
float power() {
return this.level * this.strength;
}
}
class Archer {
...
float power() {
return this.level * this.speed;
}
}
- Introduce your new classes with their behavior according to the requirement:
class Mage {
...
float power() {
return this.level * this.intellect;
}
}
...
- (Optional) If your language requires it, create a common interface for the classes, thus limiting the impact of the change on the rest of the design. You can probably reuse the original class name:
interface Character {
...
float power();
}
class Warrior implements Character {
...
}
...
If we also combine this change with other techniques such as the pattern Factory Method, we will be able to isolate the construction of each object of a specific class in a single point of our design and the rest of the objects will interact with them knowing only their interface, thus complying with the Open/Closed principle.
Conclusion
Whenever we are faced with a property on whose value the behavior of the class depends, we are facing a case of procedural polymorphism. Property names such as type
, kind
, _Type
, _Class
, etc., are clear candidates that should set off our alarm bells to detect this lack in the design, be aware of it and analyze if we are in a point at which it pays and is justified to refactor.
Written by Samuel de Vega.