The SOLID principles, a set of five design principles that enhance
object-oriented programming (OOP). We discussed the
Single Responsibility Principle (SRP) in
this article.
SOLID Design Principle is made up of five different principles:
- S: Single Responsibility Principle (SRP)
- O: Open-Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
This blog post reviews the Open-Closed Principle (OCP) and shows how it
can be applied using Java examples.
Open-Closed Principle
This is the second principle of the SOLID design principle group. It tells us:
Classes or modules should be open for extension but closed for modification.
It states that we should not modify any existing classes or
modules. Rather, we should create a new entity by inheriting the
properties of the existing classes and modules.
So, this principle tells us to use inheritance and overriding to extend
existing behaviors.
Example without OCP
Assume an e-commerce platform has two discount types: flat discount
and percentage discount. Here's our discount system. As you can
see, the calculateDiscount(...) method is used to compute a
discount on the purchase price based on the discount type.
public class DiscountSystem {
private Double flatDiscountAmount;
private Double discountPercentage;
private Double baseDiscountPercentage;
// CALCULATE DISCOUNT
public Double calculateDiscount(EDiscountType type, Double amount) {
Double discountedAmount;
switch (type.name()) {
case "FLAT":
discountedAmount = amount - (amount * baseDiscountPercentage / 100) - flatDiscountAmount;
break;
case "PERCENTAGE":
discountedAmount = amount - (amount * baseDiscountPercentage / 100)
- (amount * discountPercentage / 100);
break;
default:
discountedAmount = 0D;
break;
}
return discountedAmount;
}
public Double getFlatDiscountAmount() {
return flatDiscountAmount;
}
public DiscountSystem setFlatDiscountAmount(Double flatDiscountAmount) {
this.flatDiscountAmount = flatDiscountAmount;
return this;
}
public Double getDiscountPercentage() {
return discountPercentage;
}
public DiscountSystem setDiscountPercentage(Double discountPercentage) {
this.discountPercentage = discountPercentage;
return this;
}
public Double getBaseDiscountPercentage() {
return baseDiscountPercentage;
}
public DiscountSystem setBaseDiscountPercentage(Double baseDiscountPercentage) {
this.baseDiscountPercentage = baseDiscountPercentage;
return this;
}
}
public enum EDiscountType {
FLAT, PERCENTAGE;
}
Assume the e-commerce platform wants to introduce another discount system. So
first, we'll create a new discount type. Then, create a new case block
to implement the new discount logic. After a few days, they plan to introduce
another new discount system. So, once again, we must take the same steps to
implement the new discount logic.
Every time, we modify the existing code to add new business logic. As a
result, these frequent changes increase the risk of breaking existing
functionality. This is what the Open-Closed Principle (OCP)
says to avoid.
Example with OCP
To implement OCP, we must first define an abstract class with an abstract
method. Here's our Discount abstract class. This abstract class has its
own property and an abstract method called
calculateDiscount(...). This abstract method will be
overridden in the class that extends the Discount class.
public abstract class Discount {
private Double baseDiscountPercentage;
public abstract Double calculateDiscount(Double amount);
public Double getBaseDiscountPercentage() {
return baseDiscountPercentage;
}
public void setBaseDiscountPercentage(Double discountPercentage) {
this.baseDiscountPercentage = discountPercentage;
}
}
The code snippet below is for FlatDiscountService, which extends the
Discount class. It also overrides the
calculateDiscount(...) method to incorporate its own discount
business logic.
public class FlatDiscountService extends Discount {
private Double flatDiscountAmount;
public Double getFlatDiscountAmount() {
return flatDiscountAmount;
}
public FlatDiscountService setFlatDiscountAmount(Double discountAmount) {
this.flatDiscountAmount = discountAmount;
return this;
}
@Override
public Double calculateDiscount(Double amount) {
return amount - (amount * super.getBaseDiscountPercentage() / 100) - flatDiscountAmount;
}
}
We can develop another new discount service. This is
PercentageDiscountService that uses the Discount class.
public class PercentageDiscountService extends Discount {
private Double discountPercentage;
public Double getDiscountPercentage() {
return discountPercentage;
}
public PercentageDiscountService setDiscountPercentage(Double discountPercentage) {
this.discountPercentage = discountPercentage;
return this;
}
@Override
public Double calculateDiscount(Double amount) {
return amount - (amount * super.getBaseDiscountPercentage() / 100) - (amount * discountPercentage / 100);
}
}
If the e-commerce platform wants to introduce a new discount system, it simply
creates a new class by extending the abstract one. At the same time, our
current discount system will remain unchanged.
Here is our test class to test the discount services.
public class OcpGoodExampleMain {
public static void main(String[] args) {
Double purchasedAmount = 4710.78;
FlatDiscountService flatDiscountService = new FlatDiscountService()
.setFlatDiscountAmount(250.98);
flatDiscountService.setBaseDiscountPercentage(1D);
System.out.println("Actual purchased amount: " + purchasedAmount);
System.out.println("After flat discount: " + flatDiscountService.calculateDiscount(purchasedAmount));
System.out.println("-------------------------------------");
purchasedAmount = 7980.98;
PercentageDiscountService percentageDiscountService = new PercentageDiscountService()
.setDiscountPercentage(3.6);
percentageDiscountService.setBaseDiscountPercentage(1.7);
System.out.println("Actual purchased amount: " + purchasedAmount);
System.out
.println("After percentage discount: " + percentageDiscountService.calculateDiscount(purchasedAmount));
}
}
Using the Open-Closed Principle (COP), new discount types can be added
without changing the Discount class. When we add new discount strategies, the
core system is left untouched.
The Open-Closed Principle (OCP) can be applied in various real-world
software development scenarios. In those cases, we need to add functionality
without changing the existing code. Here are some real-world examples.
- Payment Systems: Credit card payments are initially accepted by an online store, but PayPal, Apple Pay, and cryptocurrency payments must be added later.
- Logging System: An application begins by logging into a text file, but it must eventually support logging into databases, cloud services (such as AWS CloudWatch), or external monitoring tools.
- Notification Services: A notification system sends email notifications at first, but it must eventually support SMS, push notifications, and WhatsApp messages.
- User Role Management: A web application has an Admin role, but additional roles such as Editor, Viewer, and Moderator with varying permissions must be added.
- Machine Learning Model Deployment: A system begins with a simple linear regression model but eventually needs to support deep learning models, decision trees, and ensemble methods.
Happy coding!!! 😊