Advantages Open Closed Principle (OCP)
The idea behind the OCP is that it promotes greater flexibility in your code. When you need to add new behavior or features, instead of modifying existing code (and thus possibly introducing new bugs), you write new code that extends the old code.
Reduced Risk of Bugs
Since we aren't modifying existing code, we're not introducing the risk of breaking existing functionality. The code that was working before we added new features will continue to work.
To understand how OCP helps to reduce the risk of adding new bugs, let's go back to the Discount example.
Without using OCP, our Discount
class might look like this:
class Customer {
constructor(public type: string) {}
}
class Discount {
giveDiscount(customer: Customer): number {
if (customer.type === "Regular") {
return 10;
} else if (customer.type === "Premium") {
return 20;
}
return 0;
}
}
Now, if we want to introduce a new type of customer, let's say a "Gold" customer
with a different discount, we would have to modify the giveDiscount
method in
the Discount
class:
class Discount {
giveDiscount(customer: Customer): number {
if (customer.type === "Regular") {
return 10;
} else if (customer.type === "Premium") {
return 20;
} else if (customer.type === "Gold") {
return 30;
}
return 0;
}
}
In this scenario, each time a new customer type is introduced, we risk
introducing bugs into our existing giveDiscount
function. For example, a
change could accidentally affect the discount given to "Regular" or "Premium"
customers. Additionally, these changes could affect other functions that rely on
the giveDiscount
function. This is a significant risk in large and complex
codebases.
Now, let's look at the OCP approach:
interface Customer {
giveDiscount(): number;
}
class RegularCustomer implements Customer {
giveDiscount(): number {
return 10;
}
}
class PremiumCustomer implements Customer {
giveDiscount(): number {
return 20;
}
}
class GoldCustomer implements Customer {
giveDiscount(): number {
return 30;
}
}
class Discount {
giveDiscount(customer: Customer): number {
return customer.giveDiscount();
}
}
With this approach, each type of customer is responsible for its own discount calculation. When adding a new customer type ("GoldCustomer"), we don't touch the existing "RegularCustomer" or "PremiumCustomer" classes. This approach significantly reduces the risk of introducing bugs into the existing codebase. Each customer class can be tested individually, and we can be confident that adding or modifying a type of customer doesn't affect the others.
This is how OCP helps to reduce the risk of bugs when adding or changing functionality in your code.
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSIncreased Code Reusability
By encouraging the development of code that is more modular, and thus more reusable. You can reuse components in different parts of your application, or even in different applications, if they're properly abstracted and closed to modifications.
The Open-Closed Principle (OCP) encourages a greater degree of code reusability through its emphasis on creating components that are open for extension but closed for modification. Let's use the previous discount example to illustrate this.
For instance, if we wanted to add a new functionality that calculates loyalty points differently for each customer type, we could use the RegularCustomer, PremiumCustomer, and GoldCustomer classes without modification, because they encapsulate the behavior associated with each customer type. Here's an example of how you could do this:
interface Customer {
giveDiscount(): number;
addLoyaltyPoints(amountSpent: number): number;
}
class RegularCustomer implements Customer {
giveDiscount(): number {
return 10;
}
addLoyaltyPoints(amountSpent: number): number {
// Let's assume that regular customers get 1 point for each dollar spent.
return amountSpent;
}
}
class PremiumCustomer implements Customer {
giveDiscount(): number {
return 20;
}
addLoyaltyPoints(amountSpent: number): number {
// Let's assume that premium customers get 3 points for each dollar spent.
return 3 * amountSpent;
}
}
class GoldCustomer implements Customer {
giveDiscount(): number {
return 30;
}
addLoyaltyPoints(amountSpent: number): number {
// Gold customers get 5 points for each dollar spent.
return 5 * amountSpent;
}
}
class Discount {
giveDiscount(customer: Customer): number {
return customer.giveDiscount();
}
}
class LoyaltyProgram {
addPoints(customer: Customer, amountSpent: number): number {
return customer.addLoyaltyPoints(amountSpent);
}
}
In the code above, I've added a method to the Customer
interface
called addLoyaltyPoints
. Each class that implements Customer
has to define
how this method works. The GoldCustomer
class defines it as returning 5 times
the amount spent.
This way, we can continue to add new behaviors and classes to our code without
modifying existing classes, maintaining adherence to the Open-Closed Principle.
We also have a new LoyaltyProgram
class that adds points to any customer,
further demonstrating the reusability of the code.
Simplified Versioning and Patching
Since old, stable modules remain untouched, it's easier to create patches that consist of new modules, rather than having to distribute a new version of an existing module.
The Open-Closed Principle (OCP) also plays a significant role in simplifying versioning and patching of software. By following this principle, existing, tested components don't need to be modified to add new features or behavior to the software. Instead, new functionality is added via extension, typically by adding new classes or modules. This separation has important implications for versioning and patching.
For example if a new feature like a VIPCustomer
class is added to a new
version
of the software, it can be added without changing the existing code. If users
don't need the new feature, they can continue using the older version of the
software without any problems. Users that do need the new feature can switch to
the new version of the software, confident that the existing functionality they
rely on hasn't been changed and still works as expected.
interface Customer {
giveDiscount(): number;
addLoyaltyPoints(amountSpent: number): number;
}
class RegularCustomer implements Customer {
giveDiscount(): number {
return 10;
}
addLoyaltyPoints(amountSpent: number): number {
return amountSpent;
}
}
class PremiumCustomer implements Customer {
giveDiscount(): number {
return 20;
}
addLoyaltyPoints(amountSpent: number): number {
return 3 * amountSpent;
}
}
class GoldCustomer implements Customer {
giveDiscount(): number {
return 30;
}
addLoyaltyPoints(amountSpent: number): number {
return 5 * amountSpent;
}
}
class VIPCustomer implements Customer {
giveDiscount(): number {
return 50;
}
addLoyaltyPoints(amountSpent: number): number {
return 10 * amountSpent;
}
}
class Discount {
giveDiscount(customer: Customer): number {
return customer.giveDiscount();
}
}
class LoyaltyProgram {
addPoints(customer: Customer, amountSpent: number): number {
return customer.addLoyaltyPoints(amountSpent);
}
}
With this new addition, we can see that the Discount and LoyaltyProgram classes remained unaltered even as a new type of customer was introduced. As long as our new VIPCustomer class correctly implements the Customer interface, we can be confident that our existing code will continue to work correctly. This is a direct result of following the Open-Closed Principle.
What Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.