Polymorphism and Interfaces: One Interface, Many Forms

Introduction
The word polymorphism comes from Greek, meaning "many forms." In object-oriented programming, it's one of the most powerful concepts—the ability for different objects to respond to the same message in different ways.
When combined with interfaces, polymorphism enables you to write flexible, extensible code that depends on abstractions rather than concrete implementations. This is the foundation of many design patterns and the key to building maintainable software.
What You'll Learn
✅ Understand polymorphism and its "many forms" concept
✅ Master compile-time (static) polymorphism: method overloading
✅ Master runtime (dynamic) polymorphism: method overriding
✅ Design with interfaces and abstract contracts
✅ Understand duck typing in dynamic languages
✅ Apply the Liskov Substitution Principle (LSP)
Prerequisites
- Completed The Four Pillars of OOP
- Completed Classes, Objects, and Abstraction
- Completed Encapsulation and Information Hiding
- Completed Inheritance and Composition
Understanding Polymorphism
Polymorphism allows objects of different types to be treated as objects of a common type. The same method call can produce different behavior depending on the actual object type.
The "Many Forms" Concept
Think of a universal remote control. When you press "Power," the TV turns on. Press the same button with a different device selected, and the stereo turns on. Same button (interface), different behavior (implementation).
// TypeScript - Polymorphism example
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): void {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
speak(): void {
console.log(`${this.name} barks: Woof!`);
}
}
class Cat extends Animal {
speak(): void {
console.log(`${this.name} meows: Meow!`);
}
}
class Duck extends Animal {
speak(): void {
console.log(`${this.name} quacks: Quack!`);
}
}
// Polymorphism in action - same method, different behavior
function makeAnimalSpeak(animal: Animal): void {
animal.speak(); // Which speak() is called depends on actual type
}
const animals: Animal[] = [
new Dog("Rex"),
new Cat("Whiskers"),
new Duck("Donald")
];
animals.forEach(animal => makeAnimalSpeak(animal));
// Output:
// Rex barks: Woof!
// Whiskers meows: Meow!
// Donald quacks: Quack!The makeAnimalSpeak function doesn't know or care about the specific animal type. It just calls speak(), and polymorphism ensures the correct implementation runs.
Two Types of Polymorphism
Polymorphism comes in two flavors, distinguished by when the method to call is determined:
1. Compile-Time (Static) Polymorphism
Resolved at compile time. The compiler determines which method to call based on:
- Method signature (overloading)
- Parameter types
- Number of parameters
Also known as: Method overloading, static binding, early binding
2. Runtime (Dynamic) Polymorphism
Resolved at runtime. The actual method called depends on:
- The actual object type (not the declared type)
- Method overriding in subclasses
Also known as: Method overriding, dynamic binding, late binding
Compile-Time Polymorphism: Method Overloading
Method overloading allows multiple methods with the same name but different parameters in the same class.
Java Example (Full Overloading Support)
Java supports method overloading natively:
// Java - Method overloading
public class Calculator {
// Same method name, different parameter types
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
public String add(String a, String b) {
return a + b; // String concatenation
}
}
// Usage
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // 5 (int version)
System.out.println(calc.add(2.5, 3.5)); // 6.0 (double version)
System.out.println(calc.add(1, 2, 3)); // 6 (three-parameter version)
System.out.println(calc.add("Hello, ", "World")); // "Hello, World"The compiler determines which add() method to call based on the argument types.
TypeScript Overloading (Signature-Based)
TypeScript uses overload signatures with a single implementation:
// TypeScript - Overload signatures
class Calculator {
// Overload signatures
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: number, b: number, c: number): number;
// Single implementation
add(a: number | string, b: number | string, c?: number): number | string {
if (typeof a === "string" && typeof b === "string") {
return a + b;
}
if (typeof a === "number" && typeof b === "number") {
return c !== undefined ? a + b + c : a + b;
}
throw new Error("Invalid arguments");
}
}
const calc = new Calculator();
console.log(calc.add(2, 3)); // 5
console.log(calc.add("Hello, ", "World")); // "Hello, World"
console.log(calc.add(1, 2, 3)); // 6Python (No Traditional Overloading)
Python doesn't support method overloading in the traditional sense. The last method definition wins:
# Python - No traditional overloading
class Calculator:
# This method will be overwritten!
def add(self, a: int, b: int) -> int:
return a + b
# Only this method exists
def add(self, a: float, b: float) -> float:
return a + b
calc = Calculator()
# calc.add(1, 2) would call the float versionInstead, Python uses default parameters, *args, or @singledispatch:
# Python - Alternatives to overloading
from functools import singledispatch
# Option 1: Default parameters
class Calculator:
def add(self, a, b, c=None):
if c is not None:
return a + b + c
return a + b
# Option 2: Using singledispatch for type-based dispatch
@singledispatch
def add(a, b):
raise NotImplementedError(f"Unsupported types: {type(a)}, {type(b)}")
@add.register(int)
def _(a: int, b: int) -> int:
return a + b
@add.register(str)
def _(a: str, b: str) -> str:
return a + b
@add.register(float)
def _(a: float, b: float) -> float:
return a + b
# Usage
print(add(2, 3)) # 5
print(add("Hello, ", "World")) # "Hello, World"
print(add(2.5, 3.5)) # 6.0Runtime Polymorphism: Method Overriding
Method overriding occurs when a subclass provides its own implementation of a method defined in its parent class.
Basic Method Overriding
// TypeScript - Method overriding
class Shape {
getArea(): number {
return 0;
}
describe(): string {
return `A shape with area ${this.getArea()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
// Override getArea
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super();
}
// Override getArea
getArea(): number {
return this.width * this.height;
}
}
class Triangle extends Shape {
constructor(
private base: number,
private height: number
) {
super();
}
// Override getArea
getArea(): number {
return 0.5 * this.base * this.height;
}
}
// Runtime polymorphism - correct getArea() called based on actual type
function calculateTotalArea(shapes: Shape[]): number {
return shapes.reduce((total, shape) => total + shape.getArea(), 0);
}
const shapes: Shape[] = [
new Circle(5), // Area: 78.54
new Rectangle(4, 6), // Area: 24
new Triangle(3, 4) // Area: 6
];
console.log(calculateTotalArea(shapes)); // 108.54...
shapes.forEach(shape => console.log(shape.describe()));# Python - Method overriding
import math
class Shape:
def get_area(self) -> float:
return 0
def describe(self) -> str:
return f"A shape with area {self.get_area()}"
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def get_area(self) -> float:
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def get_area(self) -> float:
return self.width * self.height
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def get_area(self) -> float:
return 0.5 * self.base * self.height
# Runtime polymorphism
def calculate_total_area(shapes: list[Shape]) -> float:
return sum(shape.get_area() for shape in shapes)
shapes = [
Circle(5),
Rectangle(4, 6),
Triangle(3, 4)
]
print(calculate_total_area(shapes)) # 108.54...// Java - Method overriding with @Override annotation
public abstract class Shape {
public abstract double getArea();
public String describe() {
return "A shape with area " + getArea();
}
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}Key Differences: Overloading vs Overriding
| Aspect | Overloading | Overriding |
|---|---|---|
| When resolved | Compile time | Runtime |
| Where | Same class | Parent-child classes |
| Method signature | Must differ | Must be same |
| Return type | Can differ | Must be same or covariant |
| Access modifier | Can differ | Can't be more restrictive |
| Purpose | Multiple ways to call same operation | Specialize behavior |
Interfaces: Contracts for Polymorphism
Interfaces define a contract—a set of methods that implementing classes must provide. They're the purest form of abstraction and the foundation for polymorphic design.
Defining and Implementing Interfaces
// TypeScript - Interface definition
interface IPaymentProcessor {
processPayment(amount: number): boolean;
refund(transactionId: string): boolean;
getTransactionHistory(): string[];
}
// Implementing classes must fulfill the contract
class CreditCardProcessor implements IPaymentProcessor {
private transactions: string[] = [];
processPayment(amount: number): boolean {
console.log(`Processing $${amount} via Credit Card`);
const txId = `CC-${Date.now()}`;
this.transactions.push(txId);
return true;
}
refund(transactionId: string): boolean {
console.log(`Refunding transaction ${transactionId}`);
return true;
}
getTransactionHistory(): string[] {
return [...this.transactions];
}
}
class PayPalProcessor implements IPaymentProcessor {
private transactions: string[] = [];
processPayment(amount: number): boolean {
console.log(`Processing $${amount} via PayPal`);
const txId = `PP-${Date.now()}`;
this.transactions.push(txId);
return true;
}
refund(transactionId: string): boolean {
console.log(`Refunding PayPal transaction ${transactionId}`);
return true;
}
getTransactionHistory(): string[] {
return [...this.transactions];
}
}
class CryptoProcessor implements IPaymentProcessor {
private transactions: string[] = [];
processPayment(amount: number): boolean {
console.log(`Processing $${amount} via Cryptocurrency`);
const txId = `CRYPTO-${Date.now()}`;
this.transactions.push(txId);
return true;
}
refund(transactionId: string): boolean {
console.log(`Crypto refunds not supported for ${transactionId}`);
return false; // Crypto refunds might not be possible
}
getTransactionHistory(): string[] {
return [...this.transactions];
}
}
// Client code depends on interface, not implementation
class CheckoutService {
constructor(private paymentProcessor: IPaymentProcessor) {}
checkout(amount: number): boolean {
console.log(`Checkout for $${amount}`);
return this.paymentProcessor.processPayment(amount);
}
// Can change processor at runtime
setPaymentProcessor(processor: IPaymentProcessor): void {
this.paymentProcessor = processor;
}
}
// Usage - polymorphism through interfaces
const checkout = new CheckoutService(new CreditCardProcessor());
checkout.checkout(100); // Processing $100 via Credit Card
checkout.setPaymentProcessor(new PayPalProcessor());
checkout.checkout(50); // Processing $50 via PayPal
checkout.setPaymentProcessor(new CryptoProcessor());
checkout.checkout(200); // Processing $200 via Cryptocurrency# Python - Interface using ABC (Abstract Base Class)
from abc import ABC, abstractmethod
class IPaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
@abstractmethod
def refund(self, transaction_id: str) -> bool:
pass
@abstractmethod
def get_transaction_history(self) -> list[str]:
pass
class CreditCardProcessor(IPaymentProcessor):
def __init__(self):
self._transactions: list[str] = []
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via Credit Card")
tx_id = f"CC-{id(self)}"
self._transactions.append(tx_id)
return True
def refund(self, transaction_id: str) -> bool:
print(f"Refunding transaction {transaction_id}")
return True
def get_transaction_history(self) -> list[str]:
return self._transactions.copy()
class PayPalProcessor(IPaymentProcessor):
def __init__(self):
self._transactions: list[str] = []
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via PayPal")
tx_id = f"PP-{id(self)}"
self._transactions.append(tx_id)
return True
def refund(self, transaction_id: str) -> bool:
print(f"Refunding PayPal transaction {transaction_id}")
return True
def get_transaction_history(self) -> list[str]:
return self._transactions.copy()
# Client code depends on interface
class CheckoutService:
def __init__(self, payment_processor: IPaymentProcessor):
self._processor = payment_processor
def checkout(self, amount: float) -> bool:
print(f"Checkout for ${amount}")
return self._processor.process_payment(amount)
def set_payment_processor(self, processor: IPaymentProcessor):
self._processor = processor// Java - Interface definition
public interface IPaymentProcessor {
boolean processPayment(double amount);
boolean refund(String transactionId);
List<String> getTransactionHistory();
}
public class CreditCardProcessor implements IPaymentProcessor {
private List<String> transactions = new ArrayList<>();
@Override
public boolean processPayment(double amount) {
System.out.println("Processing $" + amount + " via Credit Card");
transactions.add("CC-" + System.currentTimeMillis());
return true;
}
@Override
public boolean refund(String transactionId) {
System.out.println("Refunding transaction " + transactionId);
return true;
}
@Override
public List<String> getTransactionHistory() {
return new ArrayList<>(transactions);
}
}Multiple Interface Implementation
Unlike classes (single inheritance in most languages), a class can implement multiple interfaces:
// TypeScript - Multiple interfaces
interface IReadable {
read(): string;
}
interface IWritable {
write(data: string): void;
}
interface IClosable {
close(): void;
isOpen(): boolean;
}
// Implement multiple interfaces
class FileHandler implements IReadable, IWritable, IClosable {
private content: string = "";
private open: boolean = true;
read(): string {
if (!this.open) throw new Error("File is closed");
return this.content;
}
write(data: string): void {
if (!this.open) throw new Error("File is closed");
this.content += data;
}
close(): void {
this.open = false;
}
isOpen(): boolean {
return this.open;
}
}
// Functions can require specific interfaces
function copyContent(source: IReadable, destination: IWritable): void {
const content = source.read();
destination.write(content);
}
function safeClose(resource: IClosable): void {
if (resource.isOpen()) {
resource.close();
}
}# Python - Multiple interface implementation
from abc import ABC, abstractmethod
class IReadable(ABC):
@abstractmethod
def read(self) -> str:
pass
class IWritable(ABC):
@abstractmethod
def write(self, data: str) -> None:
pass
class IClosable(ABC):
@abstractmethod
def close(self) -> None:
pass
@abstractmethod
def is_open(self) -> bool:
pass
# Implement multiple interfaces
class FileHandler(IReadable, IWritable, IClosable):
def __init__(self):
self._content = ""
self._open = True
def read(self) -> str:
if not self._open:
raise RuntimeError("File is closed")
return self._content
def write(self, data: str) -> None:
if not self._open:
raise RuntimeError("File is closed")
self._content += data
def close(self) -> None:
self._open = False
def is_open(self) -> bool:
return self._open// Java - Multiple interfaces
public interface IReadable {
String read();
}
public interface IWritable {
void write(String data);
}
public interface IClosable {
void close();
boolean isOpen();
}
public class FileHandler implements IReadable, IWritable, IClosable {
private StringBuilder content = new StringBuilder();
private boolean open = true;
@Override
public String read() {
if (!open) throw new IllegalStateException("File is closed");
return content.toString();
}
@Override
public void write(String data) {
if (!open) throw new IllegalStateException("File is closed");
content.append(data);
}
@Override
public void close() {
open = false;
}
@Override
public boolean isOpen() {
return open;
}
}Abstract Classes vs Interfaces
Both provide abstraction, but they serve different purposes:
Comparison Table
| Aspect | Abstract Class | Interface |
|---|---|---|
| Methods | Can have implementation | Only signatures (mostly) |
| Fields | Can have instance fields | Only constants (Java) |
| Constructors | Yes | No |
| Inheritance | Single | Multiple |
| Purpose | Shared implementation | Define contract |
| "Is-a" vs "Can-do" | Is-a relationship | Can-do capability |
When to Use Abstract Classes
- When you have shared implementation across subclasses
- When you need constructors or instance fields
- When the relationship is truly "is-a"
// Abstract class - shared implementation
abstract class DatabaseConnection {
protected host: string;
protected port: number;
protected connected: boolean = false;
constructor(host: string, port: number) {
this.host = host;
this.port = port;
}
// Shared implementation
connect(): void {
console.log(`Connecting to ${this.host}:${this.port}`);
this.performConnection();
this.connected = true;
}
// Abstract - must be implemented
protected abstract performConnection(): void;
abstract executeQuery(query: string): any[];
// Shared implementation
disconnect(): void {
if (this.connected) {
console.log("Disconnecting...");
this.connected = false;
}
}
}
class PostgresConnection extends DatabaseConnection {
protected performConnection(): void {
console.log("PostgreSQL handshake...");
}
executeQuery(query: string): any[] {
console.log(`PostgreSQL executing: ${query}`);
return [];
}
}
class MySQLConnection extends DatabaseConnection {
protected performConnection(): void {
console.log("MySQL handshake...");
}
executeQuery(query: string): any[] {
console.log(`MySQL executing: ${query}`);
return [];
}
}When to Use Interfaces
- When you want to define a contract without implementation
- When you need multiple inheritance of types
- When unrelated classes need to share a capability
// Interface - contract only
interface ISerializable {
serialize(): string;
deserialize(data: string): void;
}
interface IComparable<T> {
compareTo(other: T): number;
}
interface ICloneable<T> {
clone(): T;
}
// Unrelated classes can implement same interface
class User implements ISerializable, IComparable<User> {
constructor(
public id: number,
public name: string,
public email: string
) {}
serialize(): string {
return JSON.stringify({ id: this.id, name: this.name, email: this.email });
}
deserialize(data: string): void {
const obj = JSON.parse(data);
this.id = obj.id;
this.name = obj.name;
this.email = obj.email;
}
compareTo(other: User): number {
return this.id - other.id;
}
}
class Product implements ISerializable, IComparable<Product> {
constructor(
public sku: string,
public price: number
) {}
serialize(): string {
return JSON.stringify({ sku: this.sku, price: this.price });
}
deserialize(data: string): void {
const obj = JSON.parse(data);
this.sku = obj.sku;
this.price = obj.price;
}
compareTo(other: Product): number {
return this.price - other.price;
}
}Duck Typing: "If It Quacks Like a Duck..."
Duck typing is a form of polymorphism in dynamic languages where an object's suitability is determined by its methods and properties, not its class.
"If it walks like a duck and quacks like a duck, then it must be a duck."
Python Duck Typing
# Python - Duck typing (no interface needed!)
class Duck:
def speak(self):
print("Quack!")
def walk(self):
print("Waddle waddle")
class Person:
def speak(self):
print("Hello!")
def walk(self):
print("Step step")
class Robot:
def speak(self):
print("Beep boop!")
def walk(self):
print("Clank clank")
# No common base class or interface needed
def make_it_walk_and_talk(thing):
thing.walk()
thing.speak()
# All work because they have walk() and speak()
make_it_walk_and_talk(Duck()) # Waddle waddle, Quack!
make_it_walk_and_talk(Person()) # Step step, Hello!
make_it_walk_and_talk(Robot()) # Clank clank, Beep boop!TypeScript Structural Typing
TypeScript uses structural typing — types are compatible if their structures match:
// TypeScript - Structural typing (like duck typing with types)
interface Walkable {
walk(): void;
}
interface Talkable {
speak(): void;
}
// No explicit implements needed if structure matches
class Duck {
speak(): void {
console.log("Quack!");
}
walk(): void {
console.log("Waddle waddle");
}
}
class Robot {
speak(): void {
console.log("Beep boop!");
}
walk(): void {
console.log("Clank clank");
}
charge(): void {
console.log("Charging...");
}
}
// Both work even though Robot doesn't explicitly implement the interfaces
function makeItWalkAndTalk(thing: Walkable & Talkable): void {
thing.walk();
thing.speak();
}
makeItWalkAndTalk(new Duck()); // Works!
makeItWalkAndTalk(new Robot()); // Works! (extra methods are fine)Benefits and Risks of Duck Typing
Benefits:
- More flexible code
- Less boilerplate (no interface declarations needed)
- Easier to work with third-party code
Risks:
- Runtime errors if method doesn't exist
- Less explicit contracts
- Harder to understand dependencies
The Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states:
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
This is the "L" in SOLID and is fundamental to proper polymorphism.
LSP Violation Example: Square and Rectangle
The classic example of an LSP violation:
// LSP Violation - Square extends Rectangle
class Rectangle {
protected width: number;
protected height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(size: number) {
super(size, size);
}
// Must override to maintain square invariant
setWidth(width: number): void {
this.width = width;
this.height = width; // Must keep it square!
}
setHeight(height: number): void {
this.height = height;
this.width = height; // Must keep it square!
}
}
// This function works with Rectangle but breaks with Square
function doubleWidth(rect: Rectangle): void {
const originalHeight = rect.getArea() / rect.getArea() * rect.getArea();
rect.setWidth(rect.getArea() / 10); // Some width manipulation
}
// Test LSP
function testLSP(rect: Rectangle): void {
rect.setWidth(5);
rect.setHeight(4);
const area = rect.getArea();
// Expected: 5 * 4 = 20
console.log(`Expected area: 20, Actual: ${area}`);
if (area !== 20) {
console.log("LSP VIOLATION! Square changed both dimensions");
}
}
testLSP(new Rectangle(1, 1)); // Expected: 20, Actual: 20 ✓
testLSP(new Square(1)); // Expected: 20, Actual: 16 ✗ (4*4)LSP-Compliant Design
// LSP-Compliant - Use interface instead of inheritance
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(
private width: number,
private height: number
) {}
getWidth(): number {
return this.width;
}
getHeight(): number {
return this.height;
}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private size: number) {}
getSize(): number {
return this.size;
}
setSize(size: number): void {
this.size = size;
}
getArea(): number {
return this.size * this.size;
}
}
// No inheritance relationship - no LSP violation possible
// Each class has its own appropriate methods
function calculateTotalArea(shapes: Shape[]): number {
return shapes.reduce((total, shape) => total + shape.getArea(), 0);
}LSP Guidelines
- Preconditions cannot be strengthened in a subclass
- Postconditions cannot be weakened in a subclass
- Invariants must be preserved in all subclasses
- Behavior must be consistent with parent class expectations
Interface Segregation
The Interface Segregation Principle (ISP) states that clients shouldn't depend on interfaces they don't use. This is closely related to interface-based polymorphism.
Problem: Fat Interface
// Problem: Fat interface
interface IWorker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
writeReport(): void;
}
class HumanWorker implements IWorker {
work(): void { console.log("Working..."); }
eat(): void { console.log("Eating lunch..."); }
sleep(): void { console.log("Sleeping..."); }
attendMeeting(): void { console.log("In meeting..."); }
writeReport(): void { console.log("Writing report..."); }
}
class RobotWorker implements IWorker {
work(): void { console.log("Processing tasks..."); }
eat(): void { /* Robots don't eat! */ }
sleep(): void { /* Robots don't sleep! */ }
attendMeeting(): void { console.log("Video conferencing..."); }
writeReport(): void { console.log("Generating report..."); }
}Solution: Segregated Interfaces
// Solution: Segregated interfaces
interface IWorkable {
work(): void;
}
interface IFeedable {
eat(): void;
}
interface ISleepable {
sleep(): void;
}
interface IMeetingAttendee {
attendMeeting(): void;
}
interface IReportWriter {
writeReport(): void;
}
// Human implements all that apply
class HumanWorker implements IWorkable, IFeedable, ISleepable, IMeetingAttendee, IReportWriter {
work(): void { console.log("Working..."); }
eat(): void { console.log("Eating lunch..."); }
sleep(): void { console.log("Sleeping..."); }
attendMeeting(): void { console.log("In meeting..."); }
writeReport(): void { console.log("Writing report..."); }
}
// Robot implements only what applies
class RobotWorker implements IWorkable, IMeetingAttendee, IReportWriter {
work(): void { console.log("Processing tasks..."); }
attendMeeting(): void { console.log("Video conferencing..."); }
writeReport(): void { console.log("Generating report..."); }
}
// Functions require only what they need
function scheduleWork(worker: IWorkable): void {
worker.work();
}
function scheduleLunch(feedable: IFeedable): void {
feedable.eat();
}
// Works with both
scheduleWork(new HumanWorker());
scheduleWork(new RobotWorker());
// Only works with humans
scheduleLunch(new HumanWorker());
// scheduleLunch(new RobotWorker()); // Compile error!Real-World Example: Notification System
Let's build a complete notification system using polymorphism and interfaces:
// TypeScript - Complete notification system
// Core interfaces
interface INotification {
send(recipient: string, message: string): Promise<boolean>;
}
interface INotificationFormatter {
format(message: string): string;
}
interface INotificationLogger {
log(notification: string, success: boolean): void;
}
// Notification implementations
class EmailNotification implements INotification {
async send(recipient: string, message: string): Promise<boolean> {
console.log(`📧 Sending email to ${recipient}: ${message}`);
// Simulate async email sending
await new Promise(resolve => setTimeout(resolve, 100));
return true;
}
}
class SMSNotification implements INotification {
async send(recipient: string, message: string): Promise<boolean> {
console.log(`📱 Sending SMS to ${recipient}: ${message}`);
await new Promise(resolve => setTimeout(resolve, 50));
return true;
}
}
class PushNotification implements INotification {
async send(recipient: string, message: string): Promise<boolean> {
console.log(`🔔 Sending push to ${recipient}: ${message}`);
await new Promise(resolve => setTimeout(resolve, 30));
return true;
}
}
class SlackNotification implements INotification {
async send(recipient: string, message: string): Promise<boolean> {
console.log(`💬 Sending Slack to ${recipient}: ${message}`);
await new Promise(resolve => setTimeout(resolve, 80));
return true;
}
}
// Formatter implementations
class PlainTextFormatter implements INotificationFormatter {
format(message: string): string {
return message;
}
}
class HTMLFormatter implements INotificationFormatter {
format(message: string): string {
return `<html><body><p>${message}</p></body></html>`;
}
}
class MarkdownFormatter implements INotificationFormatter {
format(message: string): string {
return `**Notification:** ${message}`;
}
}
// Logger implementations
class ConsoleLogger implements INotificationLogger {
log(notification: string, success: boolean): void {
const status = success ? "✅" : "❌";
console.log(`${status} [LOG] ${notification}`);
}
}
class FileLogger implements INotificationLogger {
log(notification: string, success: boolean): void {
// In real implementation, would write to file
console.log(`[FILE LOG] ${success ? "SUCCESS" : "FAILURE"}: ${notification}`);
}
}
// Notification service using composition and polymorphism
class NotificationService {
private notifications: INotification[] = [];
private formatter: INotificationFormatter;
private logger: INotificationLogger;
constructor(
formatter: INotificationFormatter = new PlainTextFormatter(),
logger: INotificationLogger = new ConsoleLogger()
) {
this.formatter = formatter;
this.logger = logger;
}
addNotificationChannel(notification: INotification): void {
this.notifications.push(notification);
}
removeNotificationChannel(notification: INotification): void {
const index = this.notifications.indexOf(notification);
if (index > -1) {
this.notifications.splice(index, 1);
}
}
setFormatter(formatter: INotificationFormatter): void {
this.formatter = formatter;
}
setLogger(logger: INotificationLogger): void {
this.logger = logger;
}
async notify(recipient: string, message: string): Promise<void> {
const formattedMessage = this.formatter.format(message);
for (const notification of this.notifications) {
try {
const success = await notification.send(recipient, formattedMessage);
this.logger.log(
`Sent to ${recipient}: ${message.substring(0, 50)}...`,
success
);
} catch (error) {
this.logger.log(
`Failed to send to ${recipient}: ${error}`,
false
);
}
}
}
async notifyAll(recipients: string[], message: string): Promise<void> {
for (const recipient of recipients) {
await this.notify(recipient, message);
}
}
}
// Usage
async function main() {
// Create service with custom formatter and logger
const service = new NotificationService(
new MarkdownFormatter(),
new ConsoleLogger()
);
// Add notification channels (polymorphic)
service.addNotificationChannel(new EmailNotification());
service.addNotificationChannel(new SMSNotification());
service.addNotificationChannel(new PushNotification());
service.addNotificationChannel(new SlackNotification());
// Send notifications
await service.notify("user@example.com", "Your order has been shipped!");
// Change formatter at runtime
service.setFormatter(new HTMLFormatter());
await service.notify("admin@example.com", "New user signup");
}
main();# Python - Complete notification system
from abc import ABC, abstractmethod
import asyncio
# Core interfaces
class INotification(ABC):
@abstractmethod
async def send(self, recipient: str, message: str) -> bool:
pass
class INotificationFormatter(ABC):
@abstractmethod
def format(self, message: str) -> str:
pass
class INotificationLogger(ABC):
@abstractmethod
def log(self, notification: str, success: bool) -> None:
pass
# Notification implementations
class EmailNotification(INotification):
async def send(self, recipient: str, message: str) -> bool:
print(f"📧 Sending email to {recipient}: {message}")
await asyncio.sleep(0.1)
return True
class SMSNotification(INotification):
async def send(self, recipient: str, message: str) -> bool:
print(f"📱 Sending SMS to {recipient}: {message}")
await asyncio.sleep(0.05)
return True
class PushNotification(INotification):
async def send(self, recipient: str, message: str) -> bool:
print(f"🔔 Sending push to {recipient}: {message}")
await asyncio.sleep(0.03)
return True
# Formatter implementations
class PlainTextFormatter(INotificationFormatter):
def format(self, message: str) -> str:
return message
class HTMLFormatter(INotificationFormatter):
def format(self, message: str) -> str:
return f"<html><body><p>{message}</p></body></html>"
# Logger implementations
class ConsoleLogger(INotificationLogger):
def log(self, notification: str, success: bool) -> None:
status = "✅" if success else "❌"
print(f"{status} [LOG] {notification}")
# Notification service
class NotificationService:
def __init__(
self,
formatter: INotificationFormatter = None,
logger: INotificationLogger = None
):
self._notifications: list[INotification] = []
self._formatter = formatter or PlainTextFormatter()
self._logger = logger or ConsoleLogger()
def add_notification_channel(self, notification: INotification) -> None:
self._notifications.append(notification)
def set_formatter(self, formatter: INotificationFormatter) -> None:
self._formatter = formatter
async def notify(self, recipient: str, message: str) -> None:
formatted_message = self._formatter.format(message)
for notification in self._notifications:
try:
success = await notification.send(recipient, formatted_message)
self._logger.log(
f"Sent to {recipient}: {message[:50]}...",
success
)
except Exception as e:
self._logger.log(
f"Failed to send to {recipient}: {e}",
False
)
# Usage
async def main():
service = NotificationService(
formatter=PlainTextFormatter(),
logger=ConsoleLogger()
)
service.add_notification_channel(EmailNotification())
service.add_notification_channel(SMSNotification())
service.add_notification_channel(PushNotification())
await service.notify("user@example.com", "Your order has been shipped!")
# Change formatter at runtime
service.set_formatter(HTMLFormatter())
await service.notify("admin@example.com", "New user signup")
asyncio.run(main())Summary and Key Takeaways
Polymorphism
- Allows objects to take "many forms"
- Same interface, different implementations
- Enables flexible, extensible code
Two Types
| Compile-Time (Static) | Runtime (Dynamic) |
|---|---|
| Method overloading | Method overriding |
| Resolved at compile time | Resolved at runtime |
| Based on parameters | Based on actual object type |
| Same class | Parent-child classes |
Interfaces
- Define contracts without implementation
- Enable multiple inheritance of types
- Foundation for dependency injection
- Allow swapping implementations at runtime
Duck Typing
- Object compatibility based on structure, not type
- "If it walks like a duck and quacks like a duck..."
- More flexible but less safe
- Common in Python, supported in TypeScript
Liskov Substitution Principle
- Subclasses must be substitutable for parent classes
- Avoid violating parent class contracts
- Square/Rectangle is a classic violation example
- Prefer composition when inheritance doesn't fit
Best Practices
- Program to interfaces, not implementations
- Prefer composition over inheritance for flexibility
- Keep interfaces small (Interface Segregation)
- Respect LSP — subtypes must honor parent contracts
- Use polymorphism to eliminate conditional logic
Practice Exercises
-
Build a Plugin System: Create a plugin architecture where plugins implement an
IPlugininterface withinit(),execute(), andcleanup()methods. The main application should be able to load and run any plugin polymorphically. -
Implement a Sorting Framework: Design a sorting framework with a
ISortableinterface. Implement bubble sort, quick sort, and merge sort. Create aSortingServicethat can use any sorting algorithm. -
Create a Media Player: Build a media player that can play different media types (Audio, Video, Stream) through a common
IPlayableinterface. Add support for playlists and different playback strategies.
What's Next?
Now that you understand polymorphism and interfaces, you're ready to dive into the SOLID Principles in the next post. You'll learn:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP) in depth
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Continue your OOP journey: SOLID Principles Explained
Additional Resources
Previous Posts in This Series
- OOP & Design Patterns Roadmap - Complete learning path
- The Four Pillars of OOP - OOP fundamentals
- Classes, Objects, and Abstraction - Building blocks of OOP
- Encapsulation and Information Hiding - Protecting data integrity
- Inheritance and Composition - Code reuse strategies
Related Posts
- Java Phase 2: Object-Oriented Programming - OOP in Java
- Python Phase 2: OOP & Advanced Features - OOP in Python
- TypeScript Advanced Types Deep Dive - Advanced type system features
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.