Getty Images
How to apply the single responsibility principle in Java
How does the single responsibility model work in a Java program? Here we show you what this SOLID principle means, and how to implement the single responsibility model in Java.
What is the single responsibility principle?
The single responsibility principle in Java demands that a class serves a single, clear purpose.
Any attempt to add peripheral functionality to a well-designed class must be thwarted. Instead, you must place it into another Java component.
Single responsibility and SOLID
The single responsibility principle (SRP) represents the first letter in the object-oriented design pattern acronym SOLID, which is composed from the following terms:
- Single responsibility.
- Open/closed.
- Liskov substitution.
- Interface segregation.
- Dependency inversion.
Benefits of the single responsibility principle
Software components built according to the SRP will accrue benefits in the following 10 domains:
- Simplicity. With only a single purpose, SRP components are much simpler than composite components with many functions.
- Reusability. A simple component with a clear and focused purpose is easier to correctly reuse in other parts of a complex system.
- Testability. It is much easier to think up tests for a component that has one clear purpose, as opposed to one with a wide array of responsibilities.
- Flexibility. It is easier to make changes to a small, simple component that has clear integration points, as there is lower possibility of unintended consequences.
- Scalability. SRP components have less internal state and fewer dependencies versus a coarse-grained component, which makes them easier to scale.
- Collaboration. When a system is under active development, SRP components make it easier for developers to work on individual components and not cause merge conflicts.
- Maintainability. Codebases are easier to maintain with SRP components that are smaller with fewer responsibilities blended together.
- Quality control. A system with individual components that are singularly focused is easier to troubleshoot, as the sources of problems are easier to isolate.
- Change management. When you edit or test a component with fewer properties and methods, it is easier to test integration points and identify potential bugs.
- Refactoring. Refactoring a monolithic system built with SRP components into a microservice is much simpler than one built with coarse-grained components.
Single responsibility example in Java
Here's a simple example of a Java class that correctly observes the single responsibility principle. Imagine a number guessing game where each time it is played the game tracks two things: what the magic number was, and the number of attempts to guess it.
public class GameResult { int guesses; int magicNumber;
GameResult( int numberOfGuesses, int theNumber) { guesses = numberOfGuesses; magicNumber = theNumber; }
}
This GameResult has a single responsibility that is clear and concise: Keep track of the results of a game.
Example of a SOLID single responsibility principle violation
Now imagine a developer wants to additionally keep track of the results of all games played. One way to achieve this is to add a List to the GameResult class and add to it with every creation of a new GameResult object.
public class GameResult {
static List<GameResult> history = new ArrayList();
int guesses; int magicNumber; GameResult( int numberOfGuesses, int theNumber) { guesses = numberOfGuesses; magicNumber = theNumber; history.add(this); } }
This code will work, but it's ugly.
The code also violates the single responsibility principle in Java. The GameResult component, which should do nothing more than keep track of a single game, is now also tracking history.
Refactoring with SOLID
Currently the number-guessing-game code is manageable, but imagine if we need methods that cover several other functions, such as the following:
- Print the history.
- Edit the history.
- Delete elements from the history.
These methods don't serve the class's primary purpose, and will pollute the GameResult class. The result would morph into something like the following:
public class GameResult {
static List<GameResult> history = new ArrayList(); int guesses; int magicNumber; GameResult( int numberOfGuesses, int theNumber) { guesses = numberOfGuesses; magicNumber = theNumber; history.add(this); }
public static void printHistory(){ /* lots of code */ } public static void updateRow() { /* lots of code */ } public static void deleteRow() { /* lots of code */ } public static void editRow() { /* lots of code */ } }
Refactor single responsibility violations
In this example, to easily refactor the code and fix the single responsibility principle violation, just create a new class dedicated exclusively to manage the history.
This new class's single responsibility is to provide history-related functions.
public class GameHistory { private static List<GameResult> history = new ArrayList(); public static void printHistory(){ /* lots of code */ } public static void updateRow() { /* lots of code */ } public static void deleteRow() { /* lots of code */ } public static void editRow() { /* lots of code */ } }
Drawbacks to the single responsibility principle
As with all design patterns, there are drawbacks to weigh against the benefits.
When taken to the extreme, application of the single responsibility principle can result in extremely fine-grained micro-components that present the following challenges:
- Increase overall complexity, as the number of components gets too large.
- Increase coupling between classes that call each other, which makes modifications harder.
- Decrease cohesion within the code, which makes it harder to understand how the integrated system works.
- Create an overengineered system that becomes more difficult to maintain.
Ready to implement the single responsibility principle
As software developers become more experienced with object-oriented principles and domain-driven design, the observation of SOLID principles becomes a natural part of the development process.
Over time, they will better understand which functions to group together in a single Java class, and which components to separate through an easily integrated interface.
When you correctly balance those factors, observation of the single responsibility model in Java makes enterprise systems faster to develop, easier to troubleshoot and cheaper to maintain over the long term.
The code for this single responsibility principle in Java example is available on GitHub.