Skip to content

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:

  1. Unified Interface: Define common method names
  2. Different Implementations: Each subclass provides its unique implementation
  3. Dynamic Calling: Call the appropriate method at runtime based on the object's actual type

Let's understand polymorphism through a simple example:

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

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

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

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

javascript
// 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

javascript
// 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

  1. Code Reuse: Generic code can handle multiple types of objects
  2. Extensibility: Adding new types doesn't require modifying existing code
  3. Maintainability: Logic for each type is independent, easy to understand and modify
  4. Flexibility: Can dynamically select specific implementations at runtime

Best Practices

  1. Interface Consistency: Ensure all subclasses implement the same interface
  2. Avoid Type Checking: Don't use instanceof or typeof to decide behavior
  3. Liskov Substitution Principle: Subclass objects should be able to replace parent objects without affecting program correctness
  4. 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.