Single Responsibility principle with example

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.

No comments :

Post a Comment