What is Open close software design principle?
- Benjamin disraeli said ‘Change is inevitable, Change is constant’.
- Change is constant in case of software development also.
- Software development mostly follows an iterative life cycle.
- Requirements are changing everyday, either a change in existing functionalities or introduction of new functionalities.
- Open-closed principle provides guidelines to the software designer/developer, how the software should handle the change (or requirements)
- Open-Closed principle says that “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.
Even though Open-Closed principle (OCP) sounds simple, but OCP might not be as easy to practice (always). Let us consider the case of a class (a class is a software entity). How easy will it be to design a class for a functionality which will be extendable (‘open’) for the future needs? The important point here is the ‘future needs’. When we don’t know, what customer can request from software in future, how will we design a class, modules, functions etc. meeting those needs? This is the reason OCP is quite difficult to practice & need exhaustive thought process.
What would be good design solution for such problems? While designing a software, care should be taken to ensure the software can extended easily. During the design phase all the known use cases for the class should be noted down. Once we have the use cases, design the interface(s) for all use cases. Creating an abstract class (or interface) is a way of ensuring that our class is extendable for unforeseen requirements. Keep in mind that we do not to violates SRP, while doing so. It would be always better to logically break down the classes if we are trying to do more than one job.
Modifying the already developed or existing code has disadvantage also. Modifying the existing code means re-testing the existing functionality as modification are prone to induce new defects ,in the already tested code. Modifications also mean a change to the existing binaries.
Open close principle with the real world examples:
Open close software design principle example 1:
Chartered accountant approached us & asked us to create a software which is capable of calculating the tax. We will get the input details of tax payer & we just need to calculate the tax.
- CA will input the data of taxpayer.
- We will get information like name, gender or salary details of male & female taxpayer.
- In our software application, we will calculate the tax.
- We will use flat tax calculations for simplicity.
With are given the required information & we come up with our design. We have TaxCalculator class responsible for calculating the tax. We have assumed 25% flat tax for male taxpayer & 20% flat tax for female taxpayer (we can make tax percentage as configurable in property etc., but for the sake of simplicity, we will ignored it.)
The module was working fine, untill next financial year, government change the tax structure & government wants to provide tax rebate to senior citizen females. The client (CA) approached us & want us to add the support for senior citizen female (as taxpayer party). As we always thought, we have good design, we introduce one more case for senior citizen female. We have to test our complete application, as we have changed the existing functionality & we are ready with our release for current financial year.
Program: Calculate the tax for male, female & senior citizen females.
1.) Individual Class:
- Individual class containing personal information about the taxpayers.
package org.learn.without.ocp; public class Individual { private Gender gender; private double salary; private int age; private String name; public Individual(double salary, int age, String name, Gender gender) { this.gender = gender; this.salary = salary; this.age = age; this.name = name; } public Gender getGender() { return gender; } public double getSalary() { return salary; } public int getAge() { return age; } public String getName() { return name; } }
2.) TaxCalculator Class:
- TaxCalculator class is used to calculate the tax of each individual taxpayer.
package org.learn.without.ocp; public class TaxCalculator { /* * Tax slab female = 20% on taxable income * Tax slab male = 25% on taxable * income Tax slab senior = 18% on taxable income */ public double calculateTax(Individual individual) { double tax = 0; switch (individual.getGender()) { case MALE: tax = 0.25 * individual.getSalary(); break; case FEMALE: tax = 0.20 * individual.getSalary(); break; case SR_CITIZEN_FEMALE: tax = 0.17 * individual.getSalary(); break; default: //Handler exception condition System.out.println("Unknow choice"); tax = -1; break; } return tax; } }
3.) Gender enum:
- Enumeration containing individuals, taxed for current financial year.
package org.learn.without.ocp; enum Gender { MALE, FEMALE, SR_CITIZEN_FEMALE }
4.) CharteredAccountant class:
- CharteredAccountant is client of our application & used to calculate the tax of individuals taxpayers.
package org.learn.without.ocp; public class CharteredAccountant { public static void main(String[] args) { Individual sharon = new Individual(1000, 25, "Sharon", Gender.FEMALE); Individual nicola = new Individual(1000, 45, "Nicola", Gender.FEMALE); Individual gretzky = new Individual(1000, 40, "Gretzky", Gender.MALE); Individual howe = new Individual(1000, 65, "Howe", Gender.MALE); Individual kim = new Individual(1000, 61, "Kim", Gender.SR_CITIZEN_FEMALE); TaxCalculator taxCalculator = new TaxCalculator(); System.out.println("1. Tax liability of Sharon is: " + taxCalculator.calculateTax(sharon)); System.out.println("2. Tax liability of Nicola is: " + taxCalculator.calculateTax(nicola)); System.out.println("3. Tax liability of Gretzky is: " + taxCalculator.calculateTax(gretzky)); System.out.println("4. Tax liability of Howe is: " + taxCalculator.calculateTax(howe)); System.out.println("5. Tax liability of Kim is: " + taxCalculator.calculateTax(kim)); } }
Disadvantage of existing software design:
With introduction of new individual taxpayer (like sr citizen female), we need to modify the existing code (adding new switch case in TaxCalculator class). Our design is not open for extension, We need to relook into our design again, as there is scope of improvement.
So, after brainstorming about the current design, we have modified our current design and come up with design shown in Fig 3. We have introduced the classes like Male, Female, SrCitizen(F) by extending abstract class Individual. Now, with new design, we can handle new requirement like to support Senior citizen male taxpayer without modifying the existing code. So our design adheres to OCP principle (open for extension & close for modification).
Program: Application code adhering to open close design principle.
1.) Individual Class:
- Individual Class containing personal information about the taxpayer(s).
package org.learn.ocp; public abstract class Individual { protected double salary; protected int age; protected String name; protected Individual(double salary, int age, String name) { this.salary = salary; this.age = age; this.name = name; } abstract double calculateTax(); }
2.) TaxCalculator Class:
- TaxCalculator class is used to calculate the tax of each individual(s).
package org.learn.ocp; public class TaxCalculator { /* * Tax slab female = 20% on taxable income * Tax slab male = 25% on taxable income * Tax slab senior citizen = 17% on taxable income */ public double calculateTax(Individual individual) { return individual.calculateTax(); } }
3.) Male class:
- Male class responsible for calculating tax for male individuals.
package org.learn.ocp; public class Male extends Individual { public Male(double salary, int age, String name) { super(salary, age, name); } /* Tax slab male = 25% on taxable */ public double calculateTax() { return 0.25 * this.salary; } }
4.) Female class:
- Female class responsible for calculating tax for female individuals.
package org.learn.ocp; public class Male extends Individual { public Male(double salary, int age, String name) { super(salary, age, name); } /* Tax slab male = 25% on taxable */ public double calculateTax() { return 0.25 * this.salary; } }
5.) SrCitizenFemale class:
- SrCitizenFemale class is responsible for calculating tax for sr. citizen female individuals.
package org.learn.ocp; public class SrCitizenFemale extends Individual { public SrCitizenFemale(double salary, int age, String name) { super(salary, age, name); } /* * Income Tax slab senior citizen female = 17% on taxable income */ public double calculateTax() { return 0.17 * this.salary; } }
6.) CharteredAccountant class:
- CharteredAccountant is client of our application & used to calculate the tax of individuals taxpayers.
package org.learn.ocp; public class CharteredAccountant { public static void main(String[] args) { Individual sharon = new Female(1000, 25, "Sharon"); Individual nicola = new Female(1000, 45, "Nicola"); Individual gretzky = new Male(1000, 40, "Gretzky"); Individual howe = new Male(1000, 65, "Howe"); Individual kim = new SrCitizenFemale(1000, 61, "Kim"); TaxCalculator taxCalculator = new TaxCalculator(); System.out.println("1. Tax liability of Sharon is: " + taxCalculator.calculateTax(sharon)); System.out.println("2. Tax liability of Nicola is: " + taxCalculator.calculateTax(nicola)); System.out.println("3. Tax liability of Gretzky is: " + taxCalculator.calculateTax(gretzky)); System.out.println("4. Tax liability of Howe is: " + taxCalculator.calculateTax(howe)); System.out.println("5. Tax liability of Kim is: " + taxCalculator.calculateTax(kim)); } }
We can further improve current design but we can ignore it as of now. We would like to convey the thought process of open close design principle.
Open close software design principle example 2:
Suppose our customer chose to use FireBird database for one of embedded projects and our task is to design a database handler for FireBird database. so we come up with FireBird database handler as shown in Fig 4.
Everything looks good, works fine and our customer is happy. For whatever reason (say size of the database and some other feature supported by the new database), a few months later customer came back and decided to use another database engine let’s say SQlite. Now our whole design is at risk. Our database handler class is not at all open for extension and we expects lot of modifications, both in our database handler class and also every application which uses the database handler.
Clearly, we can able to fulfill the request of the customer, but we needs the considerable effort to entertain the requests and we need to go through the complete SDLC life cycle (which has a cost involved). Our design needs to be revisited again, so that whenever we need to support new database, there should not be any change with the existing functionality. Moreover, code maintainability is also a major concern. So, we redesign our database handler on same line with example 1 and our application design will look like as shown in Fig 6.
1.) DatabaseHandler interface:
- Interface defining the features of database handler(s).
package org.learn.ocp.db; public interface DatabaseHandler { boolean open(); boolean close(); int create(Object record); Object retrieve(int id); int retrieve(Object record); int delete(int id); }
2.) FireBirdDatabaseHandler Class:
- FireBirdDatabaseHandler class implementing DatabaseHandler interface.
package org.learn.ocp.db; public class FireBirdDatabaseHandler implements DatabaseHandler { public boolean open() { // Open database connection of FireBird database return false; } public boolean close() { // close database connection of FireBird database return false; } public int create(Object record) { // ... return 0; } public Object retrieve(int id) { // ... return null; } public int retrieve(Object record) { // ... return 0; } public int delete(int id) { // ... return 0; } }
3.) SQLiteDatabaseHandler Class:
- SQLiteDatabaseHandler class implementing DatabaseHandler interface.
package org.learn.ocp.db; public class SQLiteDatabaseHandler implements DatabaseHandler { public boolean open() { // Open database connection of SQLite database return false; } public boolean close() { // close database connection of SQLite database return false; } public int create(Object record) { // ... return 0; } public Object retrieve(int id) { // ... return null; } public int retrieve(Object record) { // ... return 0; } public int delete(int id) { // ... return 0; } }
Now, with improved design we can able to support new databases like MySQL without changes in the existing code. Our existing functionality will remain intact. We just need to extend the DatabaseHandler interface to support new database. Our design will be open for extension & close for modification.
Download Code – Open close design principle (examples)