- Published on
Mastering the Open/Closed Principle in TypeScript with Real-World Examples
- π Mastering the Open/Closed Principle in TypeScript with Real-World Examples
- π Introduction
- β Bad Example β Violating OCP
- 𧨠Problems:
- β Good Example β Applying OCP with Strategy Pattern
- β¨ Benefits of Good (OCP-Compliant) Code
- π§ How to Detect the Need for OCP
- π οΈ Techniques to Apply the Open/Closed Principle
- π§© Advanced Real-World Scenario
- π§ͺ Unit Testing Advantage
- π§ Pro Tips
- π Relationship Between OCP and SRP
- π How SRP Supports OCP
- π¦ Example: Notifications Revisited
- π― Combined Benefits
- π§ Real-World Insight
- π οΈ Quick Checklist: When Designing a Class
- π§© Final Thought
π Mastering the Open/Closed Principle in TypeScript with Real-World Examples
π Introduction
The Open/Closed Principle (OCP) is the second principle of SOLID and one of the most powerful tools in a software engineer's toolkit.
Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
That means your system should allow you to add new functionality without changing existing code.
β Bad Example β Violating OCP
Suppose you're building a payment processing module:
class PaymentProcessor {
processPayment(method: string, amount: number) {
if (method === 'paypal') {
console.log(`Processing PayPal payment: $${amount}`);
} else if (method === 'stripe') {
console.log(`Processing Stripe payment: $${amount}`);
} else if (method === 'bitcoin') {
console.log(`Processing Bitcoin payment: $${amount}`);
} else {
throw new Error('Unsupported payment method');
}
}
}
𧨠Problems:
- Every time a new payment method is introduced, you have to modify this class.
- It violates OCP by being closed to extension and open to modification.
- Harder to test and maintain.
- Increases risk of bugs when changing core logic.
β Good Example β Applying OCP with Strategy Pattern
We'll use interfaces and polymorphism to fix this:
interface PaymentMethod {
process(amount: number): void;
}
class PayPalPayment implements PaymentMethod {
process(amount: number) {
console.log(`Processing PayPal payment: $${amount}`);
}
}
class StripePayment implements PaymentMethod {
process(amount: number) {
console.log(`Processing Stripe payment: $${amount}`);
}
}
class BitcoinPayment implements PaymentMethod {
process(amount: number) {
console.log(`Processing Bitcoin payment: $${amount}`);
}
}
class PaymentProcessor {
constructor(private method: PaymentMethod) {}
pay(amount: number) {
this.method.process(amount);
}
}
Now you can add new payment methods without touching the PaymentProcessor
class.
β¨ Benefits of Good (OCP-Compliant) Code
β Benefit | π Impact |
---|---|
Easier to extend | Add new functionality without touching existing logic |
Safer changes | Reduced risk of breaking existing features |
Better testability | Each strategy (payment method) can be unit-tested in isolation |
Higher maintainability | Clear separation of responsibilities |
Enhanced readability | Code is easier to reason about |
Promotes team collaboration | Multiple devs can work on new features without conflicts |
π§ How to Detect the Need for OCP
You might need to apply OCP when:
- You have a class with growing
if/else
orswitch
statements. - You frequently need to modify a class to support new requirements.
- Your code changes frequently due to new business rules or types.
- There are many changes to the same class by different developers.
- Your system is hard to test because logic is tightly coupled.
π οΈ Techniques to Apply the Open/Closed Principle
Technique | Description |
---|---|
Use Interfaces/Abstract Classes | Allow implementation to vary without changing usage |
Strategy Pattern | Encapsulate different behaviors behind a common interface |
Template Method Pattern | Define a base algorithm with customizable steps |
Factory Pattern | Delegate object creation to a factory to avoid hard-coded classes |
Dependency Injection | Inject behavior via constructor to enable easier substitution |
π§© Advanced Real-World Scenario
Problem: Notification System
You need to send notifications through multiple channels (Email, SMS, Push, WhatsApp). Initially, you write:
class NotificationService {
send(channel: string, message: string) {
if (channel === 'email') {
// Send email
} else if (channel === 'sms') {
// Send SMS
}
// You get the idea...
}
}
Refactored with OCP:
interface Notifier {
send(message: string): void;
}
class EmailNotifier implements Notifier {
send(message: string) {
console.log(`Email: ${message}`);
}
}
class SMSNotifier implements Notifier {
send(message: string) {
console.log(`SMS: ${message}`);
}
}
class PushNotifier implements Notifier {
send(message: string) {
console.log(`Push: ${message}`);
}
}
class NotificationService {
constructor(private notifier: Notifier) {}
notify(message: string) {
this.notifier.send(message);
}
}
Now you can add WhatsAppNotifier
without touching any other class.
π§ͺ Unit Testing Advantage
const mockNotifier: Notifier = {
send: jest.fn(),
};
const service = new NotificationService(mockNotifier);
service.notify("Hello!");
expect(mockNotifier.send).toHaveBeenCalledWith("Hello!");
β‘ Easy to mock and test behavior in isolation.
π§ Pro Tips
- Keep your abstractions focused on behavior, not data.
- Don't abstract too earlyβrefactor when patterns emerge.
- Combine with SRP and Dependency Injection for powerful architecture.
π Relationship Between OCP and SRP
π§ Definitions Recap:
SRP (Single Responsibility Principle)
A class should have only one reason to change.
OCP (Open/Closed Principle)
Software entities should be open for extension, but closed for modification.
π How SRP Supports OCP
To apply the Open/Closed Principle, we must first separate concerns β and that's exactly what SRP does.
π Here's how:
SRP Helps You⦠| Which Enables OCP by⦠|
---|---|
Splitting large classes into focused components | Making it easier to extend behavior through new components |
Creating clear, single-purpose interfaces | Allowing interchangeable implementations that can be injected or extended |
Avoiding tight coupling | Enabling safe extension without breaking existing logic |
Improving modularity | Making behavior pluggable, testable, and extendable |
π¦ Example: Notifications Revisited
Let's revisit our notification system from earlier, now focusing on SRP + OCP.
β Without SRP or OCP:
class NotificationService {
send(channel: string, message: string) {
if (channel === 'email') {
// logic to send email
} else if (channel === 'sms') {
// logic to send SMS
}
}
}
- π« Breaks SRP: One class handles message formatting, channel logic, and sending.
- π« Breaks OCP: You must modify the method every time a new channel is added.
β With SRP and OCP:
Step 1: SRP β One Class, One Responsibility
interface Notifier {
send(message: string): void;
}
class EmailNotifier implements Notifier {
send(message: string) {
// Send email
}
}
class SMSNotifier implements Notifier {
send(message: string) {
// Send SMS
}
}
Each notifier class does only one thing: handles a specific channel.
Step 2: OCP β Inject Behavior
class NotificationService {
constructor(private notifier: Notifier) {}
notify(message: string) {
this.notifier.send(message);
}
}
You can now extend the system by adding new notifiers (e.g. PushNotifier
, WhatsAppNotifier
) without modifying existing code.
π― Combined Benefits
Principle | What It Ensures | How They Complement |
---|---|---|
SRP | Code has clear, atomic responsibilities | Makes code more modular and pluggable, a requirement for safe extension |
OCP | Code can be extended without changing existing parts | Depends on responsibilities being already separated to work effectively |
π§ Real-World Insight
In large projects:
- SRP prepares the codebase for OCP.
- OCP builds on SRP to ensure code remains stable as it grows.
- Violating SRP usually leads to violating OCP.
π οΈ Quick Checklist: When Designing a Class
β Does this class do only one thing? (SRP) β Can I add new features without modifying it? (OCP) β Can each class or component be tested in isolation?
If yes, you're on the right track toward SOLID design.
π§© Final Thought
Think of SRP as the foundation, and OCP as the architecture built on it.
When you follow SRP, you naturally create the environment where OCP thrives β allowing you to scale your application confidently, with fewer bugs and greater agility.
π§© Summary
The Open/Closed Principle helps create extensible and stable systems. You can handle new requirements easily while minimizing the risk of bugs. This is crucial in large-scale apps where changes are frequent.
By designing your system to be open for extension, closed for modification, you achieve:
- Clean code separation
- Better testability
- Reduced regressions
- Long-term maintainability