Open close principle

  • Bertrand Meyer coined the term open close principle in his 1988 book Object Oriented Software Construction.
  • Software entities like classes, modules and functions should be "open for extension but closed for modifications".
  • It is a generic principle. You can consider it when writing your classes to make sure that when you need to extend their behaviour you don't have to change the class but to extend it.
  • When referring to the classes Open Close principle can be ensured by the use of Abstract classes and/ or Interfaces and concrete classes implementing their behaviour. This will enforce having concrete classes implementing Abstract classes/ Interfaces instead of changing them. 
  • Some particular cases where this principle is used are Template Design Pattern, Strategy design pattern.

We want to draw different kind of images. For this, We wrote a generic class ImageEditer which can draw shapes. See the below code snippet.

package javawithgaurav.openclose;

/**
 * 
 * @author Gaurav Rai Mazra
 * <a href="www.javawithgaurav.blogspot.in">Click here to view more</a>
 */
//Abstract class different type of Shape
abstract class Shape {
    public static final int TYPE_RECTANGLE = 1;
    public static final int TYPE_SQUARE = 2;
    
    private int type;
    
    Shape (int type) {
        
    }
    
    public int getType() {
        return type;
    }
}

//Rectangle
class Rectangle extends Shape {
    Rectangle () {
        super(TYPE_RECTANGLE);
    }
}

//Square
class Square extends Shape {
    Square () {
        super(TYPE_SQUARE);
    }
}
package javawithgaurav.openclose;

/**
 * 
 * @author Gaurav Rai Mazra
 * <a href="www.javawithgaurav.blogspot.in">Click here to view more</a>
 */

public class ImageEditor
{
    public void drawShape (Shape s) {
        final int shapeType = s.getType();
        //Based on shape type draw shapes code
        if (shapeType == Shape.TYPE_RECTANGLE) {
           drawRectangle(s); 
        }
        else if (shapeType == Shape.TYPE_SQUARE) {
            drawSquare(s);
        }
    }
    
    private void drawRectangle(Shape s) {
        // Logic to draw Rectangle
    }
    
    private void drawSquare(Shape s) {
        // logic to draw Square
    }
}

By looking into above code snippet, we see no problem. We have Shape as abstract class and then its concrete implementations like Rectangle, Square. And, we have ImageEditor class who have only exposing drawShape() to draw shape of any type and it is hiding method to draw specific image like Rectangle, Square etc. Atleast, we are using abstraction, encapsulation, hiding etc. features of OOPs(pun intended).

Problem with above structure

In case we are required to add new shape say Polygon then what?. Will our ImageEditor be able to draw it? Answer is No.

We need to change ImageEditor class to support this behavior. It means we need to modify it??? If we modify it then we need to unit test it. One change can raise some other issue in class.

So, how we can ensure to close ImageEditor for modifications?

Good approach

We will change it according to OCP and also SRP. Let's do it in the code snippet below.

package javawithgaurav.openclose;

/**
 * 
 * @author Gaurav Rai Mazra
 * <a href="www.javawithgaurav.blogspot.in">Click here to view more</a>
 */
//Abstract class different type of Shape
abstract class Shape {
  private String name;

  Shape (String name) {
      this.name = name;
  }
  
  public String getName() {
      return name;
  }
  
  abstract void draw();
}

//Rectangle
class Rectangle extends Shape {
  Rectangle () {
      super("Rectangle");
  }
  
  @Override
  public void draw() {
      //logic to draw RECTANGLE
  }
}

//Square
class Square extends Shape {
  Square () {
      super("Square");
  }
  
  @Override
  public void draw() {
      //logic to draw SQUARE
  }
}
package javawithgaurav.openclose;

/**
 * 
 * @author Gaurav Rai Mazra
 * <a href="www.javawithgaurav.blogspot.in">Click here to view more</a>
 */

public class ImageEditor
{
    public void drawShape (Shape s) {
        s.draw();
    }
    // other methods related to editing image goes here
}

We declared the responsibility to draw in Shape but also make it out abstract so that every concrete class should define how to draw itself.

In the ImageEditor class, the drawShape() delegate the call to draw to Shape. Now, with this change we can draw any kind of shape and in future if it has to draw new shape then we don't have to modify it.

Single Responsibility Principle

  • Single responsibility principle was introduced by Tom DeMarco in his book "Structured Analysis and Systems Specification, 1979". Robert Martin reinterpreted the concept and defined the responsibility as a reason to change.
  • A class should have only one reason to change.
  • In this context, responsibility is considered as reason to change. This principle states that if we have two reasons to change for a class, we have to split the functionality in two classes. Each class will handle only one responsibility and on future if we need to make one change we are going to make it in the class which handles it. When we need to make a change in the class having more responsibilities the change might affect the other functionality of the classes.

Let's see this by example.

package com.gauravbytes.srp.parser;

import java.io.File;

class FileParser {
 
 public void parseFile(File file) {
  // parse file logic for xml, csv, json data in files
  if (isValidFile(file, FileType.CSV, FileType.XML, FileType.JSON)) {
   //parsing logic starts
  }
 }
 
 private boolean isValidFile(File file, FileType... types) {
  if(file == null || types == null || types.length == 0)
   return false;
  
  String fileName = file.getName().toLowerCase();
  for (FileType type : types) {
   if (fileName.endsWith(type.getExtension()))
    return true;
  }
  
  return false;
 }
 
}
package com.gauravbytes.srp.parser;

enum FileType { 
 
 CSV(".csv"), XML(".xml"), JSON(".json"), PDF(".pdf"), RICHTEXT(".rtf"), TXT(".txt");
 
 private String extension;
 
 private FileType (String extension) {
  this.extension = extension;
 }
 
 public String getExtension() {
  return this.extension;
 }
}

FileParser class parses the csv, xml and json file and generates the data for it. It also have method to first validate the file which checks if the file is valid. This fileparser is doing more than one stuff.

- It is validating the files.

- Currently, It is parsing the csv, json, xml files.

In future, if we want to text file, rtf file and so on then we need to change this class. Also if we want to change how we validate the file, then also we need to change this file. This leads to the problem like unit testing the class again because one change can affect the existing functionality and so on.

In the above example, if I want to change the strategy to parse xml file. lets say previously it was using dom parser to parse xml but i want to use SAX parser for parsing due to change in requirement or due to bigger size of file. Then i need to change this class again. Same can happen with json parsing or csv parsing.

We can avoid multiple reasons for change in the fileparser class by introducing separate class for validating the file and also modify the structure of the class and introduce new classes for which have specific responsibility to parse specific type of class.

The new and improved structure for the class will look something like this.

1. FileParser.java

In this class, we removed method to validate files which was present in earlier file and placed it in FileValidationUtils. We removed the extra responsibility to validate files from this FileParser.

We also removed the code which actually parses the file based on whatever file it is like xml, csv or json. Now, In case we need to change our xml reading/parsing logic that that can be changed without changing FileParser class. The solution here to divide the responsibility of parsing specific type to their respective classes and then those classes may have other specific method to parse those files.

In our solution, we created interface Parser and then created the specific classes to handle XML, CSV and JSON parsing like CSVFileParser, JsonFileParser and XmlFileParser.

We used composition relation in FileParser and give setter to change parsing at any time in this FileParser but the functionality will never change in FileParser.

package com.gauravbytes.good.srp.parser;

import java.io.File;

/**
 * @author Gaurav Rai Mazra
 * 
 */
public class FileParser {
 private Parser parser;
 
 public FileParser(Parser parser) {
  this.parser = parser;
 }
 
 public void setParser(Parser parser) {
  this.parser = parser;
 }
 
 public void parseFile(File file) {
  if (FileValidationUtils.isValidFile(file, parser.getFileType())) {
   parser.parse(file);
  }
 }
}
package com.gauravbytes.good.srp.parser;

import java.io.File;

/**
 * @author Gaurav Rai Mazra
 * 
 */
public class FileValidationUtils {
 
 private FileValidationUtils() {
  
 }
 
 public static boolean isValidFile (File file, FileType... types) {
  if (file == null || types == null || types.length == 0)
   return false;
  
  String fileName = file.getName().toLowerCase();
  for (FileType type : types) {
   if (fileName.endsWith(type.getExtension()))
    return true;
  }
  
  return false;
 }
 
 public static boolean isValidFile (File file, FileType type) {
  if (file == null || type == null)
   return false;
  
  String fileName = file.getName().toLowerCase();
  if (fileName.endsWith(type.getExtension()))
   return true;
  
  return false;
 }
}
package com.gauravbytes.good.srp.parser;

/**
 * @author Gaurav Rai Mazra
 * 
 */
public enum FileType { 
 
 CSV(".csv"), XML(".xml"), JSON(".json"), PDF(".pdf"), RICHTEXT(".rtf"), TXT(".txt");
 
 private String extension;
 
 private FileType (String extension) {
  this.extension = extension;
 }
 
 public String getExtension() {
  return this.extension;
 }
}
package com.gauravbytes.good.srp.parser;

import java.io.File;

/**
 * @author Gaurav Rai Mazra
 * 
 */
public interface Parser {
 //method to parse file
 public void parse(File file);
 
 // return filetype to validate
 public FileType getFileType();
}
package com.gauravbytes.good.srp.parser;

import java.io.File;

/**
 * @author Gaurav Rai Mazra
 * 
 */
public class CSVFileParser implements Parser {

 @Override
 public void parse(File file) {
  //logic to parse CSV file goes here
 }

 @Override
 public FileType getFileType() {
  return FileType.XML;
 }

}
package com.gauravbytes.good.srp.parser;

import java.io.File;

/**
 * @author Gaurav Rai Mazra
 * 
 */
public class XmlFileParser implements Parser {

 @Override
 public void parse(File file) {
  // logic to parse xml file
 }

 @Override
 public FileType getFileType() {
  return FileType.XML;
 }

}
package com.gauravbytes.good.srp.parser;

import java.io.File;

/**
 * @author Gaurav Rai Mazra
 * 
 */
public class JsonFileParser implements Parser {

 @Override
 public void parse(File file) {
  // Logic to parse json file
 }

 @Override
 public FileType getFileType() {
  return FileType.JSON;
 }

}

Benefits of SRP

Organize the code: By following SRP, we organized the code in well defined classes. Every class will have its own purpose (single purpose) and single reason for change.

Less fragile: When a class has more than one reason to change then it is more fragile. One change may lead to unexpected behaviour or problems at other places which will never be known to us until later stage of the project.

Low Coupling: More functionalities in single class mean high coupling or cohesion. In our example coupling is lowered by composition relation.

Code refactoring: Code refactoring is easy task. If we want to change behaviour then we can change by setting other parser type in our example.

Maintainability, Testability and easier debugging are the other benefits of following SRP in class designing.

This is how we can gain long-term benefits from SRP. You can find the example code on github.