// Pending picture
- An entity
(e.g., classes, modules, functions, etc.)
should be open for extension but closed for modification. This means you should be able to add new functionality without changing the existing code. - Extend functionality by adding new code instead of changing existing code.
- Goal: Get to a point where you can never break the core of your system.
- Importance:
OCP
encourages a more stable and resilient codebase. It promotes the use ofinterfaces
andabstract classes
to allow for behaviors to be extended without modifying existing code. - Writing code structure in such a way new functionality can be added by adding new code not by modifying existing code.
- OPEN for
extending
and CLOSE formodification
The Open-Closed Principle states that classes should be open for extension but closed for modification. This means that the behavior of a class should be extendable without modifying its source code.
Once a class is written, it should be closed for modifications but open for extensions.
- Your smartphone — you don’t open it up to add features; you just download apps to extend its capabilities.
The UML diagram below depicts the relationship between a graphic editor
and the shapes
it can draw.
// Class representing a graphic editor
public class GraphicEditor {
// Method to draw a shape
public void drawShape(Shape s) {
// Check the type of the shape and call the appropriate drawing method
if (s.type == 1) {
drawRectangle(s);
} else if (s.type == 2) {
drawCircle(s);
}
// Every time a new shape is added, this method has to change
}
// Method to draw a circle
public void drawCircle(Circle r) {
// logic to draw a circle
}
// Method to draw a rectangle
public void drawRectangle(Rectangle r) {
// logic to draw a rectangle
}
}
// Class representing a shape
public class Shape {
int type; // Type of the shape
}
// Class representing a rectangle, which is a type of shape
public class Rectangle extends Shape {
// Constructor to set the type of the shape to 1 (rectangle)
public Rectangle() {
super.type = 1;
}
}
// Class representing a circle, which is a type of shape
public class Circle extends Shape {
// Constructor to set the type of the shape to 2 (circle)
public Circle() {
super.type = 2;
}
}
When we want to add a new shape for the graphic editor to be able to draw, the method
drawShape
in theGraphicEditor
class has to be modified. Such modification is not desirable as it is changing the existing code in order to add a new functionality. This makes the code hard to maintain as the code has to keep changing with more shapes added. More importantly, it could break the program as new changes may lead to undesirable consequences on other classes that also interact with theGraphicEditor
class.
To resolve the issue, we could introduce a layer of abstraction
, which is the Shape
interface, between the GraphicEditor
class and the different shape classes that will implement this interface, as shown below.
When a new shape is added, such as an oval
, there is no need to modify the code in the GraphicEditor
class. Instead, we can create a new class for the oval shape that just has to implement the Shape
interface with its own draw method defined. The code implementation is shown below.
// Class representing a graphic editor
public class GraphicEditor {
// Method to draw a shape
public void drawShape(Shape s) {
s.draw(); // Calls the draw method of the shape
}
}
// Interface representing a shape
public interface Shape {
void draw(); // Method to draw the shape
}
// Class representing a rectangle, which implements the Shape interface
public class Rectangle implements Shape {
// Method to draw a rectangle
public void draw() {
// logic to draw a rectangle
}
}
// Class representing a circle, which implements the Shape interface
public class Circle implements Shape {
// Method to draw a circle
public void draw() {
// logic to draw a circle
}
}
// Class representing an oval, which implements the Shape interface
public class Oval implements Shape {
// Method to draw an oval
public void draw() {
// logic to draw an oval
}
}
This implementation method aligns with the open-closed principle as we no longer have to modify any existing code, but instead, we are just extending the code by adding new shape classes that implement the
Shape
interface whenever new shapes are introduced for the graphic editor to draw.
// Class representing a graphic editor
public class GraphicEditor {
// Method to draw a shape
public void drawShape(Shape s) {
s.draw(); // Calls the draw method of the shape
}
}
// Interface representing a shape
interface Shape {
void draw(); // Method to draw the shape
}
// Class representing a rectangle, which implements the Shape interface
class Rectangle implements Shape {
// Method to draw a rectangle
public void draw() {
System.out.println("Drawing a rectangle"); // Example logic to draw a rectangle
}
}
// Class representing a circle, which implements the Shape interface
class Circle implements Shape {
// Method to draw a circle
public void draw() {
System.out.println("Drawing a circle"); // Example logic to draw a circle
}
}
// Class representing an oval, which implements the Shape interface
class Oval implements Shape {
// Method to draw an oval
public void draw() {
System.out.println("Drawing an oval"); // Example logic to draw an oval
}
}
// Main class to test the GraphicEditor
public class Main {
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
// Create shapes
Shape rectangle = new Rectangle();
Shape circle = new Circle();
Shape oval = new Oval();
// Draw shapes
editor.drawShape(rectangle);
editor.drawShape(circle);
editor.drawShape(oval);
}
}
The UML diagram below depicts the relationship between a ecommerce app
and the payment processor
it can draw.
public class EcommerceApp {
public void payment(PaymentProcessor p) {
if (p.type == 1) {
payCredit(p);
} else if (p.type == 2) {
payDebit(p);
}
// Every time a new payment processor is added, this method has to change
}
public void payCredit(CreditCard c) {
// logic to pay via credit card
}
public void payDebit(DebitCard d) {
// logic to pay via debit card
}
}
public class PaymentProcessor {
int type;
}
public class CreditCard extends PaymentProcessor {
public CreditCard() {
super.type = 1;
}
}
public class DebitCard extends PaymentProcessor {
public DebitCard() {
super.type = 2;
}
}
public class EcommerceApp {
public void paymentProcess(PaymentProcessor p) {
p.payment();
}
}
// Abstraction layer
Interface PaymentProcessor {
void payment();
}
public class CreditCard implements PaymentProcessor {
public void payment() {
// logic to pay via credit card
}
}
public class DebitCard implements PaymentProcessor {
public void payment() {
// logic to pay via debit card
}
}
public class UPI implements PaymentProcessor {
public void payment() {
// logic to pay via UPI
}
}
// Class representing an ecommerce application
public class EcommerceApp {
// Method to process payment using a PaymentProcessor
public void paymentProcess(PaymentProcessor p) {
p.payment(); // Calls the payment method of the PaymentProcessor
}
}
// Abstraction layer
interface PaymentProcessor {
void payment(); // Method to process payment
}
// Class representing payment via credit card
public class CreditCard implements PaymentProcessor {
// Method to process payment via credit card
public void payment() {
System.out.println("Payment via Credit Card"); // Example logic to pay via credit card
}
}
// Class representing payment via debit card
public class DebitCard implements PaymentProcessor {
// Method to process payment via debit card
public void payment() {
System.out.println("Payment via Debit Card"); // Example logic to pay via debit card
}
}
// Class representing payment via UPI
public class UPI implements PaymentProcessor {
// Method to process payment via UPI
public void payment() {
System.out.println("Payment via UPI"); // Example logic to pay via UPI
}
}
// Main class to test the EcommerceApp
public class Main {
public static void main(String[] args) {
EcommerceApp app = new EcommerceApp();
// Process payments
PaymentProcessor creditCard = new CreditCard();
PaymentProcessor debitCard = new DebitCard();
PaymentProcessor upi = new UPI();
app.paymentProcess(creditCard);
app.paymentProcess(debitCard);
app.paymentProcess(upi);
}
}
There is a relationship between the Open-Closed Principle (OCP)
and the Single Responsibility Principle (SRP)
in object-oriented design.
-
The Open-Closed Principle (OCP) states that a class should be open for extension but closed for modification. This means that the behavior of a class can be extended without modifying its source code. By adhering to OCP, new functionality can be added by creating new classes that extend the existing ones, rather than modifying the existing classes.
-
On the other hand, the Single Responsibility Principle (SRP) states that a class should have only one reason to change, meaning it should have only one job or responsibility. Each class should encapsulate one and only one aspect of functionality.
-
When a class adheres to OCP, it often indirectly promotes SRP. This is because by designing classes to be open for extension, you are also implicitly designing them to have a single responsibility. New functionality is added by creating new classes rather than modifying existing ones, ensuring that each class has a clear and specific purpose.
-
In essence, OCP encourages modular and extensible design, which tends to lead to classes that adhere to SRP by focusing on specific responsibilities. Therefore, while OCP and SRP are distinct principles, they often complement each other in promoting good object-oriented design practices.
1. 🧔🏻♂️ if & switch statement
2. 🧔🏻♂️ Voilet OCP
3. 🧔🏻♂️ Cyclomatic Complexity (Like a tree)
4. 🧔🏻♂️ Downcasting (Child hold the Parent)
5. 🧔🏻♂️ TypeCheckErrors (Like compile time errors)
6. 🧔🏻♂️ Anti Abstraction
import java.util.ArrayList;
import java.util.List;
/*
Order takes multiples responsibilities:
1. Order's Responsibilities: addItem, totalPrice
2. Not Order's Responsibility: pay
*/
public class Order {
private List<String> items;
private List<Integer> quantities;
private List<Double> prices;
private String status;
public Order() {
this.items = new ArrayList<>();
this.quantities = new ArrayList<>();
this.prices = new ArrayList<>();
this.status = "open";
}
public void addItem(String name, int quantity, double price) {
items.add(name);
quantities.add(quantity);
prices.add(price);
}
private double totalPrice() {
double total = 0;
for (int i = 0; i < prices.size(); i++) {
total += quantities.get(i) * prices.get(i);
}
return total;
}
// This Responsibility Violet the SRP & OCP
public void pay(String paymentType, String securityCode) {
if ("debit".equals(paymentType)) {
System.out.println("Processing debit payment type");
System.out.println("Verifying security code: " + securityCode);
status = "paid";
} else if ("credit".equals(paymentType)) {
System.out.println("Processing credit payment type");
System.out.println("Verifying security code: " + securityCode);
status = "paid";
} else {
throw new RuntimeException("Unknown payment type: " + paymentType);
}
}
public static void main(String[] args) {
Order order = new Order();
order.addItem("Keyboard", 1, 50);
order.addItem("SSD", 1, 150);
order.addItem("USB cable", 2, 5);
System.out.println(order.totalPrice());
order.pay("debit", "0372846");
order.pay("credit", "0372847");
}
}
-
The
Order
class is responsible for managing orders, including adding items, calculating total price, and processing payments. -
The
pay
method within theOrder
class handles payment processing, violating both the Single Responsibility Principle (SRP) and the Open-Closed Principle (OCP). -
Violation of SRP:
The
Order
class is responsible for order management but also for payment processing. This violates the SRP, as the class should have a single reason to change, and handling payment processing is a separate responsibility. -
Violation of OCP:
The
pay
method directly incorporates payment processing logic within the Order class. If new payment methods are introduced in the future, this method would need to be modified, violating the OCP, which states that classes should be open for extension but closed for modification.
import java.util.ArrayList;
import java.util.List;
// Encapsulation Order Class
public class Order {
private List<String> items;
private List<Integer> quantities;
private List<Double> prices;
private String status;
public Order() {
this.items = new ArrayList<>();
this.quantities = new ArrayList<>();
this.prices = new ArrayList<>();
this.status = "open";
}
public void addItem(String name, int quantity, double price) {
items.add(name);
quantities.add(quantity);
prices.add(price);
}
public double totalPrice() {
double total = 0;
for (int i = 0; i < prices.size(); i++) {
total += quantities.get(i) * prices.get(i);
}
return total;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}
// Interface: PaymentProcessor
interface PaymentProcessor {
void pay(Order order, String securityCode);
}
class DebitPaymentProcessor implements PaymentProcessor {
@override
public void pay(Order order, String securityCode) {
System.out.println("Processing debit payment type");
System.out.println("Verifying security code: " + securityCode);
order.setStatus("paid");
}
}
class CreditPaymentProcessor implements PaymentProcessor {
@override
public void pay(Order order, String securityCode) {
System.out.println("Processing credit payment type");
System.out.println("Verifying security code: " + securityCode);
order.setStatus("pending");
}
}
// New Payment Processor: Does not need to change for adding it (Not violet the OCP)
class UPIPaymentProcessor implements PaymentProcessor {
@override
public void pay(Order order, String securityCode) {
System.out.println("Processing UPI payment type");
System.out.println("Verifying security code: " + securityCode);
order.setStatus("failed");
}
}
class Main {
public static void main(String[] args) {
Order order = new Order();
order.addItem("Keyboard", 1, 50);
order.addItem("SSD", 1, 150);
order.addItem("USB cable", 2, 5);
System.out.println(order.totalPrice());
// This is upcasting (parent hold the child)
PaymentProcessor debitCard = new DebitPaymentProcessor();
PaymentProcessor creditorCard = new CreditPaymentProcessor();
PaymentProcessor UPI = new UPIPaymentProcessor();
debitCard.pay(order, "0372846");
creditorCard.pay(order, "0372847");
UPI.pay(order, "0372848");
}
}
-
The
Order
class is responsible for managing order details such as items, quantities, prices, and status. It encapsulates these responsibilities and does not handle payment processing, adhering to the (SRP
). -
Payment processing is delegated to classes implementing the
PaymentProcessor
interface. Each payment processor is responsible for processing payments using a specific method (debit card, credit card, etc.). -
Adding a new payment processor, such as the
UPIPaymentProcessor
, does not require modification to the existing code in theOrder
class, adhering to the (OCP
).