Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
Mayer Bertrand
Program behavior (functions, classes, modules, etc.) should be changed without modifying existing code. Instead, new code should be added to extend the program.
The code should be written in a way that makes it easy to add new features without changing already working parts. The goal is to grow the program by adding new elements, not by editing existing ones.
To follow this rule, it’s important to use abstractions.
Let’s say we have two classes, each representing a different type of customer:
public class RegularCustomer {
final double discountLevel = 0.95;
// method returns unit price with discount
public double calculateDiscountedPrice (double basePrice) {
return basePrice * discountLevel ;
}
}
public class PremiumCustomer {
final double discountLevel = 0.8;
// method returns unit price with discount
public double calculateDiscountedPrice (double basePrice) {
return basePrice * discountLevel ;
}
}
We also have the PriceCalculator class. It calculates the final price depending on the customer type and quantity:
public class PriceCalculator {
// method returns final price depending on the type of base price, customer type and quantity
public double calculateFinalPrice (double basePrice, int quantity, String customerType) {
switch (customerType) {
case "REGULAR":
RegularCustomer regularCustomer = new RegularCustomer();
return quantity * regularCustomer.calculateDiscountedPrice(basePrice);
case "PREMIUM":
PremiumCustomer premiumCustomer = new PremiumCustomer();
return quantity * premiumCustomer.calculateDiscountedPrice(basePrice);
default:
return quantity * basePrice;
}
}
}
public class Runner {
public static void main (String [] args) {
System.out.println(PriceCalculator.calculateFinalPrice(100, 100, "PREMIUM"));
}
This code works, but there is a problem:
to add a new type of customer, one must change the code in the PriceCalculator class:
public class VipCustomer {
final double discountLevel = 0.5;
// method returns unit price with discount
public double calculateDiscountedPrice (double basePrice) {
return basePrice * discountLevel ;
}
}
public class PriceCalculator {
// method returns final price depending on the type of base price, customer type and quantity
public double calculateFinalPrice (double basePrice, int quantity, String customerType) {
switch (customerType) {
case "REGULAR":
RegularCustomer regularCustomer = new RegularCustomer();
return quantity * regularCustomer.calculateDiscountedPrice(basePrice);
case "PREMIUM":
PremiumCustomer premiumCustomer = new PremiumCustomer();
return quantity * premiumCustomer.calculateDiscountedPrice(basePrice);
case "VIP":
VipCustomer vipCustomer = new VipCustomer();
return quantity * vipCustomer.calculateDiscountedPrice(basePrice);
default:
return quantity * basePrice;
}
}
}
We are breaking the Open closed principle — we are modifying working code just to add something new.
Improved implementation
Let’s add an interface.
public interface Customer {
public double calculateDiscountedPrice (double basePrice) {
return basePrice * discountLevel ;
}
}
and implement it in all cusotmer classes:
public class RegularCustomer implements Customer{
final double discountLevel = 0.95;
@Override
public double calculateDiscountedPrice (double basePrice) {
return basePrice * discountLevel ;
}
}
public class PremiumCustomer implements Customer{
final double discountLevel = 0.8;
@Override
public double calculateDiscountedPrice (double basePrice) {
return basePrice * discountLevel ;
}
}
public class VipCustomer implements Customer{
final double discountLevel = 0.5;
@Override
public double calculateDiscountedPrice (double basePrice) {
return basePrice * discountLevel ;
}
}
Now the PriceCalculator class is much simpler:
public class PriceCalculator {
public double calculateFinalPrice (double basePrice, int quantity, Customer customer) {
return quantity * customer.calculateDiscountedPrice(basePrice);
}
}
We don’t need to check the customer type anymore. We just pass in the correct object, and Java calls the correct method.
public class Runner {
public static void main (String [] args) {
System.out.println(PriceCalculator.calculateFinalPrice(100, 100, new RegularCustomer()));
}
Why to use the Open-closed principle
Code written with the Open-closed principle is easier to grow and change. By using an interface that contains common behaviors , we can:
Add a new customer type by just creating a new class that implements the interface and without need to touch the old code, which is already tested and working.