Open Closed Design Principle in Java
Last Updated :
17 Jan, 2023
Open Closed Design Principle is one of the most important design principles in the world of software development. It’s part of a group of 5 design principles commonly known by the acronym “SOLID”
S - Single Responsibility Principle
O - Open Closed Principle
L - Liskov Substitution Principle
I - Interface Segregation
D - Dependency Inversion
As per Robert C Martin, this principle states that software entities (classes, modules, functions) should be open for extensions, closed for modifications. This means that a class should be flexible enough to accommodate changes when business requirements change without breaking the existing code. Thus, the dependent classes do not have to change. This minimizes the testing effort and we only need to focus on new code changes.
Methods:
There are basically two primary ways of implementing the interfaces:
- Using inheritance
- Using Interfaces and abstract methods
Method 1: Using inheritance
One way to follow this principle is using inheritance. We have a superclass that is closed for modifications, and it is baselined, and part of a jar file. Anybody who wants to modify the superclass behavior can subclass it, extend it and add new features. This ensures that superclass behavior remains intact and the clients to the superclass do not have to make code changes. However, inheritance introduces tight coupling between the superclass and sub–class implementations. Suppose there is a defect in the superclass or there is an upgrade in the superclass version which introduces code changes, then the subclass also needs to be modified.
Example: Suppose Jane starts a bakery shop and decided to keep it simple and scale as in accordance with her sales.
Java
import java.io.*;
class Cake {
private int size;
private float weight;
public Cake( int size, float weight)
{
this .size = size;
this .weight = weight;
}
public void bake()
{
System.out.println(
"Baking cake with base as vanilla" );
System.out.println( "Size is " + this .size
+ " inches and weight is "
+ this .weight + " kg." );
}
}
class PineappleCake extends Cake {
private int size;
private float weight;
public PineappleCake( int size, float weight)
{
super (size, weight);
this .size = size;
this .weight = weight;
}
private void decorateCake()
{
System.out.println( "Decorating cake" );
System.out.println( "Adding pineapple pieces" );
}
private void addCream()
{
System.out.println( "Adding white cream" );
}
public void bake()
{
super .bake();
addCream();
decorateCake();
System.out.println( "Pineapple cake - " + this .size
+ " inches is ready" );
}
}
class ChocolateCake extends Cake {
private int size;
private float weight;
public ChocolateCake( int size, float weight)
{
super (size, weight);
this .size = size;
this .weight = weight;
}
private void decorateCake()
{
System.out.println( "Decorating cake" );
System.out.println( "Adding chocolate chips" );
}
private void addCream()
{
System.out.println( "Adding chocolate cream" );
}
public void bake()
{
super .bake();
addCream();
decorateCake();
System.out.println( "Chocolate cake - " + this .size
+ " inches is ready" );
}
}
class GFG {
public static void main(String[] args)
{
PineappleCake pineappleCake
= new PineappleCake( 7 , 3 );
pineappleCake.bake();
ChocolateCake chocolateCake
= new ChocolateCake( 5 , 2 );
chocolateCake.bake();
}
}
|
Output
Baking cake with base as vanilla
Size is 7 inches and weight is 3.0 kg.
Adding white cream
Decorating cake
Adding pineapple pieces
Pineapple cake - 7 inches is ready
Baking cake with base as vanilla
Size is 5 inches and weight is 2.0 kg.
Adding chocolate cream
Decorating cake
Adding chocolate chips
Chocolate cake - 5 inches is ready
Output explanation: ‘Cake’ class is the base class. The base for every cake has a vanilla flavor. We have two specializations i.e Pineapple and Chocolate flavor cake. These classes use the Cake’s bake() method to make a vanilla base cake and then add cream and decorate the cake based on the flavor.
Note: After few days, Jane’s bakery shop sales are high. So she decides to bake cakes with different bases to offer more varieties to her customers. With these new requirements, we need to change the ‘Cake’ class constructor, so do the changes in the above code will be reflected. The code changes are as below. However, we need to modify the subclasses to accept a 3rd parameter – flavor for the code to work.
Example:
Java
import java.io.*;
class Cake {
private int size;
private float weight;
private String flavor;
public Cake( int size, float weight, String flavor)
{
this .size = size;
this .weight = weight;
this .flavor = flavor;
}
public void bake()
{
System.out.println( "Baking cake with base as "
+ this .flavor);
System.out.println( "Size is " + this .size
+ " inches and weight is "
+ this .weight + " kg." );
}
}
class PineappleCake extends Cake {
private int size;
private float weight;
private String flavor;
public PineappleCake( int size, float weight,
String flavor)
{
super (size, weight, flavor);
this .size = size;
this .weight = weight;
this .flavor = flavor;
}
private void decorateCake()
{
System.out.println( "Decorating cake" );
System.out.println( "Adding pineapple pieces" );
}
private void addCream()
{
System.out.println( "Adding white cream" );
}
public void bake()
{
super .bake();
addCream();
decorateCake();
System.out.println( "Pineapple cake - " + this .size
+ " inches is ready" );
}
}
class ChocolateCake extends Cake {
private int size;
private float weight;
private String flavor;
public ChocolateCake( int size, float weight,
String flavor)
{
super (size, weight, flavor);
this .size = size;
this .weight = weight;
this .flavor = flavor;
}
private void decorateCake()
{
System.out.println( "Decorating cake" );
System.out.println( "Adding chocolate chips" );
}
private void addCream()
{
System.out.println( "Adding chocolate cream" );
}
public void bake()
{
super .bake();
addCream();
decorateCake();
System.out.println( "Chocolate cake - " + this .size
+ " inches is ready" );
}
}
class GFG {
public static void main(String[] args)
{
PineappleCake pineappleCake
= new PineappleCake( 7 , 3 , "vanilla" );
pineappleCake.bake();
ChocolateCake chocolateCake
= new ChocolateCake( 5 , 2 , "chocolate" );
chocolateCake.bake();
}
}
|
Output
Baking cake with base as vanilla
Size is 7 inches and weight is 3.0 kg.
Adding white cream
Decorating cake
Adding pineapple pieces
Pineapple cake - 7 inches is ready
Baking cake with base as chocolate
Size is 5 inches and weight is 2.0 kg.
Adding chocolate cream
Decorating cake
Adding chocolate chips
Chocolate cake - 5 inches is ready
Note: Thus there is tight coupling between the base class that is, ‘Cake’ class, and it’s subclasses. To avoid tight coupling we go with the interface approach.
Method 2: Using interfaces and abstract methods
- We define an interface with a set of methods.
- The interface defines a contract, and it is closed for modifications. We could have different implementations of the interface as per the business requirements. This ensures there is loose coupling between classes. The implementation classes are independent of each other.
Implementation: We refactor the previous example for better understanding. We have a Cake interface that has different steps for baking a cake. Pineapple and Chocolate Cake class implement this interface defining their own bread base, cream, and decoration.
Example:
Java
import java.io.*;
interface Cake {
public void bake();
public void addCream();
public void decorateCake();
}
class ChocolateCake implements Cake {
private String base;
private int size;
private float weight;
public ChocolateCake(String base, int size,
float weight)
{
this .base = base;
this .size = size;
this .weight = weight;
}
public void addCream()
{
System.out.println( "Adding chocolate cream" );
}
public void bake()
{
System.out.println( "Baking cake with base as "
+ this .base);
addCream();
decorateCake();
System.out.println( "Chocolate cake with "
+ this .size
+ " inches and weight:"
+ this .weight + " kg is ready" );
}
public void decorateCake()
{
System.out.println(
"Cake decoration with choco chips" );
}
}
class PineappleCake implements Cake {
private String base;
private int size;
private float weight;
public PineappleCake(String base, int size,
float weight)
{
this .base = base;
this .size = size;
this .weight = weight;
}
public void addCream()
{
System.out.println( "Adding white cream" );
}
public void decorateCake()
{
System.out.println(
"Cake decoration with pineapple pieces" );
}
}
public void bake()
{
System.out.println( "Baking cake with base as "
+ this .base);
addCream();
decorateCake();
System.out.println( "Pineapple cake with " + this .size
+ " inches and weight:" + this .weight
+ " kg is ready" );
}
class GFG {
public static void main(String[] args)
{
Cake pineappleCake
= new PineappleCake( "vanilla" , 7 , 3 );
pineappleCake.bake();
Cake chocolateCake
= new ChocolateCake( "chocolate" , 5 , 2 );
chocolateCake.bake();
}
}
|
Output
Baking cake with base as vanilla
Adding white cream
Cake decoration with pineapple pieces
Pineapple cake with 7 inches and weight:3.0 kg is ready
Baking cake with base as chocolate
Adding chocolate cream
Cake decoration with choco chips
Chocolate cake with 5 inches and weight:2.0 kg is ready
Output explanation: The interface provides an abstraction layer and promotes loose coupling. In the future, Jane can introduce other flavors of cake with different bases, creams, and decorations.
Share your thoughts in the comments
Please Login to comment...