Introduction
Open/Closed Principle
is the second principle in SOLID
principles, it states that, a software entity should be open for extension, but closed for modification.
What is a software entity?
A software entity is a class, module, function, etc. Basically, it is a piece of code that does something.
What does that mean?
Basically, you should be able to extend the behavior of a software entity without modifying its source code.
Why would i need that?
Actually, for many reasons, let's list them:
-
Maintainable
,Your code will be more maintainable, because you will not need to modify the source code of the software entity. -
Testable
, as it does not require modification, you will not need to modify the tests. -
Reusable
, you can use the software entity in other places which can be easily extended. -
Scalable
, add more features or functions without the need to modify the original source code.
Enough Talking... Show me the code
Let's say we have a User
class, and we want to add a new feature to it, which is sendEmail
function, which sends an email to the user.
class User {
constructor(public name: string, public email: string) {}
sendEmail() {
console.log(`Sending email to ${this.name} at ${this.email}`);
}
}
Now, we want to add a new feature to the User
class, which is sendSMS
function, which sends an SMS to the user.
class User {
constructor(public name: string, public email: string) {}
sendEmail() {
console.log(`Sending email to ${this.name} at ${this.email}`);
}
sendSMS() {
console.log(`Sending SMS to ${this.name} at ${this.email}`);
}
}
Now, we want to add a new feature to the User
class, which is sendPushNotification
function, which sends a push notification to the user.
class User {
constructor(public name: string, public email: string) {}
sendEmail() {
console.log(`Sending email to ${this.name} at ${this.email}`);
}
sendSMS() {
console.log(`Sending SMS to ${this.name} at ${this.email}`);
}
sendPushNotification() {
console.log(`Sending Push Notification to ${this.name} at ${this.email}`);
}
}
Now, we want to add a new feature to the User
class, which is sendWhatsAppMessage
function, which sends a WhatsApp message to the user.
class User {
constructor(public name: string, public email: string) {}
sendEmail() {
console.log(`Sending email to ${this.name} at ${this.email}`);
}
sendSMS() {
console.log(`Sending SMS to ${this.name} at ${this.email}`);
}
sendPushNotification() {
console.log(`Sending Push Notification to ${this.name} at ${this.email}`);
}
sendWhatsAppMessage() {
console.log(`Sending WhatsApp Message to ${this.name} at ${this.email}`);
}
}
This is good, but we still have to modify the User
class every time we want to add a new feature, which is not totally good, because we are modifying the User
class and make it over complicated.
Now let's head to the Open/Closed Principle
solution.
Open/Closed Principle Solution
The idea is simple, we will make a contract A.K.A interface, which will be used among variant classes, and each class will implement the contract, and add its own implementation.
The point here is to make the contract as generic as possible, to allow multiple communication channels without touching the original User class
.
Step 1: Create a contract
interface CommunicationChannel {
send(user: User): void;
}
Step 2: Create a class for each communication channel
class EmailChannel implements CommunicationChannel {
send(user: User) {
console.log(`Sending email to ${user.name} at ${user.email}`);
}
}
Now we can add as many communication channels as we want, without touching the User
class.
class SMSChannel implements CommunicationChannel {
send(user: User) {
console.log(`Sending SMS to ${user.name} at ${user.email}`);
}
}
class PushNotificationChannel implements CommunicationChannel {
send(user: User) {
console.log(`Sending Push Notification to ${user.name} at ${user.email}`);
}
}
class WhatsAppChannel implements CommunicationChannel {
send(user: User) {
console.log(`Sending WhatsApp Message to ${user.name} at ${user.email}`);
}
}
Area Calculator Example
Let's take another example, we have a Shape
and an AreaCalculator
classes, and we want to add a new shape to the AreaCalculator
class.
class Shape {
constructor(public type: string) {}
}
class AreaCalculator {
constructor(public shapes: Shape[]) {}
area() {
return this.shapes.reduce((sum, shape) => {
if (shape.type === "circle") {
sum += Math.PI * Math.pow(shape.radius, 2);
} else if (shape.type === "square") {
sum += Math.pow(shape.side, 2);
} else if (shape.type === "rectangle") {
sum += shape.width * shape.height;
}
return sum;
}, 0);
}
}
What if we want to add a new shape, let's say triangle
, we will have to modify the AreaCalculator
class, which is not good.
class Shape {
constructor(public type: string) {}
}
class AreaCalculator {
constructor(public shapes: Shape[]) {}
area() {
return this.shapes.reduce((sum, shape) => {
if (shape.type === "circle") {
sum += Math.PI * Math.pow(shape.radius, 2);
} else if (shape.type === "square") {
sum += Math.pow(shape.side, 2);
} else if (shape.type === "rectangle") {
sum += shape.width * shape.height;
} else if (shape.type === "triangle") {
sum += (shape.base * shape.height) / 2;
}
return sum;
}, 0);
}
}
TOO BIG, is'nt it? let's break it down.
Step 1: Create a contract
interface Shape {
area(): number;
}
Step 2: Create a class for each shape
class SquareShape implements Shape {
public constructor(public side: number) {}
public area() {
return this.side * this.side;
}
}
class CircleShape implements Shape {
public constructor(public radius: number) {}
public area() {
return Math.PI * Math.pow(this.radius, 2);
}
}
class RectangleShape implements Shape {
public constructor(public width: number, public height: number) {}
public area() {
return this.width * this.height;
}
}
class TriangleShape implements Shape {
public constructor(public base: number, public height: number) {}
public area() {
return (this.base * this.height) / 2;
}
}
Step 3: Use the contract
class AreaCalculator {
public constructor(public shapes: Shape[]) {}
public sum() {
return this.shapes.reduce((total, shape) => {
return total + shape.area();
}, 0);
}
}
Clean, clear, easy to read and maintainable!
Conclusion
Open/Closed principle is very essential and crucial when you build your software architecture, it will make your code more maintainable, testable, reusable and scalable.
Following the list
You can see the updated list of design principles from the following link
https://mentoor.io/en/posts/634524154/open-closed-principle-in-typescript
Join us in our Discord Community
https://discord.gg/XDZcTuU8c8
Top comments (0)