In this article I want to present a refactoring method in respects to the Single Responsibility Principle.

What is the Single Responsibility Principle?

Using my own words, understanding and experience, the Single Responsibility Principle, SRP for short, states that a class or module only serves a very narrowly defined and specific task or role, a single responsibility. If this is given, classes can be more easily rearranged in case of large-scale system changes that affect upper abstraction layers, i.e. layers that orchestrate the logic of multiple different classes together (e.g. business logic on web service level).

In Robert C. Martin’s own words, SRP is a separation of concerns, a reason to change: one class should only have one reason to change or be rewritten. The term SRP was popularized in his 2003 book Agile Software Development, Principles, Patterns, and Practices.

How to use Single Responsibility Principle

SRP is not just cutting your code into multiple methods or classes. Each method or class also needs to have a well-defined and limited scope, a single responsibility. It is also necessary to identify all “atomic tasks” that may be reused elsewhere. From database and operating system environments (and even mathematics) the term atomic operation describes a small unit of instructions. Technically, these exist in a development context as well, perhaps you can even call them “micro use cases”, but let’s call them Atomic Tasks for now. Atomic tasks by themselves should be considered as “library functionality”, that means something you’d add to a (static) utility class. A good practice is to pack these into class instances you create as necessary, rather than use static methods (overusing static methods would be functional programming, not object oriented programming, so keep that in mind when applying this to your programming language of choice). In either case, these always need an orchestrating or controlling class that ensures all tasks are strung together to solve a problem. This is how you can rearrange them easily as your application grows.

Refactoring using the Single Responsibility Principle

For example, if I wish to read JSON data from a file and then turn that into business objects (and wish to reinvent the wheel for educational purposes), it can be broken down to the following responsibilities:

  • Reading the file (File Reader)
  • Parsing the file contents (Parser)
  • Turning the parsed file content into usable objects (Converter)
  • Additional logic that strings these tasks together (“Conductor”)

Ergo this use case can be implemented by creating classes for a file reader, a JSON parser, a JSON converter. These are our atomic tasks. However, we still need an orchestrating class (like a conductor) that strings the logic of all these classes together. Let’s call this class a JSON object loader. The JSON object loader realistically would require a file path String as input and realistically may output a list of business objects.

Four classes just for such a simple task? That seems excessive!

Believe me, it is not.

At least not in the long-run…

Suppose later on in the project, I do not want only want to read JSON data from a file, but also from a web service (e.g. using REST). The mechanism for parsing and converting objects remains the same.

So how do you go on about this?

If you look closely, you will notice a problem: our JSON object loader is not fit to be used in a REST-environment, as it reads its JSON data from a file path using a file reader.

Do you notice how there is still a need for refactoring here? We may have split functionality distributed to four classes, which indeed is SRP in action, but their field of responsibility still differs. We have two responsibilities or roles mixed in our example, namely file access and an object converter. You cannot always anticipate such implementation details before actually implementing them, so I cannot stress enough how important it is that your code stays flexible during development; the ground work for that is done if you identify roles and tasks in your use case.

There are multiple ways to solve this, but one way adhering to the SRP is to change the API of our JSON object loader and then adding another abstraction layer, ergo another orchestrating class to deal with different JSON sources (via REST and file access).

  • Instead of accepting a file path as input, our JSON object loader will accept an unparsed JSON String as input, that means, a pure String containing JSON directly read from a file. This ensures the JSON object loader can be reused. It will still return a list of business objects.
  • We create another abstraction layer on top of the JSON Object Loader (or in other words, an object layer that controls it), using two concrete implementations: JSON Web Object Loader that gets its data from a Web Helper of the sort, and the other is the JSON File Loader that reads the JSON from a file. They both return a list of business objects.

This allows for both implementation of a JSON Webservice Object Loader and a JSON File Object Loader, without accidentally cooking spaghetti code (most other solutions would produce that). As a rough object graph, this solution looks something like this:

In Java, URL can both be a file path and a web-based path, so even this approach can be further improved or simplified e.g. by creating a common interface “JSONSource” from which File Reader and Web Helper can inherit from; this is also how an application’s internal structure can incrementally grow without forcing it into anti-patterns, but that is enough material worth for another article. But I do hope it gets the refactoring process across.

Voila, our your little application grew incrementally and we even moved the File Reader to a different abstraction layer without much hassle, simply because we applied a refactoring that adheres to SRP. Code that is structured as in above graph is reusable and can be rearranged freely for any future use case. Even the test coverage is at its maximum efficiency, as you only had to implement a few additional tests for File Object Loader and Web Object Loader to extend your current use case. And if you do it right, these tests can consist of a few lines each.

Why using the Single Responsibility Principle

You may have to get used to it! The code you write in the beginning seems clunky and unnecessary as you spend a lot of time creating classes rather than adding the actual logic. You also might still need to learn about refactoring techniques in order to properly apply this principle. In either case, metaphorically spoken, using SRP you are creating a solid skeleton before adding the flesh and soul of the application, which is vital as an application grows:

  • Sticking to SRP causes applications to grow in a modular and incremental way and allows for maximum flexibility as new features are being added
  • If a large system refactoring is in order, you only have to rearrange your logic or class dependency on higher abstraction levels.
  • That means unit tests that you have written on lowest abstraction level will be left untouched by changes. Metaphorically, this is like rearranging the bones of your skeleton; that does not mean you have to change the bones themselves, as long as their function is well-defined and small-scoped enough
  • By actively applying SRP practices in a project, it is possible to refactor an application in such a way that metaphorically equates to replacing the pink flesh of a water melon with the yellow flesh of a grapefruit
  • SRP ensures a modular tree hierarchy in your code. A tree hierarchy can be quickly traversed, e.g. HashMaps have very short access time; Something similar applies to internal code structure and its maintainability or legibility

Part of these aspects also apply to SOLID principles as a whole; these methods and principles are interconnected.

Some Practical Tips

  • Test-Driven-Development helps you sticking to the SRP
  • The smaller the responsibility of one class, the better (I’m not sure if there is a lower limit yet, in my experience small chunks were always beneficial to quality so far). As developer, it’s like you are designing puzzles pieces: the smaller these pieces are, the easier they are to handle, break and put back together as you need
  • Avoid larger refactorings without automated tests (recommended), or compensate with very thorough manual tests (not recommended)
  • You can use UML and/or code generation tools to save yourself from creating many classes manually
  • As rule of thumb, a class should not be longer than 300 to 500 lines of code, depending on the logical complexity of the class. If a class is longer than that, it is time to reflect on whether it serves one responsibility or actually possesses more responsibilities

Refactoring using the Single Responsibility Principle

Post navigation


One thought on “Refactoring using the Single Responsibility Principle

Leave a Reply

Your email address will not be published. Required fields are marked *