☰ Menu     Home » SOLID Design Principles » Single Responsibility Principle

Single Responsibility Principle

Motivation

In this context, a responsibility is considered to be one reason to change. This principle states that if we have 2 reasons to change for a class, we have to split the functionality in two classes. Each class will handle only one responsibility and if in the future we need to make one change we are going to make it in the class which handles it. When we need to make a change in a class having more responsibilities the change might affect the other functionality related to the other responsibility of the class.

The Single Responsibility Principle is a simple and intuitive principle, but in practice it is sometimes hard to get it right.

Intent

A class should have only one reason to change.

Single Responsibility Principle Diagram

Example

Let's assume we need an object to keep an email message. We are going to use the IEmail interface from the below sample. At the first sight everything looks just fine. At a closer look we can see that our IEmail interface and Email class have 2 responsibilities (reasons to change). One would be the use of the class in some email protocols such as pop3 or imap. If other protocols must be supported the objects should be serialized in another manner and code should be added to support new protocols. Another one would be for the Content field. Even if content is a string maybe we want in the future to support HTML or other formats.

If we keep only one class, each change for a responsibility might affect the other one:

  • Adding a new protocol will create the need to add code for parsing and serializing the content for each type of field.
  • Adding a new content type (like html) make us to add code for each protocol implemented.
Single Responsibility Principle: Email Example

// single responsibility principle - bad example

interface IEmail {
	public void setSender(String sender);
	public void setReceiver(String receiver);
	public void setContent(String content);
}

class Email implements IEmail {
	
	public void setSender(String sender) {
		// set sender;
	}
	
    public void setReceiver(String receiver) {
		// set receiver;
	}
	
	public void setContent(String content) {
		// set content; 
	}
}

We can create a new interface and class called IContent and Content to split the responsibilities. Having only one responsibility for each class give us a more flexible design:

  • adding a new protocol causes changes only in the Email class.
  • adding a new type of content supported causes changes only in Content class.

// single responsibility principle - good example

interface IEmail {
	public void setSender(String sender);
	
	public void setReceiver(String receiver);
	
	public void setContent(IContent content);
}

interface Content {
	public String getAsString(); // used for serialization
}

class Email implements IEmail {
	public void setSender(String sender) {
		// set sender; 
	}
	
	public void setReceiver(String receiver) {
		// set receiver;
	}
	public void setContent(IContent content) {
		// set content;
	}
}

Real World Examples

To better understand the practical implication of single responsibility principle let's look in some real-world examples when SRP is applied in common programming scenarios.

Example 1: User Management in a Web Applications

In most of the web applications, we use a User class that is responsible for managing user information. As a project starts, this class might handle tasks like storing user details and validating user data. However, as the application grows, developers might be tempted to add more responsibilities to the User class, such as generating reports on user activity or handling authentication and authorization.

Single Responsibility Principle: User Example

Applying SRP, these functions should be separated into different classes:

  • A User class solely for storing and managing user details.
  • A UserValidator class for validating user data.
  • A UserReportGenerator class for creating user activity reports.
  • An Authenticator class for handling authentication.
  • An Authorizer class for managing user permissions.

This separation ensures that changes in reporting mechanisms, authentication methods, or user data validation rules only affect the relevant classes, not the core User class. As a project grows, this separation would allow not only easier maintanibility and extension of code, but also allowing different team members to work on parts of user functionality in the same time.

Example 2: Order Processing System

Let's consider an Order class in an e-commerce system, initially designed to handle order details and process payments. As the system evolves, it's common to see additional responsibilities like order serialization for storage, sending notifications to customers, and logging being added to the Order class.

By following SRP, we would divide the responsibilities as follows:

  • An Order class for managing order details.
  • A PaymentProcessor class for handling payment transactions.
  • An OrderSerializer class for converting orders into a format suitable for storage.
  • A NotificationService class for sending order status updates to customers.
  • A OrderLogger class for logging order processing steps.

This separation allows for independent modification of how orders are processed, how notifications are sent, or how orders are serialized without affecting the other functionalities.

Example 3: Reporting in a Financial System

In a financial system, a class like FinancialReport might be tasked with generating various types of financial reports. Over time, responsibilities like exporting reports in different formats (PDF, Excel, etc.), sending these reports via email, and formatting the report data could accumulate in the same class.

By implementing SRP, we would separate and allocate responsibilities to distinct classes:

  • A FinancialReport class for compiling financial data.
  • A ReportExporter class with subclasses for each format (e.g., PDFExporter, ExcelExporter) for exporting reports.
  • A EmailService class for sending reports via email.
  • A ReportFormatter class for formatting the report data.

By separting responsibilities and assign them to different classes, we ensure that changes to the report format, the export mechanism, or the email service do not interfere with the fundamental task of compiling financial data.

Conclusion

The Single Responsibility Principle represents a good way of identifying classes during the design phase of an application and it reminds you to think of all the ways a class can evolve. A good separation of responsibilities is done only when the full picture of how the application should work is well understand.