Introduction to Polymorphism
Imagine you're at a restaurant. When you order a burger, whether it's a beef burger, veggie burger, or chicken burger - you still use the same action: "order a burger." Yet, what you get can be completely different! This is polymorphism in real life - same interface, different implementations.
In programming terms, polymorphism (from Greek words meaning "many forms") allows us to perform a single action in different ways. It's like having a universal remote control that can operate different devices, each responding in its own way to the same button press.
Types of Polymorphism
Function Overloading
Think of a Swiss Army knife - one tool name ("knife") but different uses based on which attachment you use. In programming, this is function overloading:
class Calculator {
// Add two numbers
sum(a, b) {
return a + b;
}
// Add an array of numbers
sum(numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
}
Function Overriding
Imagine different artists drawing the same scene - each will have their own style, but they're all creating art. In code:
class Animal {
makeSound() {
return "Some sound";
}
}
class Dog extends Animal {
makeSound() {
return "Woof!";
}
}
class Cat extends Animal {
makeSound() {
return "Meow!";
}
}
Hands-on Exercise: Building a Shape Calculator
Let's create a practical example that you can follow along with:
class Shape {
calculateArea() {
throw new Error("Method 'calculateArea()' must be implemented");
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
// Using our shapes
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);
console.log(circle.calculateArea()); // 78.54...
console.log(rectangle.calculateArea()); // 24
Real-World Applications
User Interface Components
Modern web frameworks use polymorphism extensively. Consider a Button component:
class Button {
render() {
return ``;
}
}
class PrimaryButton extends Button {
render() {
return ``;
}
}
class DangerButton extends Button {
render() {
return ``;
}
}
Payment Processing Systems
Another practical example is payment processing:
class PaymentProcessor {
processPayment(amount) {
throw new Error('Must implement processPayment');
}
}
class CreditCardProcessor extends PaymentProcessor {
processPayment(amount) {
// Credit card specific logic
return `Processing $${amount} via Credit Card`;
}
}
class PayPalProcessor extends PaymentProcessor {
processPayment(amount) {
// PayPal specific logic
return `Processing $${amount} via PayPal`;
}
}
Best Practices and Tips
- Always use meaningful method names that reflect the action being performed
- Keep the method signature consistent across all implementations
- Follow the Liskov Substitution Principle - subclasses should be substitutable for their base classes
- Use polymorphism to reduce conditional logic in your code
Related Topics to Explore
- Interface Segregation Principle
- Abstract Classes vs Interfaces
- Duck Typing
- Design Patterns (Strategy, Template Method, Factory)
Practice Exercise
Create a notification system that can send messages through different channels (email, SMS, push notification). Each channel should implement a common interface but handle the delivery differently.
// Start with this base class
class NotificationChannel {
send(message) {
throw new Error('Must implement send method');
}
}
// Your task: Implement EmailChannel, SMSChannel, and PushNotificationChannel
Further Reading
- Design Patterns: Elements of Reusable Object-Oriented Software
- Clean Code by Robert C. Martin
- Head First Design Patterns