The Problem: Rigid Code
Imagine you have different kinds of things that behave similarly, but not exactly the same. Without polymorphic dispatch, you end up with long if/else or switch that grows each time you add a new type.
// fragile, every new animal means editing this function
function makeSound(animalType) {
if (animalType === 'dog') console.log('woof');
else if (animalType === 'cat') console.log('meow');
// ... add another 'if' for each animal
}
You don't need to know OOP theory - just notice the pain.
The Solution: A Promise (Contract)
We want each object to own its behaviour. Polymorphism means “many forms” - we just need a common guarantee: every animal knows how to speak.
In many languages we use an interface or a base class. But in dynamic languages (JS, Python) we just agree on method names. Let's see it in JavaScript (works everywhere).
// a “contract” - if you have a .speak() method, you're an animal
const dog = {
speak: () => console.log('woof')
};
const cat = {
speak: () => console.log('meow')
};
Both objects dispatch polymorphically: the caller doesn't check type, it just calls .speak().
Polymorphic Call = No Conditionals
Write one function that works for any object that follows the contract.
function performSpeak(animal) {
animal.speak(); // dispatch - dynamic decision
}
performSpeak(dog); // woof
performSpeak(cat); // meow
// add a new animal without touching existing code
const bird = {
speak: () => console.log('chirp')
};
performSpeak(bird); // chirp
This is the heart of polymorphic dispatch: same call, different behaviour.
Classes (Optional but Clean)
If you work in a large codebase, classes help create many objects with the same interface. Still zero if/else.
class Animal {
constructor(name) { this.name = name; }
speak() { throw new Error('subclass must implement'); } // "abstract"
}
class Dog extends Animal {
speak() { console.log(`${this.name} says woof`); }
}
class Cat extends Animal {
speak() { console.log(`${this.name} says meow`); }
}
const animals = [new Dog('Bella'), new Cat('Luna')];
animals.forEach(a => a.speak()); // Bella says woof / Luna says meow
No type checks, no switches - adding new Cow doesn't break anything.
Production-ready: Payment Processors
E-commerce: different payment gateways. Polymorphism keeps it rock solid.
class PayPalProcessor {
process(amount) {
// real API logic here
return `PayPal: charged $${amount}`;
}
}
class StripeProcessor {
process(amount) {
return `Stripe: charged $${amount} (fee 2.9%)`;
}
}
class CashProcessor {
process(amount) {
return `Cash: collect $${amount} (no fee)`;
}
}
// checkout function - completely open to new processors
function checkout(paymentMethod, amount) {
console.log(paymentMethod.process(amount));
}
// usage - no matter what processor you add later
checkout(new PayPalProcessor(), 99);
checkout(new StripeProcessor(), 45);
checkout(new CashProcessor(), 20);
New CryptoProcessor? just pass it. Zero changes in checkout.
This pattern is used in frameworks, drivers, middlewares - everywhere.
Polymorphism Without Inheritance
You don't even need classes. JavaScript objects can be created on the fly - as long as the method name matches, dispatch works.
const logger = {
log: (msg) => console.log(`${msg}`)
};
const alertSystem = {
log: (msg) => console.log(`ALERT: ${msg}`)
};
function write(device, message) {
device.log(message); // polymorphic dispatch
}
write(logger, 'server started'); // server started
write(alertSystem, 'CPU overload'); // ALERT: CPU overload
That's it - you already understand polymorphic dispatch.
Your Polymorphic Toolkit
- Define a contract (method name + signature) - informally or via interface/abstract class
- Implement the contract in each concrete object/class
- Call the method without checking type - let the language dispatch it
- Extend forever: new implementations, zero changes to existing code