+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 278 of 354

๐Ÿ“˜ Open/Closed Principle: Extension without Modification

Master open/closed principle: extension without modification in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on the Open/Closed Principle! ๐ŸŽ‰ In this guide, weโ€™ll explore one of the most powerful SOLID principles that will transform how you design your TypeScript applications.

Youโ€™ll discover how the Open/Closed Principle can help you build software thatโ€™s flexible yet stable. Whether youโ€™re building web applications ๐ŸŒ, server-side code ๐Ÿ–ฅ๏ธ, or libraries ๐Ÿ“š, understanding this principle is essential for writing code that gracefully handles change without breaking existing functionality.

By the end of this tutorial, youโ€™ll feel confident applying the Open/Closed Principle in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding the Open/Closed Principle

๐Ÿค” What is the Open/Closed Principle?

The Open/Closed Principle is like building with LEGO blocks ๐Ÿงฑ. Think of it as creating structures where you can add new pieces without breaking or modifying the existing ones. Just like how you can extend a LEGO castle by adding new towers without rebuilding the foundation!

In TypeScript terms, the Open/Closed Principle states that software entities (classes, modules, functions) should be:

  • โœจ Open for extension: You can add new functionality
  • ๐Ÿš€ Closed for modification: You donโ€™t change existing code
  • ๐Ÿ›ก๏ธ Stable yet flexible: New features donโ€™t break old ones

๐Ÿ’ก Why Use the Open/Closed Principle?

Hereโ€™s why developers love this principle:

  1. Reduced Risk ๐Ÿ”’: No changes to tested code
  2. Better Maintainability ๐Ÿ’ป: Add features without fear
  3. Cleaner Architecture ๐Ÿ“–: Clear extension points
  4. Easier Testing ๐Ÿ”ง: New code doesnโ€™t affect old tests

Real-world example: Imagine building a payment system ๐Ÿ’ณ. With the Open/Closed Principle, you can add new payment methods (PayPal, crypto, etc.) without touching the existing credit card processing code!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example

Letโ€™s start with a friendly example:

// ๐Ÿ‘‹ Let's build a shape calculator!

// ๐ŸŽจ Base shape interface
interface Shape {
  type: string;
  calculateArea(): number;
}

// ๐ŸŸฆ Rectangle implementation
class Rectangle implements Shape {
  type = "rectangle";
  
  constructor(
    private width: number,
    private height: number
  ) {}
  
  calculateArea(): number {
    return this.width * this.height; // ๐Ÿ“ Width ร— Height
  }
}

// ๐ŸŸก Circle implementation
class Circle implements Shape {
  type = "circle";
  
  constructor(private radius: number) {}
  
  calculateArea(): number {
    return Math.PI * this.radius * this.radius; // ๐ŸŽฏ ฯ€rยฒ
  }
}

// โœจ Area calculator - open for extension!
class AreaCalculator {
  calculateTotalArea(shapes: Shape[]): number {
    return shapes.reduce((total, shape) => {
      return total + shape.calculateArea();
    }, 0);
  }
}

๐Ÿ’ก Explanation: Notice how we can add new shapes without modifying the AreaCalculator class! The calculator is closed for modification but open for extension through the Shape interface.

๐ŸŽฏ Common Patterns

Here are patterns youโ€™ll use daily:

// ๐Ÿ—๏ธ Pattern 1: Strategy Pattern
interface PaymentStrategy {
  processPayment(amount: number): void;
}

class CreditCardPayment implements PaymentStrategy {
  processPayment(amount: number): void {
    console.log(`๐Ÿ’ณ Processing $${amount} via credit card`);
  }
}

class PayPalPayment implements PaymentStrategy {
  processPayment(amount: number): void {
    console.log(`๐Ÿ…ฟ๏ธ Processing $${amount} via PayPal`);
  }
}

// ๐ŸŽจ Pattern 2: Template Method
abstract class DataProcessor {
  // ๐Ÿ”„ Template method
  process(data: string): void {
    const validated = this.validate(data);
    const transformed = this.transform(validated);
    this.save(transformed);
  }
  
  protected abstract validate(data: string): string;
  protected abstract transform(data: string): string;
  protected abstract save(data: string): void;
}

// ๐Ÿ”„ Pattern 3: Plugin Architecture
interface Plugin {
  name: string;
  execute(): void;
}

class PluginManager {
  private plugins: Plugin[] = [];
  
  register(plugin: Plugin): void {
    this.plugins.push(plugin);
    console.log(`๐Ÿ”Œ Registered plugin: ${plugin.name}`);
  }
  
  executeAll(): void {
    this.plugins.forEach(plugin => plugin.execute());
  }
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce Discount System

Letโ€™s build something real:

// ๐Ÿ›๏ธ Discount calculator following Open/Closed Principle
interface DiscountRule {
  name: string;
  emoji: string;
  isApplicable(order: Order): boolean;
  calculateDiscount(order: Order): number;
}

interface Order {
  total: number;
  customerType: "regular" | "vip" | "new";
  items: number;
  couponCode?: string;
}

// ๐Ÿ’ฐ Base discounts
class PercentageDiscount implements DiscountRule {
  constructor(
    public name: string,
    public emoji: string,
    private percentage: number,
    private condition: (order: Order) => boolean
  ) {}
  
  isApplicable(order: Order): boolean {
    return this.condition(order);
  }
  
  calculateDiscount(order: Order): number {
    return order.total * (this.percentage / 100);
  }
}

// ๐ŸŽฏ Specific discount implementations
const newCustomerDiscount = new PercentageDiscount(
  "New Customer",
  "๐ŸŽ‰",
  15,
  (order) => order.customerType === "new"
);

const vipDiscount = new PercentageDiscount(
  "VIP Member",
  "โญ",
  20,
  (order) => order.customerType === "vip"
);

const bulkDiscount = new PercentageDiscount(
  "Bulk Order",
  "๐Ÿ“ฆ",
  10,
  (order) => order.items >= 10
);

// ๐Ÿ›’ Discount calculator (closed for modification)
class DiscountCalculator {
  private rules: DiscountRule[] = [];
  
  // โž• Open for extension via registration
  registerRule(rule: DiscountRule): void {
    this.rules.push(rule);
    console.log(`${rule.emoji} Registered discount: ${rule.name}`);
  }
  
  // ๐Ÿ’ฐ Calculate best discount
  calculateBestDiscount(order: Order): number {
    const applicableDiscounts = this.rules
      .filter(rule => rule.isApplicable(order))
      .map(rule => ({
        rule,
        amount: rule.calculateDiscount(order)
      }));
    
    if (applicableDiscounts.length === 0) {
      return 0;
    }
    
    const best = applicableDiscounts.reduce((prev, curr) =>
      curr.amount > prev.amount ? curr : prev
    );
    
    console.log(`${best.rule.emoji} Applied ${best.rule.name}: $${best.amount.toFixed(2)}`);
    return best.amount;
  }
}

// ๐ŸŽฎ Let's use it!
const calculator = new DiscountCalculator();
calculator.registerRule(newCustomerDiscount);
calculator.registerRule(vipDiscount);
calculator.registerRule(bulkDiscount);

// ๐ŸŽŠ Easy to add new discounts without modifying calculator!
const holidayDiscount = new PercentageDiscount(
  "Holiday Special",
  "๐ŸŽ„",
  25,
  (order) => order.couponCode === "HOLIDAY2024"
);
calculator.registerRule(holidayDiscount);

๐ŸŽฏ Try it yourself: Add a โ€œloyalty pointsโ€ discount or a โ€œflash saleโ€ discount without modifying the calculator!

๐ŸŽฎ Example 2: Game Achievement System

Letโ€™s make it fun:

// ๐Ÿ† Achievement system that's extensible
interface Achievement {
  id: string;
  name: string;
  emoji: string;
  points: number;
  checkCondition(player: Player): boolean;
}

interface Player {
  name: string;
  score: number;
  level: number;
  playtime: number; // in minutes
  enemiesDefeated: number;
  achievements: string[];
}

// ๐ŸŽฏ Base achievement class
abstract class BaseAchievement implements Achievement {
  constructor(
    public id: string,
    public name: string,
    public emoji: string,
    public points: number
  ) {}
  
  abstract checkCondition(player: Player): boolean;
}

// ๐ŸŒŸ Specific achievements
class ScoreAchievement extends BaseAchievement {
  constructor(
    id: string,
    name: string,
    emoji: string,
    points: number,
    private requiredScore: number
  ) {
    super(id, name, emoji, points);
  }
  
  checkCondition(player: Player): boolean {
    return player.score >= this.requiredScore;
  }
}

class LevelAchievement extends BaseAchievement {
  constructor(
    id: string,
    name: string,
    emoji: string,
    points: number,
    private requiredLevel: number
  ) {
    super(id, name, emoji, points);
  }
  
  checkCondition(player: Player): boolean {
    return player.level >= this.requiredLevel;
  }
}

// ๐Ÿ… Achievement manager (closed for modification)
class AchievementManager {
  private achievements: Achievement[] = [];
  
  // โž• Open for extension
  registerAchievement(achievement: Achievement): void {
    this.achievements.push(achievement);
    console.log(`${achievement.emoji} New achievement available: ${achievement.name}`);
  }
  
  // ๐ŸŽŠ Check for new achievements
  checkAchievements(player: Player): Achievement[] {
    const newAchievements = this.achievements.filter(achievement => {
      const hasAchievement = player.achievements.includes(achievement.id);
      const meetsCondition = achievement.checkCondition(player);
      return !hasAchievement && meetsCondition;
    });
    
    newAchievements.forEach(achievement => {
      console.log(`๐ŸŽ‰ ${player.name} unlocked: ${achievement.emoji} ${achievement.name}!`);
      player.achievements.push(achievement.id);
    });
    
    return newAchievements;
  }
  
  // ๐Ÿ“Š Get total points
  getTotalPoints(player: Player): number {
    return player.achievements.reduce((total, achievementId) => {
      const achievement = this.achievements.find(a => a.id === achievementId);
      return total + (achievement?.points || 0);
    }, 0);
  }
}

// ๐ŸŽฎ Create achievements
const manager = new AchievementManager();

// ๐Ÿ“ˆ Score achievements
manager.registerAchievement(new ScoreAchievement(
  "first_100", "Century Club", "๐Ÿ’ฏ", 10, 100
));
manager.registerAchievement(new ScoreAchievement(
  "high_scorer", "High Scorer", "๐Ÿš€", 25, 1000
));

// ๐ŸŽฏ Level achievements
manager.registerAchievement(new LevelAchievement(
  "level_5", "Rising Star", "โญ", 15, 5
));
manager.registerAchievement(new LevelAchievement(
  "level_10", "Master Player", "๐Ÿ‘‘", 50, 10
));

// ๐Ÿ†• Easy to add new achievement types!
class ComboAchievement extends BaseAchievement {
  constructor(
    id: string,
    name: string,
    emoji: string,
    points: number,
    private requiredScore: number,
    private requiredLevel: number
  ) {
    super(id, name, emoji, points);
  }
  
  checkCondition(player: Player): boolean {
    return player.score >= this.requiredScore && 
           player.level >= this.requiredLevel;
  }
}

manager.registerAchievement(new ComboAchievement(
  "elite", "Elite Player", "๐Ÿ†", 100, 500, 7
));

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Decorator Pattern

When youโ€™re ready to level up, try this advanced pattern:

// ๐ŸŽฏ Advanced notification system with decorators
interface Notification {
  send(message: string): void;
}

// ๐Ÿ“ง Base notification
class EmailNotification implements Notification {
  send(message: string): void {
    console.log(`๐Ÿ“ง Email: ${message}`);
  }
}

// ๐ŸŽจ Notification decorator base
abstract class NotificationDecorator implements Notification {
  constructor(protected notification: Notification) {}
  
  abstract send(message: string): void;
}

// โœจ Add emoji decorator
class EmojiDecorator extends NotificationDecorator {
  constructor(
    notification: Notification,
    private emoji: string
  ) {
    super(notification);
  }
  
  send(message: string): void {
    this.notification.send(`${this.emoji} ${message}`);
  }
}

// ๐ŸŒŸ Add priority decorator
class PriorityDecorator extends NotificationDecorator {
  constructor(
    notification: Notification,
    private priority: "low" | "medium" | "high"
  ) {
    super(notification);
  }
  
  send(message: string): void {
    const priorityEmoji = {
      low: "๐ŸŸข",
      medium: "๐ŸŸก",
      high: "๐Ÿ”ด"
    };
    this.notification.send(`${priorityEmoji[this.priority]} [${this.priority.toUpperCase()}] ${message}`);
  }
}

// ๐Ÿช„ Using the decorators
let notification: Notification = new EmailNotification();
notification = new EmojiDecorator(notification, "๐ŸŽ‰");
notification = new PriorityDecorator(notification, "high");
notification.send("System update completed!");

๐Ÿ—๏ธ Advanced Topic 2: Factory Pattern with Open/Closed

For the brave developers:

// ๐Ÿš€ Extensible factory pattern
interface Vehicle {
  type: string;
  emoji: string;
  start(): void;
  stop(): void;
}

// ๐Ÿญ Vehicle factory with registration
class VehicleFactory {
  private creators = new Map<string, () => Vehicle>();
  
  // ๐Ÿ“ Register new vehicle types
  registerVehicle(type: string, creator: () => Vehicle): void {
    this.creators.set(type, creator);
    console.log(`๐Ÿญ Registered new vehicle type: ${type}`);
  }
  
  // ๐ŸŽจ Create vehicles
  createVehicle(type: string): Vehicle {
    const creator = this.creators.get(type);
    if (!creator) {
      throw new Error(`Unknown vehicle type: ${type}`);
    }
    return creator();
  }
}

// ๐Ÿš— Vehicle implementations
class Car implements Vehicle {
  type = "car";
  emoji = "๐Ÿš—";
  
  start(): void {
    console.log(`${this.emoji} Car engine started! Vroom!`);
  }
  
  stop(): void {
    console.log(`${this.emoji} Car parked safely`);
  }
}

class Bicycle implements Vehicle {
  type = "bicycle";
  emoji = "๐Ÿšฒ";
  
  start(): void {
    console.log(`${this.emoji} Started pedaling!`);
  }
  
  stop(): void {
    console.log(`${this.emoji} Bicycle stopped`);
  }
}

// ๐Ÿญ Setup factory
const factory = new VehicleFactory();
factory.registerVehicle("car", () => new Car());
factory.registerVehicle("bicycle", () => new Bicycle());

// ๐Ÿ†• Easy to add new vehicles!
class ElectricScooter implements Vehicle {
  type = "scooter";
  emoji = "๐Ÿ›ด";
  
  start(): void {
    console.log(`${this.emoji} Electric scooter powered on!`);
  }
  
  stop(): void {
    console.log(`${this.emoji} Scooter powered off`);
  }
}

factory.registerVehicle("scooter", () => new ElectricScooter());

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Modifying Existing Classes

// โŒ Wrong way - modifying existing class!
class PaymentProcessor {
  process(type: string, amount: number): void {
    if (type === "credit") {
      console.log("Processing credit card...");
    } else if (type === "paypal") {
      console.log("Processing PayPal...");
    } else if (type === "bitcoin") { // ๐Ÿ’ฅ Adding new code!
      console.log("Processing Bitcoin...");
    }
  }
}

// โœ… Correct way - use abstraction!
interface PaymentMethod {
  process(amount: number): void;
}

class PaymentProcessor {
  private methods = new Map<string, PaymentMethod>();
  
  registerMethod(type: string, method: PaymentMethod): void {
    this.methods.set(type, method);
  }
  
  process(type: string, amount: number): void {
    const method = this.methods.get(type);
    if (method) {
      method.process(amount); // โœ… No modification needed!
    }
  }
}

๐Ÿคฏ Pitfall 2: Over-Engineering

// โŒ Too complex for simple cases!
interface GreeterStrategy {
  greet(name: string): string;
}

class FormalGreeter implements GreeterStrategy {
  greet(name: string): string {
    return `Good day, ${name}`;
  }
}
// ... 10 more classes for a simple greeting!

// โœ… Keep it simple when appropriate!
type GreetingStyle = "formal" | "casual" | "friendly";

const greetings: Record<GreetingStyle, (name: string) => string> = {
  formal: (name) => `Good day, ${name}`,
  casual: (name) => `Hey ${name}!`,
  friendly: (name) => `Hi ${name}! ๐Ÿ‘‹`
};

function greet(style: GreetingStyle, name: string): string {
  return greetings[style](name);
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Use Abstractions: Define clear interfaces or abstract classes
  2. ๐Ÿ“ Think Extension Points: Identify where changes might occur
  3. ๐Ÿ›ก๏ธ Avoid Conditionals: Replace if/else chains with polymorphism
  4. ๐ŸŽจ Keep It Simple: Donโ€™t over-engineer for unlikely scenarios
  5. โœจ Document Extension: Make it clear how to extend your code

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Flexible Logging System

Create a logging system that follows the Open/Closed Principle:

๐Ÿ“‹ Requirements:

  • โœ… Support multiple log destinations (console, file, remote)
  • ๐Ÿท๏ธ Different log levels (info, warning, error)
  • ๐Ÿ‘ค Customizable formatting
  • ๐Ÿ“… Timestamp support
  • ๐ŸŽจ Each logger needs an emoji identifier!

๐Ÿš€ Bonus Points:

  • Add filtering by log level
  • Implement log rotation
  • Create a composite logger that logs to multiple destinations

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Our extensible logging system!
interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: Date;
  context?: Record<string, any>;
}

type LogLevel = "info" | "warning" | "error";

interface Logger {
  name: string;
  emoji: string;
  log(entry: LogEntry): void;
}

// ๐Ÿ“ Base loggers
class ConsoleLogger implements Logger {
  name = "Console";
  emoji = "๐Ÿ’ป";
  
  log(entry: LogEntry): void {
    const levelEmoji = {
      info: "โ„น๏ธ",
      warning: "โš ๏ธ",
      error: "โŒ"
    };
    
    console.log(
      `${this.emoji} [${entry.timestamp.toISOString()}] ` +
      `${levelEmoji[entry.level]} ${entry.level.toUpperCase()}: ` +
      `${entry.message}`
    );
  }
}

class FileLogger implements Logger {
  name = "File";
  emoji = "๐Ÿ“";
  
  constructor(private filename: string) {}
  
  log(entry: LogEntry): void {
    const logLine = `[${entry.timestamp.toISOString()}] ${entry.level}: ${entry.message}`;
    console.log(`${this.emoji} Writing to ${this.filename}: ${logLine}`);
  }
}

// ๐ŸŽจ Formatters
interface LogFormatter {
  format(entry: LogEntry): string;
}

class JsonFormatter implements LogFormatter {
  format(entry: LogEntry): string {
    return JSON.stringify({
      ...entry,
      timestamp: entry.timestamp.toISOString()
    });
  }
}

// ๐Ÿš€ Advanced logger with formatting
class FormattedLogger implements Logger {
  constructor(
    public name: string,
    public emoji: string,
    private baseLogger: Logger,
    private formatter: LogFormatter
  ) {}
  
  log(entry: LogEntry): void {
    const formatted = this.formatter.format(entry);
    this.baseLogger.log({
      ...entry,
      message: formatted
    });
  }
}

// ๐ŸŽฏ Logger manager (closed for modification)
class LogManager {
  private loggers: Logger[] = [];
  private minimumLevel: Record<LogLevel, number> = {
    info: 0,
    warning: 1,
    error: 2
  };
  
  // โž• Register new loggers
  registerLogger(logger: Logger): void {
    this.loggers.push(logger);
    console.log(`${logger.emoji} Registered logger: ${logger.name}`);
  }
  
  // ๐Ÿ“ Log to all registered loggers
  log(level: LogLevel, message: string, context?: Record<string, any>): void {
    const entry: LogEntry = {
      level,
      message,
      timestamp: new Date(),
      context
    };
    
    this.loggers.forEach(logger => logger.log(entry));
  }
  
  // ๐ŸŽฏ Helper methods
  info(message: string, context?: Record<string, any>): void {
    this.log("info", message, context);
  }
  
  warning(message: string, context?: Record<string, any>): void {
    this.log("warning", message, context);
  }
  
  error(message: string, context?: Record<string, any>): void {
    this.log("error", message, context);
  }
}

// ๐ŸŽฎ Test it out!
const logManager = new LogManager();

// ๐Ÿ“ Register basic loggers
logManager.registerLogger(new ConsoleLogger());
logManager.registerLogger(new FileLogger("app.log"));

// ๐ŸŽจ Register formatted logger
const jsonLogger = new FormattedLogger(
  "JSON Console",
  "๐ŸŽจ",
  new ConsoleLogger(),
  new JsonFormatter()
);
logManager.registerLogger(jsonLogger);

// ๐Ÿ†• Easy to add new logger types!
class SlackLogger implements Logger {
  name = "Slack";
  emoji = "๐Ÿ’ฌ";
  
  log(entry: LogEntry): void {
    if (entry.level === "error") {
      console.log(`${this.emoji} Sending to Slack: ๐Ÿšจ ${entry.message}`);
    }
  }
}

logManager.registerLogger(new SlackLogger());

// ๐Ÿ“Š Test logging
logManager.info("Application started successfully!");
logManager.warning("Memory usage is high");
logManager.error("Failed to connect to database");

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Design extensible systems without modifying existing code ๐Ÿ’ช
  • โœ… Use abstractions to create flexible architectures ๐Ÿ›ก๏ธ
  • โœ… Apply design patterns that follow Open/Closed ๐ŸŽฏ
  • โœ… Avoid common pitfalls like over-engineering ๐Ÿ›
  • โœ… Build maintainable software that embraces change! ๐Ÿš€

Remember: The Open/Closed Principle helps you write code thatโ€™s stable yet flexible. Itโ€™s like building with LEGO - you can always add more without breaking whatโ€™s already there! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered the Open/Closed Principle!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the logging system exercise
  2. ๐Ÿ—๏ธ Refactor existing code to follow Open/Closed
  3. ๐Ÿ“š Move on to our next tutorial: Single Responsibility Principle
  4. ๐ŸŒŸ Share your extensible designs with others!

Remember: Great software architecture is about making the right things easy to change. Keep coding, keep learning, and most importantly, have fun! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ