Polymorphism: The Art of One Interface, Multiple Implementations
What is Polymorphism
Imagine walking into a smart home showroom where there's a row of switches on the wall. Each switch looks the same, with an "on/off" button, but they control completely different devices: one controls lights, another controls air conditioning, another controls speakers, and yet another controls curtains. You don't need to learn four different operation methods; you just need to know "press the switch," and the specific device will respond accordingly.
This is the essence of polymorphism: "one interface, multiple implementations." Although all switches provide the same interface (button), each switch connects to a different device and performs different operations. In programming, polymorphism allows us to handle different types of objects in a unified way, with each object responding appropriately based on its specific type.
Basic Principles of Polymorphism
The word polymorphism comes from Greek, meaning "many forms." In object-oriented programming, polymorphism allows us to:
- Unified Interface: Define common method names
- Different Implementations: Each subclass provides its unique implementation
- Dynamic Calling: Call the appropriate method at runtime based on the object's actual type
Let's understand polymorphism through a simple example:
// Base class defines common interface
class Animal {
constructor(name) {
this.name = name;
}
// Common method - to be overridden by subclasses
makeSound() {
return "Some generic animal sound";
}
introduce() {
return `I am ${this.name} and I say: ${this.makeSound()}`;
}
}
// Different subclasses, different implementations
class Dog extends Animal {
makeSound() {
return "Woof! Woof!";
}
}
class Cat extends Animal {
makeSound() {
return "Meow~";
}
}
class Cow extends Animal {
makeSound() {
return "Moo!";
}
}
class Duck extends Animal {
makeSound() {
return "Quack! Quack!";
}
}
// The power of polymorphism: unified handling
function performConcert(animals) {
console.log("🎵 Animal Concert Begins! 🎵\n");
for (const animal of animals) {
// Same call to makeSound(), but each animal makes different sounds
console.log(animal.introduce());
}
console.log("\n👏 Concert Ends!");
}
// Create different types of animals
const performers = [
new Dog("Max"),
new Cat("Whiskers"),
new Cow("Bessie"),
new Duck("Donald"),
];
// Unified handling, each animal automatically uses its own implementation
performConcert(performers);
// 🎵 Animal Concert Begins! 🎵
//
// I am Max and I say: Woof! Woof!
// I am Whiskers and I say: Meow~
// I am Bessie and I say: Moo!
// I am Donald and I say: Quack! Quack!
//
// 👏 Concert Ends!The key point is that the performConcert function doesn't need to know the specific type of each animal; it only needs to know that "all animals have a makeSound() method." Each animal object automatically uses the makeSound() implementation defined in its own class.
Method Overriding
Method overriding is the primary way to achieve polymorphism. Subclasses can provide different implementations of methods with the same name as the parent class:
class Shape {
constructor(name, color) {
this.name = name;
this.color = color;
}
// Common methods - to be implemented by subclasses
getArea() {
throw new Error(`${this.constructor.name} must implement getArea() method`);
}
getPerimeter() {
throw new Error(`${this.constructor.name} must implement getPerimeter() method`);
}
// Common method - shared by all subclasses
describe() {
return `This is a ${this.color} ${this.name}`;
}
// Common method that uses polymorphic methods
getInfo() {
return {
name: this.name,
color: this.color,
area: this.getArea(),
perimeter: this.getPerimeter(),
description: this.describe(),
};
}
}
class Circle extends Shape {
constructor(radius, color = "red") {
super("Circle", color);
this.radius = radius;
}
// Override getArea - circle-specific calculation
getArea() {
return Math.PI * this.radius ** 2;
}
// Override getPerimeter - circle-specific calculation
getPerimeter() {
return 2 * Math.PI * this.radius;
}
// Override describe - add circle-specific information
describe() {
return `${super.describe()}, radius ${this.radius}`;
}
}
class Rectangle extends Shape {
constructor(width, height, color = "blue") {
super("Rectangle", color);
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
getPerimeter() {
return 2 * (this.width + this.height);
}
describe() {
return `${super.describe()}, width ${this.width}, height ${this.height}`;
}
// Rectangle-specific method
isSquare() {
return this.width === this.height;
}
}
class Triangle extends Shape {
constructor(a, b, c, color = "green") {
super("Triangle", color);
this.a = a;
this.b = b;
this.c = c;
}
getArea() {
// Heron's formula
const s = this.getPerimeter() / 2;
return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
}
getPerimeter() {
return this.a + this.b + this.c;
}
describe() {
return `${super.describe()}, sides ${this.a}, ${this.b}, ${this.c}`;
}
}
// Polymorphic application: calculate total area of multiple shapes
function calculateTotalArea(shapes) {
console.log("📐 Shape Area Statistics 📐\n");
let totalArea = 0;
for (const shape of shapes) {
const info = shape.getInfo();
console.log(`${info.description}`);
console.log(` Area: ${info.area.toFixed(2)}`);
console.log(` Perimeter: ${info.perimeter.toFixed(2)}\n`);
totalArea += info.area;
}
return totalArea;
}
const shapes = [
new Circle(5, "red"),
new Rectangle(10, 6, "blue"),
new Triangle(3, 4, 5, "green"),
new Circle(3, "yellow"),
];
const total = calculateTotalArea(shapes);
console.log(`📊 Total Area: ${total.toFixed(2)}\n`);The Power of Interface Unification
The greatest advantage of polymorphism is that it allows us to write generic code without caring about the specific type of objects:
// Define payment interface (through base class)
class PaymentMethod {
constructor(name) {
this.name = name;
}
// All payment methods must implement this method
processPayment(amount) {
throw new Error("processPayment() must be implemented");
}
// Optional validation method
validate() {
throw new Error("validate() must be implemented");
}
getInfo() {
return {
method: this.name,
type: this.constructor.name,
};
}
}
// Various specific payment methods
class CreditCard extends PaymentMethod {
constructor(cardNumber, cvv, expiryDate) {
super("Credit Card");
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}
validate() {
// Simplified validation logic
if (this.cardNumber.length !== 16) {
throw new Error("Credit card number must be 16 digits");
}
if (this.cvv.length !== 3) {
throw new Error("CVV must be 3 digits");
}
const expiry = new Date(this.expiryDate);
if (expiry < new Date()) {
throw new Error("Credit card has expired");
}
return true;
}
processPayment(amount) {
this.validate();
console.log(`Processing $${amount} payment via credit card...`);
console.log(`Card: ****-****-****-${this.cardNumber.slice(-4)}`);
// Simulate processing delay
return {
success: true,
method: this.name,
amount,
transactionId: `CC-${Date.now()}`,
timestamp: new Date(),
};
}
}
class PayPal extends PaymentMethod {
constructor(email, password) {
super("PayPal");
this.email = email;
this.password = password;
}
validate() {
if (!this.email.includes("@")) {
throw new Error("Invalid email address");
}
return true;
}
processPayment(amount) {
this.validate();
console.log(`Processing $${amount} payment via PayPal...`);
console.log(`Account: ${this.email}`);
return {
success: true,
method: this.name,
amount,
transactionId: `PP-${Date.now()}`,
timestamp: new Date(),
};
}
}
class Bitcoin extends PaymentMethod {
constructor(walletAddress) {
super("Bitcoin");
this.walletAddress = walletAddress;
this.exchangeRate = 50000; // Simplified exchange rate
}
validate() {
if (this.walletAddress.length < 26) {
throw new Error("Invalid wallet address");
}
return true;
}
processPayment(amount) {
this.validate();
const btcAmount = (amount / this.exchangeRate).toFixed(8);
console.log(`Processing $${amount} payment via Bitcoin...`);
console.log(`BTC amount: ${btcAmount} BTC`);
console.log(`Wallet address: ${this.walletAddress.slice(0, 8)}...`);
return {
success: true,
method: this.name,
amount,
btcAmount,
transactionId: `BTC-${Date.now()}`,
timestamp: new Date(),
};
}
}
// Order processing system - polymorphic application
class OrderProcessor {
constructor() {
this.orders = [];
}
// This method accepts any payment method
processOrder(items, paymentMethod) {
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
console.log("\n=== Processing Order ===");
console.log(`Items: ${items.length}`);
console.log(`Total: $${total}`);
console.log(`Payment method: ${paymentMethod.name}\n`);
try {
// Polymorphism! No matter what payment method, call processPayment
const result = paymentMethod.processPayment(total);
const order = {
orderId: `ORD-${Date.now()}`,
items,
total,
payment: result,
status: "completed",
};
this.orders.push(order);
console.log(`\n✓ Payment successful!`);
console.log(`Order ID: ${order.orderId}`);
console.log(`Transaction ID: ${result.transactionId}\n`);
return order;
} catch (error) {
console.log(`\n✗ Payment failed: ${error.message}\n`);
return null;
}
}
getOrderHistory() {
return this.orders;
}
}
// Usage example
const processor = new OrderProcessor();
const items = [
{ name: "Laptop", price: 999, quantity: 1 },
{ name: "Mouse", price: 29, quantity: 2 },
];
// Use different payment methods, but processing code is identical
const creditCard = new CreditCard("1234567890123456", "123", "2025-12");
processor.processOrder(items, creditCard);
const paypal = new PayPal("[email protected]", "password");
processor.processOrder(items, paypal);
const bitcoin = new Bitcoin("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
processor.processOrder(items, bitcoin);Duck Typing
As a dynamic language, JavaScript supports "duck typing"—"if it walks like a duck and quacks like a duck, then it's a duck." Objects don't need to inherit from a specific class; they just need to have the corresponding methods:
// Different classes, no common parent class
class EmailNotification {
constructor(email) {
this.email = email;
}
send(message) {
console.log(`📧 Sending email to ${this.email}: ${message}`);
return { method: "email", recipient: this.email, status: "sent" };
}
}
class SMSNotification {
constructor(phoneNumber) {
this.phoneNumber = phoneNumber;
}
send(message) {
console.log(`📱 Sending SMS to ${this.phoneNumber}: ${message}`);
return { method: "sms", recipient: this.phoneNumber, status: "sent" };
}
}
class PushNotification {
constructor(deviceId) {
this.deviceId = deviceId;
}
send(message) {
console.log(`🔔 Push notification to device ${this.deviceId}: ${message}`);
return { method: "push", recipient: this.deviceId, status: "sent" };
}
}
class SlackNotification {
constructor(channel) {
this.channel = channel;
}
send(message) {
console.log(`💬 Sending Slack message to #${this.channel}: ${message}`);
return { method: "slack", recipient: this.channel, status: "sent" };
}
}
// Notification manager - doesn't care about specific type, just needs send method
class NotificationManager {
constructor() {
this.channels = [];
}
addChannel(channel) {
// Check if it has send method (duck typing check)
if (typeof channel.send === "function") {
this.channels.push(channel);
} else {
throw new Error("Notification channel must have send method");
}
}
// Polymorphism: send notifications to all channels
broadcast(message) {
console.log(`\n📢 Broadcasting message: "${message}"\n`);
const results = [];
for (const channel of this.channels) {
// Polymorphic call: each channel sends in its own way
const result = channel.send(message);
results.push(result);
}
console.log(`\n✓ Message sent through ${results.length} channels\n`);
return results;
}
sendToSpecific(message, filter) {
const filtered = this.channels.filter(filter);
console.log(`\n📤 Sending to specific channels\n`);
for (const channel of filtered) {
channel.send(message);
}
}
}
// Usage
const manager = new NotificationManager();
manager.addChannel(new EmailNotification("[email protected]"));
manager.addChannel(new SMSNotification("+1234567890"));
manager.addChannel(new PushNotification("device-abc123"));
manager.addChannel(new SlackNotification("general"));
// Broadcast to all channels
manager.broadcast("System will be down for maintenance in 5 minutes");
// Send to specific channels
manager.sendToSpecific(
"Urgent security update",
(channel) =>
channel instanceof EmailNotification || channel instanceof SMSNotification
);Strategy Pattern: Practical Application of Polymorphism
The strategy pattern is a classic application of polymorphism, allowing algorithm selection at runtime:
// Sorting strategy interface
class SortStrategy {
sort(array) {
throw new Error("sort() must be implemented");
}
getName() {
throw new Error("getName() must be implemented");
}
}
// Specific strategy: Bubble sort
class BubbleSort extends SortStrategy {
sort(array) {
const arr = [...array];
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
getName() {
return "Bubble Sort";
}
}
// Specific strategy: Quick sort
class QuickSort extends SortStrategy {
sort(array) {
if (array.length <= 1) return array;
const pivot = array[Math.floor(array.length / 2)];
const left = array.filter((x) => x < pivot);
const middle = array.filter((x) => x === pivot);
const right = array.filter((x) => x > pivot);
return [...this.sort(left), ...middle, ...this.sort(right)];
}
getName() {
return "Quick Sort";
}
}
// Specific strategy: Merge sort
class MergeSort extends SortStrategy {
sort(array) {
if (array.length <= 1) return array;
const mid = Math.floor(array.length / 2);
const left = this.sort(array.slice(0, mid));
const right = this.sort(array.slice(mid));
return this.merge(left, right);
}
merge(left, right) {
const result = [];
let i = 0,
j = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return [...result, ...left.slice(i), ...right.slice(j)];
}
getName() {
return "Merge Sort";
}
}
// Sorter - uses polymorphism
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
// Change strategy
setStrategy(strategy) {
this.strategy = strategy;
}
// Execute sorting - polymorphic call
sort(array) {
console.log(`Using ${this.strategy.getName()}...`);
const startTime = performance.now();
const result = this.strategy.sort(array);
const endTime = performance.now();
console.log(`Time: ${(endTime - startTime).toFixed(4)}ms\n`);
return result;
}
}
// Usage example
const data = [64, 34, 25, 12, 22, 11, 90, 88, 45, 50, 33, 17];
console.log("Original array:", data);
console.log();
const sorter = new Sorter(new BubbleSort());
console.log("Result:", sorter.sort(data));
// Dynamically switch strategy
sorter.setStrategy(new QuickSort());
console.log("Result:", sorter.sort(data));
sorter.setStrategy(new MergeSort());
console.log("Result:", sorter.sort(data));Advanced Applications of Polymorphism
Event Handling System
// Event base class
class Event {
constructor(type, data = {}) {
this.type = type;
this.data = data;
this.timestamp = new Date();
}
process() {
throw new Error("process() must be implemented");
}
}
// Various specific events
class UserLoginEvent extends Event {
constructor(userId, ipAddress) {
super("USER_LOGIN", { userId, ipAddress });
}
process() {
console.log(
`[LOGIN] User ${this.data.userId} logged in from ${this.data.ipAddress}`
);
// Log login activity
// Update last login time
// Check for suspicious activity
return {
action: "Record login",
userId: this.data.userId,
timestamp: this.timestamp,
};
}
}
class OrderPlacedEvent extends Event {
constructor(orderId, amount, items) {
super("ORDER_PLACED", { orderId, amount, items });
}
process() {
console.log(
`[ORDER] Order ${this.data.orderId} created, amount $${this.data.amount}`
);
// Send confirmation email
// Update inventory
// Notify warehouse
return {
action: "Process order",
orderId: this.data.orderId,
amount: this.data.amount,
};
}
}
class PaymentReceivedEvent extends Event {
constructor(orderId, amount, method) {
super("PAYMENT_RECEIVED", { orderId, amount, method });
}
process() {
console.log(
`[PAYMENT] Received payment $${this.data.amount} for order ${this.data.orderId}`
);
// Update order status
// Send receipt
// Trigger shipping process
return {
action: "Process payment",
orderId: this.data.orderId,
method: this.data.method,
};
}
}
class ErrorEvent extends Event {
constructor(errorType, message, stack) {
super("ERROR", { errorType, message, stack });
}
process() {
console.log(`[ERROR] ${this.data.errorType}: ${this.data.message}`);
// Log error
// Send alert
// Notify developers if critical
return {
action: "Log error",
severity: this.#getSeverity(),
errorType: this.data.errorType,
};
}
#getSeverity() {
const critical = ["DATABASE_ERROR", "PAYMENT_FAILURE"];
return critical.includes(this.data.errorType) ? "critical" : "warning";
}
}
// Event processor - uses polymorphism to handle all events uniformly
class EventProcessor {
constructor() {
this.processedEvents = [];
}
// Process single event
handleEvent(event) {
if (!(event instanceof Event)) {
throw new Error("Must be an Event instance");
}
console.log(`\nProcessing event: ${event.type}`);
// Polymorphic call: each event type has its own processing logic
const result = event.process();
this.processedEvents.push({
event,
result,
processedAt: new Date(),
});
return result;
}
// Batch process events
handleBatch(events) {
console.log(`\n=== Batch processing ${events.length} events ===`);
const results = [];
for (const event of events) {
const result = this.handleEvent(event);
results.push(result);
}
console.log(`\n=== Batch processing complete ===\n`);
return results;
}
getStatistics() {
const stats = {};
for (const { event } of this.processedEvents) {
stats[event.type] = (stats[event.type] || 0) + 1;
}
return stats;
}
}
// Usage
const processor = new EventProcessor();
const events = [
new UserLoginEvent("user123", "192.168.1.100"),
new OrderPlacedEvent("ORD-001", 299.99, ["Laptop Mouse"]),
new PaymentReceivedEvent("ORD-001", 299.99, "Credit Card"),
new UserLoginEvent("user456", "192.168.1.101"),
new ErrorEvent("DATABASE_ERROR", "Connection timeout", "..."),
];
processor.handleBatch(events);
console.log("Event statistics:");
console.log(processor.getStatistics());
// {
// USER_LOGIN: 2,
// ORDER_PLACED: 1,
// PAYMENT_RECEIVED: 1,
// ERROR: 1
// }Advantages and Best Practices of Polymorphism
Advantages
- Code Reuse: Generic code can handle multiple types of objects
- Extensibility: Adding new types doesn't require modifying existing code
- Maintainability: Logic for each type is independent, easy to understand and modify
- Flexibility: Can dynamically select specific implementations at runtime
Best Practices
- Interface Consistency: Ensure all subclasses implement the same interface
- Avoid Type Checking: Don't use
instanceofortypeofto decide behavior - Liskov Substitution Principle: Subclass objects should be able to replace parent objects without affecting program correctness
- Clear Documentation: Clearly specify which methods need to be overridden
Summary
Polymorphism is one of the core strengths of object-oriented programming, enabling us to:
- Write more generic, flexible code
- Easily extend system functionality
- Maintain code clarity and maintainability
Through method overriding, interface unification, and dynamic binding, polymorphism achieves the elegant design of "one interface, multiple implementations." Whether it's simple shape calculations or complex business systems, polymorphism makes code more elegant and powerful.