Understanding the principle, and learning how to ensure that we don’t violate it in our design and code.
The Single Responsibility Principle is a fundamental principle of object-oriented design. This term was originally coined by Robert Martin, who says: a class should have only one reason to change. An alternative statement of this principle is: a class should have only one responsibility, and that responsibility should be entirely encapsulated in that class.
A verse in Rigveda, the ancient scripture of India says: एकं सद् विप्रा बहुधा वदन्ति, one truth, wise people say it in many different ways. The two versions of the Single Responsibility Principle, as well as terms like “separation of concerns” and “high cohesion” are different ways of saying the same “truth”.
Let’s take an example. Suppose we have a class that computes the salary for a month for an employee and prints the salary statement. Here the class is handling two different responsibilities: (1) salary computation, based on business rules, and (2) printing the statement. This class seems to be doing too much work.
The class has two potential reasons to change in the future:
1. Changes in business rules for salary computation, and
2. Changes in the way we want the salary statement to be printed (or possibly, we may not want to print the statement at all; we may, instead, want to save the results in a file or send those as part of a message to some other service).
When a class does too many things:
- It becomes bloated, making it harder to understand and maintain; this is bad for code organization and readability
- It may need to be changed more often (as explained above), making it fragile
- It becomes difficult to reuse as it has extra baggage; another application, for example, may only be interested in one of its responsibilities
- It requires more testing due to diverse responsibilities it handles
- Automating testing of the class may become hard, specially if business logic is mixed with things like user interface or database operations.
- It is low in cohesion; various parts of the class don’t necessarily belong together.
A better alternative would be to separate the two responsibilities (aka ‘concerns’) into two different classes:
- One to handle the responsibility of salary computation
- Another to format and print the output
In the future, if the business rules for salary computation change, only one class, the first one, needs to be modified. If the printing requirement changes, only the second class needs to be modified.
If instead of printing, or in addition to it, we decide to save the computed results to a file or send the results as part of a message to another application, we can easily replace the existing class that does the printing, or introduce new classes for the alternative behaviour, without any impact on the class that handles the business rules.
Such separation of concerns into different classes results in smaller classes. Such classes:
- Are easier to understand and maintain, due to the smaller size
- Have only one reason to change (business rules / printing requirement)
- Are easy to reuse, without extra baggage
- Are easy to test because of only one responsibility
- Lend themselves to test automation more easily, specially the classes that don’t involve any user interface
- Are high in cohesion; everything in the class belongs together.
Now, how do we ensure that a class follows the Single Responsibility Principle? Here are a few concepts that you can consider when segregating responsibilities into different classes:
- Core entities of your application such as Employee, Department, Customer, Supplier, PurchaseOrder, SavingsAccount, etc. Most such classes emerge from the vocabulary of the problem domain, and are, therefore, sometimes referred to as domain classes, or entity classes. These classes would encapsulate the data about the corresponding real-world entities, and the corresponding business rules. Remember that these are not direct representations of your database tables, instead are carefully constructed objects that represent the business needs.
- Business rules / validations: If the validations are complex or may change independently from the data elements of a class, then it is better to implement these in separate classes. Examples: SalaryCalculator, TaxCalculator, InterestCalculator.
- User interfaces: Data entry forms, tables that display multiple rows of data, report outputs, query output screens, and the like should all be separate classes. This way changes to the business rules of an application and its UI can be made without impacting each other.
Related terms such as Model-View Separation Principle, Model-View-Controller (and its variants) emphasize the importance of separating the model (the core of a system, the business data) from the view (user interface).
- System interfaces: If your application needs to interact with other systems (by way of messaging, or file transfer, or the likes), don’t expose the details of such interactions to all parts of your system. Instead, encapsulate such interfaces in their own classes, with an appropriate interface, and let the rest of the application only rely on such classes to exchange information with the other applications.
- Device interfaces. If your application communicates with devices, such as temperature sensors or motion detection sensors, encapsulate their APIs in their own corresponding classes. This way, if in the future, you decide to replace one brand of a device with another one, having a different API, you don’t have to make changes all over your application; these will be localized in the device interface classes.
- Persistence. If you use an O-R mapping framework (like Hibernate) to save your application’s data in some database, you don’t need to do much in your code for this. But if you need to handle persistence on your own, don’t mix the database operations with the business logic of your application. Create separate classes for storing, retrieving, modifying or deleting data.
- Class selection. If your application needs flexibility in terms of choosing which class from some inheritance hierarchy to select and instantiate, isolate such code in separate classes. Design patterns such as Abstract Factory or Prototype are useful for such purposes.
- Alternative algorithms or processing rules. If you have alternative algorithms for some task (sorting algorithms, search algorithms), or alternative processing rules (tax calculation for salaried / retired people), create separate classes for each alternative, and provide mechanisms for conditionally, or dynamically, selecting an appropriate class. Design patterns like Strategy, Template Method, Bridge are useful for these purposes.
- Error Handling. Have separate classes for handling the errors / exceptions in your application. This keeps the rest of the code uncluttered, and makes error handling logic easy to understand and maintain.
- Logging. Again, this is unrelated to the core behaviour of the application; don’t mix such code with the rest of the application; extract it in separate classes.
- Similarly, other different, unrelated aspects such as notifications, formatting, parsing of data, etc should also be implemented in separate classes.
Begin with a clear separation of concerns as suggested above. Keep in mind, however, that you are never “done” with applying the Single Responsibility Principle. The Next Step – ThomasHarvey.me as your system grows, you may find that a class that originally appeared to have a single, well-defined purpose, now seems to be doing too many things. Time to refactor your code.