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 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:

  1. 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;
  }
}
  1. Introduce your new classes with their behavior according to the requirement:
class Mage {
  ...
  float power() {
    return this.level * this.intellect;
  }
}
...
  1. (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.

2021-03-12
Written by Samuel de Vega.
Tags