February 26, 2023

What is Polymorphism?

Like Encapsulation, Polymorphism is a concept. We use the features of a programming language to implement it—each language can provide different ways to implement it, but the concept remains the same.

Although it’s commonly applied using objects in OOP (object-oriented programming), it’s not exclusive to it. You can apply it using functions in JavaScript, for example—actually I’ll start with an example on how to apply it using variables.

First, I’ll show you how to apply polymorphism in your code, then I’ll explain why you need to use it and what benefits it provides you with.

I’ll start with a definition here, but I don’t expect you to understand it now—it should become clear to you after finishing this article.

Polymorphism is an element (object, function, or a variable) that has multiple implementations of the same interface, for which other parts of your code use without knowing which implementation it has.

What is an interface?

There’s an interface for any code you use. By the code you use, I mean a variable you read, a function you call, or a method you call on a class.

The interface of a function consists of the name of the function and its parameters. When you use a function, you don’t need to know the code in its body.

The interface for a class is the public methods and fields it contains.

A variable also has an interface: its name and type. You don’t need to know its value to use it; you just need to know its name and type (especially in statically typed languages).

Polymorphism with variables

It’s not actually a thing we think about explicitly as polymorphism; but since polymorphism is a concept, then anything applies the ideas behind that concept will be true.

Let me show you a simple example about this.

Let’s say you have a way to get the percentage of messages your users open in the last week. The goal is to display that value after rounding it with Math.round.

const openRate = getOpenRate()
console.log(Math.round(openRate))

When I used Math.round here I assumed that openRate contains a number, but I don’t know what the number is—Math.round doesn’t care what the value is as long as openRate is any valid number.

So, it’s polymorphism because the interface is the same—openRate—but it can have different possible values.

Polymorphism with functions

In this example, I need to log the details of a user object. I have different ways to format the data when logging it. Here are two examples:

const user = {
  name: 'Jane Doe',
  email: '[email protected]',
  age: 29
}

function logUserInASingleLine(user) {
  console.log(`name: ${user.name}, email: ${user.email}, age: ${user.age}`)
}

function logUserInMultipleLines(user) {
  console.log(`name: ${user.name}\r\nemail: ${user.email}\r\nage: ${user.age}`)
}

I have a main function that wants to log the user data using one of these log methods. I have another function called preferredUserLog to return the preferred log function—Let’s say in this case it’s logUserInMultipleLines.

const user = {
  name: 'Jane Doe',
  email: '[email protected]',
  age: 29
}

function preferredUserLog() {
  return logUserInMultipleLines
}

function main() {
  const log = preferredUserLog() // log is the logUserInMultipleLines function

  log(user) // it will log using logUserInMultipleLines
}

If I need to use the other log type, I just need to modify preferredUserLog to return logUserInASingleLine. After that simple update, main will still work as expected without updating it because of polymorphism.

The main function just uses the log function without knowing what implementation log contains. It will keep working as long as log has the same interface—and in this case it does because the chosen function takes a user parameter.

I don’t need to worry about the name of the chosen function because it gets renamed to log; so, the interface remains the same from the main point of view.

Polymorphism with classes

This is the main way polymorphism is usually used in. It’s similar to the functions case above, but instead of unifying the parameter list, you unify the class’s public methods and fields used in the client code—by client code I mean the code using the object.

There are countless of complex examples I can show you when applying polymorphism in classes, but I’ll use a very simple one for demonstration purposes.

In this example, I have two post classes: PublishedPost and DraftPost. Both classes inherit from Post that has a method called canEdit—inheritance is not the point here although inheritance and polymorphism are usually seen together because inheritance makes subclasses use the same methods—which means they have the same interface, and this is what polymorphism is all about.

The canEdit returns a boolean to determine if a post can be edited. In this example, draft posts can be edited, but published posts can’t.

// In this example, I'm treating Post as an abstract class that should not be instantiated.
class Post {
  this.#postData
  constructor(postData) {
    this.#postData = postData
  }
  canEdit() {}
}

class PublishedPost extends Post {
  constructor(postData) {
    super(postData)
  }

  canEdit() {
    return false
  }
}

class DraftPost extends Post {
  constructor(postData) {
    super(postData)
  }

  canEdit() {
    return true
  }
}

I want to display a message in the console based on the value of canEdit. So, in the main function I’ll get the post object from somewhere and call its canEdit method to determine what message to display.

function main() {
  const post = getLatestPost()
  if (post.canEdit()) {
    console.log('your latest post can be edited')
  } else {
    console.log('your latest post cannot be edited')
  }
}

I got that post object from a function called getLatestPost. In this example, I assume that getLatestPost will return the latest post as an object of either type: PublishedPost or DraftPost. Maybe it fetches the latest post from the database and creates the correct object based on its published date value. If it has a published date, then it should be a PublishedPost type—otherwise, it will be DraftPost.

So, for example, it might look like this:

function getLatestPost() {
  const postData = fetchLatestPostFromDatabase()
  if (postData.publishedDate) {
    return new PublishedPost(postData)
  } else {
    return new DraftPost(postData)
  }
}

The polymorphism case in this example is very clear. In the main function I call canEdit without knowing the exact type of the post—it could be DraftPost or PublishedPost. Swapping post types (or even adding a new one) doesn’t change how main works as long as they are using the same interface—in this case all post objects should have a canEdit method.

Why polymorphism?

You might have noticed that in all the examples above I don’t have to update the client code (the code that uses the class or the function) when changing which object or function to use in it.

In the last example, I can add new post types or change the logic that chooses what post type to create without changing the code using it. In other words the main function remains the same regardless what post it’s using. The main function just needs to use the canEdit method, and it doesn’t care what post type it’s dealing with.

So the main reason for polymorphism is decoupling. In the last example, the main function is decoupled from the post type it’s dealing with—it’s just dealing with a post.

Nowhere in the code can you see a check for the type of the post. So, I have less conditionals, which is always good because conditionals add more coupling to your code. Imagine you have ten different places in your code where you want to check for the post type before doing something in the code. If you add a new post type, then you have to add a new check in each of these ten places for that new type. But with polymorphism you don’t need to worry about that because all client code would remain the same—that’s why you will often hear the advice “replace conditional with polymorphism”.

Stay up-to-date on the latest projects and articles from me