Inheritance and Polymorphism
Inheritance and Polymorphism Master advanced OOP concepts that enable code reuse and flexible design through inheritance hierarchies and polymorphic behavior. I…
Inheritance and Polymorphism
Master advanced OOP concepts that enable code reuse and flexible design through inheritance hierarchies and polymorphic behavior.
Inheritance Hierarchies
Inheritance creates a parent-child relationship between classes, allowing child classes to inherit and extend parent functionality.
// Base class
class User {
protected id: string;
protected email: string;
protected createdAt: Date;
constructor(id: string, email: string) {
this.id = id;
this.email = email;
this.createdAt = new Date();
}
public getProfile(): string {
return `User: ${this.email} (ID: ${this.id})`;
}
public canAccessResource(resourceId: string): boolean {
return false; // Base users can't access resources
}
}
// First level inheritance
class RegisteredUser extends User {
protected username: string;
protected lastLogin: Date;
constructor(id: string, email: string, username: string) {
super(id, email);
this.username = username;
this.lastLogin = new Date();
}
public override getProfile(): string {
return `${this.username} (${this.email})`;
}
public override canAccessResource(resourceId: string): boolean {
return true; // Registered users can access basic resources
}
public updateLastLogin(): void {
this.lastLogin = new Date();
}
}
// Second level inheritance
class PremiumUser extends RegisteredUser {
private subscriptionEndDate: Date;
private features: string[];
constructor(
id: string,
email: string,
username: string,
subscriptionMonths: number
) {
super(id, email, username);
this.subscriptionEndDate = new Date();
this.subscriptionEndDate.setMonth(
this.subscriptionEndDate.getMonth() + subscriptionMonths
);
this.features = ["ad-free", "HD-streaming", "offline-mode"];
}
public override getProfile(): string {
return `⭐ PREMIUM: ${super.getProfile()}`;
}
public override canAccessResource(resourceId: string): boolean {
// Premium users can access all resources
return this.isSubscriptionActive();
}
private isSubscriptionActive(): boolean {
return new Date() < this.subscriptionEndDate;
}
public getFeatures(): string[] {
return [...this.features];
}
}
class AdminUser extends RegisteredUser {
private permissions: Set<string>;
constructor(id: string, email: string, username: string) {
super(id, email, username);
this.permissions = new Set(["read", "write", "delete", "admin"]);
}
public override getProfile(): string {
return `🛡️ ADMIN: ${super.getProfile()}`;
}
public override canAccessResource(resourceId: string): boolean {
return true; // Admins can access everything
}
public hasPermission(permission: string): boolean {
return this.permissions.has(permission);
}
public deleteUser(userId: string): void {
console.log(`Admin deleted user: ${userId}`);
}
}
// Usage
const users: User[] = [
new User("1", "guest@example.com"),
new RegisteredUser("2", "user@example.com", "john_doe"),
new PremiumUser("3", "premium@example.com", "jane_premium", 12),
new AdminUser("4", "admin@example.com", "admin_user")
];
users.forEach(user => {
console.log(user.getProfile());
console.log(`Can access: ${user.canAccessResource("resource-1")}`);
});Method Overriding
Child classes can provide their own implementation of methods inherited from parent classes.
class PaymentProcessor {
protected processingFee: number = 0;
processPayment(amount: number): number {
const fee = this.calculateFee(amount);
const total = amount + fee;
console.log(`Processing payment: $${amount} + $${fee} fee = $${total}`);
return total;
}
protected calculateFee(amount: number): number {
return 0; // Base class has no fee
}
}
class CreditCardProcessor extends PaymentProcessor {
protected override calculateFee(amount: number): number {
return amount * 0.029 + 0.30; // 2.9% + $0.30
}
}
class PayPalProcessor extends PaymentProcessor {
protected override calculateFee(amount: number): number {
return amount * 0.034 + 0.49; // 3.4% + $0.49
}
}
class WireTransferProcessor extends PaymentProcessor {
protected override calculateFee(amount: number): number {
return 25.00; // Flat fee
}
}
// Same method, different behavior based on object type
function processAllPayments(processors: PaymentProcessor[], amount: number): void {
processors.forEach(processor => {
processor.processPayment(amount);
});
}
const processors = [
new PaymentProcessor(),
new CreditCardProcessor(),
new PayPalProcessor(),
new WireTransferProcessor()
];
processAllPayments(processors, 100);Abstract Classes
Abstract classes cannot be instantiated directly and are meant to be extended. They can contain abstract methods that must be implemented by child classes.
abstract class DatabaseConnection {
protected connectionString: string;
protected isConnected: boolean = false;
constructor(connectionString: string) {
this.connectionString = connectionString;
}
// Abstract methods - must be implemented by subclasses
abstract connect(): Promise<void>;
abstract disconnect(): Promise<void>;
abstract query(sql: string): Promise<any[]>;
abstract execute(sql: string): Promise<number>;
// Concrete method - shared by all connections
public getStatus(): string {
return this.isConnected ? "Connected" : "Disconnected";
}
protected log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
class MySQLConnection extends DatabaseConnection {
async connect(): Promise<void> {
this.log(`Connecting to MySQL: ${this.connectionString}`);
// Simulate connection
await new Promise(resolve => setTimeout(resolve, 100));
this.isConnected = true;
this.log("MySQL connected successfully");
}
async disconnect(): Promise<void> {
this.log("Disconnecting from MySQL");
this.isConnected = false;
}
async query(sql: string): Promise<any[]> {
if (!this.isConnected) throw new Error("Not connected");
this.log(`Executing query: ${sql}`);
return []; // Simulated result
}
async execute(sql: string): Promise<number> {
if (!this.isConnected) throw new Error("Not connected");
this.log(`Executing command: ${sql}`);
return 1; // Simulated affected rows
}
}
class PostgreSQLConnection extends DatabaseConnection {
async connect(): Promise<void> {
this.log(`Connecting to PostgreSQL: ${this.connectionString}`);
await new Promise(resolve => setTimeout(resolve, 100));
this.isConnected = true;
this.log("PostgreSQL connected successfully");
}
async disconnect(): Promise<void> {
this.log("Disconnecting from PostgreSQL");
this.isConnected = false;
}
async query(sql: string): Promise<any[]> {
if (!this.isConnected) throw new Error("Not connected");
this.log(`PG Query: ${sql}`);
return [];
}
async execute(sql: string): Promise<number> {
if (!this.isConnected) throw new Error("Not connected");
this.log(`PG Execute: ${sql}`);
return 1;
}
}
// Usage
async function databaseExample() {
const connections: DatabaseConnection[] = [
new MySQLConnection("mysql://localhost:3306/mydb"),
new PostgreSQLConnection("postgresql://localhost:5432/mydb")
];
for (const conn of connections) {
await conn.connect();
await conn.query("SELECT * FROM users");
console.log(`Status: ${conn.getStatus()}`);
await conn.disconnect();
}
}Interfaces vs Abstract Classes
Understanding when to use interfaces versus abstract classes:
// Interface: Contract for implementation
interface Printable {
print(): void;
getPrintData(): string;
}
// Interface: Can extend multiple interfaces
interface Saveable {
save(): Promise<void>;
load(): Promise<void>;
}
// A class can implement multiple interfaces
class Document implements Printable, Saveable {
constructor(
private content: string,
private filename: string
) {}
print(): void {
console.log(this.getPrintData());
}
getPrintData(): string {
return `Document: ${this.filename}\n${this.content}`;
}
async save(): Promise<void> {
console.log(`Saving ${this.filename}...`);
// Save logic
}
async load(): Promise<void> {
console.log(`Loading ${this.filename}...`);
// Load logic
}
}
// Abstract class: Provides base implementation
abstract class Report {
protected title: string;
protected generatedAt: Date;
constructor(title: string) {
this.title = title;
this.generatedAt = new Date();
}
// Concrete method with implementation
public getMetadata(): string {
return `Report: ${this.title} (Generated: ${this.generatedAt.toISOString()})`;
}
// Abstract method - must be implemented
abstract generateContent(): string;
abstract export(format: string): string;
}
class SalesReport extends Report {
constructor(
title: string,
private salesData: any[]
) {
super(title);
}
generateContent(): string {
return `Sales Report Content: ${this.salesData.length} records`;
}
export(format: string): string {
return `Exporting sales report to ${format}`;
}
}
// When to use which:
// - Use INTERFACE when you need multiple inheritance or pure contracts
// - Use ABSTRACT CLASS when you have shared implementation and stateComposition over Inheritance
While inheritance is powerful, composition is often preferred for building flexible systems.
// Instead of deep inheritance, use composition
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
class FileLogger implements Logger {
constructor(private filename: string) {}
log(message: string): void {
console.log(`[FILE: ${this.filename}] ${message}`);
}
}
interface Cache {
get(key: string): any;
set(key: string, value: any): void;
}
class MemoryCache implements Cache {
private store = new Map();
get(key: string): any {
return this.store.get(key);
}
set(key: string, value: any): void {
this.store.set(key, value);
}
}
// Using composition
class UserService {
constructor(
private logger: Logger,
private cache: Cache
) {}
async getUser(id: string): Promise<any> {
this.logger.log(`Fetching user: ${id}`);
// Check cache first
const cached = this.cache.get(`user_${id}`);
if (cached) {
this.logger.log(`User ${id} found in cache`);
return cached;
}
// Fetch from database (simulated)
const user = { id, name: "John Doe" };
this.cache.set(`user_${id}`, user);
this.logger.log(`User ${id} cached`);
return user;
}
}
// Flexible - can swap implementations easily
const service1 = new UserService(
new ConsoleLogger(),
new MemoryCache()
);
const service2 = new UserService(
new FileLogger("app.log"),
new MemoryCache()
);