Single Responsibility Explained: Improve Your Software Architecture

stephen awololastephen awolola
5 min read

Introduction

Single responsibility is a design principle that states that classes take one responsibility and one only. This principle applies to object oriented design, and one might wonder, of course, a class is supposed to be concerned with a single responsibility. Most people think the single responsibility here refers to managing all features related to a model that is being abstracted by the class. In this article, I will provide a scenario of how to apply the single responsibility principle.

Context

You have to design a library system, for this exercise the features are limited, the user can checkout and return a book, but before a book is lent out to a user, the librarian checks the availability of the book. The system ensures consistency by using a remote database to persist application data.

The code snippets below show an example of how this requirement can be developed. There are 3 main classes, Author, Book, and the Library class. For the sake of this example, the snippet references a “DatabaseConnection” class that is taken to be an interface for the database.

package com.migia;

public class Author {
    private String name;
    private String email;
    //getters and setters
}
package com.migia;
import java.util.List;
public class Book {
    private String title;
    private String isbn;
    private String edition;
    private boolean isBorrowed;
    private List<Author> authors;

    public boolean updateBookDetails(){
       // Library.makeConnectionToDatabase().updateBookTable(this);
        return true;
    }
    // Getters and Setters
}
package com.migia;

import java.util.List;

public class Library {
   private List<Book> books;
   public final static String databaseUrl = "mysql://database_connectivity_details";
   private DatabaseConnection connection;

   public static DatabaseConnection makeConnectionToDatabase(){
      //Connect to the database with the url
      return null;

   }

   public boolean checkBookAvailability(String title){
      //Implementation
      return true;
   }

   public boolean checkoutBook(String title){
      // Business logic
      //makeConnectionToDatabase().updateBookTable(Book book);
      return true;

   }
   public void returnBook(String title){
      //Business Logic
      //makeConnectionToDatabase.updateBookTable(Book book);
   }

}

Can you identify what is wrong with the implementation above?

First off, taking a look at the implementation of the book class, it defines a method to update the book details, you might wonder, it is still dealing with the “Book” entity. Yes, it is. However, the implementation of the method shows that it creates a new database connection to update its state. This is a direct violation of the single responsibility principle. The first responsibility of this class is to act as a model class for the application, it is meant to hold the data required to describe a book, now with the addition of the “updateBookTable” method, the class is now concerned with how the Book entity is stored in the database.

A similar issue can be found in the Library class, it defines a method to explicitly connect to the database as well as manage the operations in the library such as checking a book out. If you are not familiar with the single responsibility principle before, you might think less of this problem, however, when the system scales and there are a lot more classes, different issues will arise because of this design. For our example, writing test cases becomes more tasking, why?

Normally, you would mock a couple of classes to test a dependent class, however, now you have to make sure that a lot of other components are working before you can write the test cases because other methods depend on the database creation method and not thoroughly testing this method will make the testing not cover all necessary areas.

Imagine this scenario, say the business logic in checking out a book is by making sure that certain editions are not checked out. Now as a developer, the librarian reports an error to you that the system crashes when trying to check a book out for a user, which class are you going to open for debugging? Yes, the Library class, what if the issue is that checked-out books are still showing that they are available? Obviously, there is something wrong with the database operation. And with our current implementation, the Library class is still the one responsible for this.

In essence, what I am trying to show is that the library class has two jobs, one is to manage the library activities and the other is to manage the database connection and persist the application. In order to follow the Single Responsibility principle, these features can be separated into two different classes where there is some sort of dependency. Same thing for the Book class.

The correct implementation would require the addition of a separate class to manage the database operations. Therefore, the high-level design of the system will feature a series of model classes (Book, Author, etc), Database connectivity and helper functions, and lastly classes for the business logic.

package com.migia;

import java.util.List;

public class Book {
    private String title;
    private String isbn;
    private String edition;
    private boolean isBorrowed;
    private List<Author> authors;

    public boolean updateBookDetails(){
       // Library.makeConnectionToDatabase().updateBookTable(this);
        return true;
    }
    // Getters and Setters
}
package com.migia;

public class Database {
 final static String databaseUrl = "mysql://database_connectivity_details";
 public Database(){
     //Initialize the database
 }

 public boolean updateBookTable(Book book){
     //Update the book table
     return true;

 }

 public boolean checkBookAvailability(String title){
  //Check the availability
  return false;
 }

public Book findBookByTitle(String title){
 //Query the database
  return null;
 }

}
package com.migia;

import java.util.List;

public class Library {
   private List<Book> books;
  private Database database;
  public Library(Database database){
     this.database = database;
  }
   public boolean checkBookAvailability(String title){
      //Implementation
      return database.checkBookAvailability(title);
   }
   public boolean checkoutBook(Book book){
      // Business logic
      //makeConnectionToDatabase().updateBookTable(Book book);
      return database.updateBookTable(book);

   }

 public void returnBook(Book book){
      //Business Logic
      //makeConnectionToDatabase.updateBookTable(Book book);
      database.updateBookTable(book);
   }

   public Book getBook(String title){
      return database.findBookByTitle(title);
   }

}

The better implementation for the problem is shown above, and this is better because it makes the code structure simpler to understand, a new team member having a database connectivity bug will know what class to address instead of checking different classes for implementation of the database.

0
Subscribe to my newsletter

Read articles from stephen awolola directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

stephen awolola
stephen awolola