SOLID Programming (1) - Single Responsibility Principle

Subscribe to my newsletter and never miss my upcoming articles

SOLID is an acronym for several of the most useful OOP design principles:

In this post, I will cover the first principle, the Single Responsibility Principle (SRP). I hope these posts will give you an understanding of how these principles can improve the way you write code and maybe impress the next person that reviews your code!

If you liked these posts feel free to follow me on Twitter! I would like to take you on a journey of becoming the best you can possibly be.

Single Responsibility Principle

"A class should have only one reason to change"

-Robert C. Martin

A class should encapsulate responsibility. A simple way to implement this is to group together methods and attributes that are going to change for the same reason. This allows us to write modular code that is easily maintainable and testable.

Another reason for learning this principle is to understand how and when to refactor code. Take a look at your source control and find the top five most touched files. There is a good chance these classes are breaking the Single Responsibility Principle. If these areas are constantly changing or, worse yet, frequently creating bugs this may be a great place to apply this principle.

git log --pretty=format: --name-only | sort | uniq -c | sort -rg | head -10

Cohesion

Cohesion refers to the responsibilities of a class or module. Low cohesion means a class does a variety of actions. High cohesion means a class is very focused and has a small scope.

// encapsulates a variety of functions
public class LowCohesionStaff{
   public void checkEmail(){..}
   public void sendEmail(){..}
   public void emailValidate(){..}
   public void printLetter(){..}
}
// fine grain POJO
public class HighCohesionStaff{
   private Integer salary;
   private String email;

   private Integer getSalary(){...}
   private void setSalary(){...}
   private String getEmail(){...}
   private void setEmail(){...}
}

Coupling

Coupling refers to how closely two classes or modules are related. Low coupling allows you to make changes in one class without affecting the other. High coupling makes it difficult to change or maintain code.

Single

What does "single" mean? This simply means all the code in a module should generally have the same theme or reason to change. This design typically has high cohesion and low coupling. Let's review a simple example:

public class Car{
  private Engine engine;

  public void start(){
    this.engine.start();
  }
}

This is an excellent demonstration of the SRP. We can start the Car without having to know anything about the Engine. Starting the Engine is abstracted to its own class. We should be able to make changes to the Engine class without changing how the Car starts.

Code Smell - Overly Coupled Classes

// Good
public Class A{
  @Autowired
  public A(B bService){
  }
}

// Potentially Bad
public Class A{
  @Autowired
  public A(B serviceB, C serviceC, ... Z serviceZ){
  }
}

If a class has an abundance of dependencies it can't possibly have a single responsibility! A common symptom of "single" is having common dependencies. If you have class B and class A requires class B to do work these classes are considered coupled. Now say class A needs classes B-Z to do all its work, this could be a potential problem. This is often a red flag, so keep an eye out!

Code Smell - God Classes

A "God Class" is an object that controls too many other objects in a system and has grown beyond all logic to become, The Class That Does Everything.

If a class is several thousand lines long this is often a terrible code smell. This often indicates that the class is overly complex. However, keep in mind this can sometimes be justified. It is recommended to split these classes apart to become more object-oriented. Breaking apart these classes will not only reduce complexity but make your code more maintainable and testable. This is an implicit side effect of using SRP!

Pro Tip: When refactoring a God Class start with abstracting dependencies to a new class and take the dependent code with it!

Bad Example - God Class

/**
* this is a short example of what a "God" class CAN look like. 
* this is not always the case but in enterprise applications, the business logic
* for different operations can grow out of hand. This can lead to coupled code
* that becomes commonplace for broken tests and bugs
*/
public class BadTeamGodClass{
  // create team
  public ResponseObj createTeam(){...}  
  public ResponseObj validateForCreate(){...}  

  private ResponseObj createBusinessLogic1(){...}

  // update team
  public ResponseObj updateTeam(){...}  
  public ResponseObj validateForUpdate(){...}      

  private ResponseObj updateBusinessLogic(){...}

  // mix / match
  // isNewObj -- this is a code smell!
  public ResponseObj createOrUpdateBusinessLogic(Boolean isNewObj){...}
}

Good Example - No God Class

/**
*  base class
*  this could also be a static helper class 
*/
public class GoodTeamClass{    
  // common logic needed in update and create
  protected ResponseObj commonBusinessLogic(){...}
}
/**
*  Class responsible for creating teams. 
*/
public class GoodCreateTeamClass extends GoodTeamClass{
  // create team
  public ResponseObj createTeam(){...}  
  public ResponseObj validateForCreate(){...}  

  private ResponseObj createBusinessLogic(){...}       

/**
*  Class responsible for updating teams
*/
public class GoodUpdateTeamClass extends GoodTeamClass{    
  // update team
  public ResponseObj updateTeam(){...}  
  public ResponseObj validateForUpdate(){...}      

  private ResponseObj updateBusinessLogic(){...}
}

What changed between these classes? Essentially, we broke down functionality into individual classes. In the bad example, we have code that is responsible for creating and updating entities. In the good example, we broke this down into multiple classes. This makes code more maintainable and testable.

Once you are done refactoring, make sure you take time to reflect and ensure these changes are an improvement to maintainability. You should design your classes to be easily manageable and encourage ease of use. While God classes also have their place in software development, they typically do not have either trait.

Bad Example - Coupled Data Access Object (DAO) Service

/**
*  This is an example of a class doing more than it needs to
*  This class should handle Team DAO logic
*  Another class should handle TeamPlaer DAO logic 
*/
public class BadTeamService{

  public ResponseObject saveTeam(Team team, List<TeamPlayers> teamPlayers){
    validateTeam(team); // throw TeamValidationException extends RuntimeException if invalid
    validateTeamPlayers(teamPlayers);

    // business logic for updating team and its players
    insertOrUpdateTeam(team);
    insertOrUpdateTeamPlayers(teamPlayers);
  }
}

Good Example - Decoupled Data Access Object (DAO) Service

/**
*   This is a better implementation to maintain single responsibility between DAO entities
*/
public class GoodTeamService{
  GoodTeamPlayerService goodTeamPlayerService = new GoodTeamPlayerService();

  public ResponseObject saveTeam(Team team, List<TeamPlayers> teamPlayers){
    validateTeam(team); // TeamValidationException extends RuntimeException

    // business logic for updating team and its players
    insertOrUpdateTeam(team);
    goodTeamPlayerService.saveTeamPlayers(teamPlayers);
  }
}
public class GoodTeamPlayerService{

  public ResponseObject saveTeamPlayers(List<TeamPlayers> teamPlayers){
    validateTeamPlayers(teamPlayers); // TeamValidationException extends RuntimeException

    // business logic for updating team and its players
    insertOrUpdateTeamPlayers(teamPlayers);
  }
}

Lastly, this is an example of a DAO responsible for saving a team and then saving the players on the team. The bad example has both of these actions taking place in the same class. SRP would encourage you to isolate this logic into a class for saving the team then the players.

Conclusion

Writing code is a true art form. It’s very similar to how a musician writes music or how an artist paints a portrait. Becoming a great artist takes time and practice. It takes making mistakes, learning what doesn’t work, and how to go about solving problems. You need to learn from blunders and be able to recognize when you’ve faced a problem you have already solved. You should be able to model solutions based on these past problems. Sometimes you need to think outside the box, while other-times keeping things simple is often the best solution.

If you liked this post feel free to follow me on Twitter! I would like to take you on a journey of becoming the best you can possibly be.

Resources

Stackify Solid Design Principles

Toptal Single Responsibility Principle

You don't Understand the Single Responsibility Principle

Active Record Pattern

How Can I Avoid Code Fragmentation

Understanding Solid Principles Single Responsibility

Difference Between Cohesion and Coupling

Maxi Contieri's photo

Great Article !

Keep Rocking!