A very important consideration when building software is to write code to produce applications which can adapt when new needs or requirements change (.. which for sure they will ) without affecting the rest of the already implemented functionality. Usually, changes in existing code should be minimized since it is assumed that the existing code is already unit tested and that probably it took a long time to get it correct and bug free.
Definition
The open closed principle states that Classes should be open for extension, but closed for modification. The goal of this principle is to allow classes to be easily extended without modifying existing code. Therefore, obtaining designs that are resilient to change and flexible enough to take a new functionality to meet changing requirements.
Example without Open Closed Principle
(Click on the image to expand it)
The Account class handles different transactions that a customer might execute depending on his type of account. For example, saving accounts holders allow only withdrawals until the balance is €0.00 but giro accounts provide a credit of €500.00. So the developer writes some code that determines the appropriate type of account and then goes about doing the withdrawals:
public Account (String name, String type)
{
/* This code is NOT closed for modification
* if new Accounts are introduced or removed
* we have to get into this code and modify it
*
* Bad Times!! :( */
this.type = type;
this.name = name;
if(type.equals("Giro Account"))
withdrawGiroAccount(amount);
else if(type.equals("Saving Account"))
withdrawSavingAccount(amount);
else if(type.equals("Personal Account"))
withdrawPersonalAccount(amount);
}
Example with Open Closed Principle
The class diagram below shows how this application will be design.
(Click on the image to expand it)
First, we have a client class which will be in charge to create accounts. For this example is just a simple Java class which makes instances of giro, saving or any other type added in the future.
public class AccountTest
{
public static void main(String args[])
{
AccountType giroAccount = new GiroAccount();
AccountType savingAccount = new SavingAccount();
Account account1 = new Account("Cipriano's Account", giroAccount);
Account account2 = new Account("Desiree's Account", savingAccount);
System.out.println("\nTransactions for Cipriano's account");
System.out.println(account1.deposit(1000));
System.out.println(account1.withdraw(4000));
System.out.println(account1.withdraw(3000));
System.out.println(account1.withdraw(1000));
System.out.println("\nTransactions for Desiree's account");
System.out.println(account2.deposit(3000));
System.out.println(account2.withdraw(2000));
System.out.println(account2.withdraw(2000)); }
}
}
public class Account {
private AccountType account;
private float amount;
public Account (String name, AccountType type) {
this.account = type;
this.account.setName(name);
}
public String deposit(float amount){
return account.deposit(amount);
}
public String withdraw(float amount){
return account.withdraw(amount);
}
public void setAmount(float amount){
this.amount = amount;
}
public float getAmount(){
return account.getAmount();
}
public String toString(){
return account.getName() + " : " +
account.getAmount() + "€";
}
}
public abstract class AccountType
{
private float amount = 0;
private String name;
public AccountType(){}
public AccountType(String name){
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public String getName(){
return this.name;
}
public void setAmount(float amount) {
this.amount = amount;
}
public float getAmount(){
return amount;
}
public String deposit(float amount){
float current = getAmount();
setAmount(current + amount);
String result = "deposited " + amount + "€" ;
result = result + " for " + getName();
result = result + "\ncurrent balance is: " +
getAmount() + "€";
return result;
}
public abstract String withdraw (float amount);
}
Finally we add our concrete account type classes with their respective withdraw implementations.
public class GiroAccount extends AccountType {
public static final float OVER_DRAFT_LIMIT = 5000f;
public GiroAccount() {
}
public GiroAccount(String name) {
super(name);
}
public String withdraw(float amount) {
float current = getAmount();
float withdrawal;
if (current - amount >= -OVER_DRAFT_LIMIT)
withdrawal = amount;
else
withdrawal = OVER_DRAFT_LIMIT + current;
setAmount(current - withdrawal);
String result = "withdrawn " + withdrawal + "€";
result = result + " from " + getName();
result = result + "\ncurrent balance is: " + getAmount() + "€";
return result;
}
@Override
public String toString() {
return "GiroAccount";
}
}
public class SavingAccount extends AccountType {Conclusion
public SavingAccount(){}
public SavingAccount(String name){
super(name);
}
@Override
public String withdraw(float amount) {
float current = this.getAmount();
float withdrawal = 0;
String result="";
if(current - amount >= 0){
withdrawal = amount;
setAmount(current - withdrawal);
result = "withdrawn " + withdrawal + "€" ;
result = result + " from " + getName();
result = result + "\ncurrent balance is: " +
getAmount() + "€";
}
else
result = "Insuficient money available";
return result;
}
@Override
public String toString(){
return "SavingAccount";
}
}
Be careful when choosing the areas of code that need to be extended; making a flexible design sometimes involves additional complexities and effort. Applying the Open Closed Principle everywhere can lead to complex, hard to understand code.