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:
- Reduced Risk ๐: No changes to tested code
- Better Maintainability ๐ป: Add features without fear
- Cleaner Architecture ๐: Clear extension points
- 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
- ๐ฏ Use Abstractions: Define clear interfaces or abstract classes
- ๐ Think Extension Points: Identify where changes might occur
- ๐ก๏ธ Avoid Conditionals: Replace if/else chains with polymorphism
- ๐จ Keep It Simple: Donโt over-engineer for unlikely scenarios
- โจ 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:
- ๐ป Practice with the logging system exercise
- ๐๏ธ Refactor existing code to follow Open/Closed
- ๐ Move on to our next tutorial: Single Responsibility Principle
- ๐ 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! ๐๐โจ