Understanding Single Responsibility Principle in Object Oriented Design

Single Responsibility Principle (SRP) is one of the basic 5 principles of object-oriented design, commonly known under the S.O.L.I.D., which were formulated first by Robert C. Martin. These principles, when applied together, are making easy the development of software easy to maintain and extend. 

SRP is the first from the 5 principles (hence the letter S from S.O.L.I.D.). The other four are: 

  • - Open-Closed Principle, 
  • L - Liskow substitution principle, 
  • I - Interface segregation principle, 
  • D - Dependency Inversion Principle. 

This principle is stating a very simple fact: A class should only have one reason to change. This translates in that a class should have only one responsibility, should do one thing only, because a responsibility is an axis of change. If a class has more than one responsibility, has more than one reason to change. 

How do you determine that single responsibility and how you can identify that a class has multiple responsibilities? That is the hardest part. In Object Oriented Design (OOD), a class is a representation of a part from the problem domain, a tight coupling of data structures and methods that act upon that data. Different methods might do different things with that data, so where you draw the line between different responsibilities? One way is to look at a responsibility as a reason for change. Let’s look at an example:

We have a UserGroup class, similar to a security group: users can be added or removed from the group, an user can be tested as belonging to the group or not, users can be imported to the group from CSV files, in a predefined structure, or the content of the group can be exported to an XLS file, again in a predefined structure. 

At first glance seems a reasonable way to model this concept. We have everything that is acting on some specific data structure, the list of users, in one place. 

But looking at the method, the class is doing more than one thing. Not only is handling the basic actions related to the concept of a user group, but also is dealing with the specific logic of reading contents from a CSV file and also outputting content to a XLS file. Moreover, the latter usually also generates an external dependency of the class to an already developed API which deals with XLS files. Coming back to what we said about a responsibility being a reason for change and looking at this class we clearly identify at least 3 reasons to change: 

  • one reason is related to the basic functions of a security group (users joining in or joining out);
  • the second is related to importUsersFromCSV function: what if the CSV structure is changing? Or is no longer a CSV file but a JSON or XML file?
  • the third is related to exportUsersToXLS: what if the XLS structure has changed or yet again it is require exporting the users also to a LDIF file.

Any of the above reasons will require changes to the UserGroup class, re-compiling the code, re-packaging the application and re-installing it. Even though the security module, which only uses the basic functions of the class, has nothing to do with the CSV format, still it will need to be also re-compiled, re-packaged and re-deployed. That increases the risk of breaking functionality that works in the security module, even though the methods used by this module have not been changed (but they are working on the same data structures as the others). 

It is clear that these responsibilities should be separated. A better design would be:

What we did is we kept the two import and export methods but we abstracted the import and export functionalities behind two interfaces which can have specific implementations that can be provided to UserGroup class at runtime. In this way, even though we keep all functionalities of the class (it makes sense in a way, since importing or exporting are also basic actions on a group), the responsibility of actual import and export relies now in other classes, behind interfaces that abstract the access to them. 

There are other designs for this, of course. One is to actually eliminate the import and export methods from the UserGroup class and create complete new classes which are handling this (with small changes on our latest design). But the point is we eliminated the additional sources of change from a class, so the principle is followed in this case. 

Should we do this all the time? Not necessarily. An axis of a change (a.k.a a responsibility) is an axis of a change only when the change actually occurs. In our case, if there will never be the case to change the way import and export are done, then there is no reason for change and the class can remain as it is. In other words, if there is no symptom, don’t take the treatment. 

SRP is one of the simples of the principles, but it is very difficult to get it right. Joining responsibilities comes naturally in our class designs, mostly from the idea that if a class models an entity from a problem domain then that class should contain everything there is know about that entity. Finding and separate responsibilities, where is the case, is what software design is all about.