SRP: Single Responsibility Principle in Swift (with code examples) — SOLID Principles

Gizem Türker
6 min readJun 22, 2022

--

A class should have one, and only one, the reason to change.

A class should have one, and only one, a reason to change.

In today’s dynamic software development landscape, adaptability and maintainability are not just desirable attributes — they are essential. As projects grow and requirements evolve, the ability to introduce changes without causing widespread disruption is a critical challenge. One of the most effective strategies to address this challenge is through the adoption of modular design principles and a steadfast commitment to the Single Responsibility Principle (SRP). This guide delves into these concepts, illustrating their importance and practical application in modern software development.

In modular systems, quickly finding what you are looking for is possible.

Sometimes, some features are removed from some libraries and made into separate libraries, as an example of the main framework doing one job well. If the code block were not modular, such an opportunity would not exist.

The Essence of Modularity in Software Design

Modularity is a design approach that involves decomposing a system into smaller, manageable, and interchangeable components, each responsible for a specific piece of functionality. This principle enables developers to tackle complex systems by focusing on smaller, isolated parts, making the development process more manageable and less error-prone. Modular design fosters several key benefits:

  • Enhanced Flexibility: Modular systems are more adaptable to change since modifications can be made to individual components without affecting the entire system.
  • Simplified Debugging: Isolating functionality into separate modules makes it easier to identify and fix bugs, as the potential problem area is narrowed down.
  • Improved Reusability: Components designed to perform a specific function can be reused across different parts of the application or even in different projects, reducing development time and effort.
  • Facilitated Collaboration: When a system is broken down into modules, teams can work on different components simultaneously, streamlining the development process.

The Single Responsibility Principle: A Closer Look

At the heart of modular design is the Single Responsibility Principle (SRP), a core concept of the SOLID principles of object-oriented design. SRP asserts that a class or module should have one, and only one, reason to change. This principle can be applied at various levels of software architecture:

  • Methods: Should accomplish a single task, making them easier to understand and test.
  • Classes: Should manage a single aspect of the functionality, enhancing cohesion and reducing complexity.
  • Packages and Modules: Should focus on a specific domain or functionality, promoting loose coupling and high cohesion.
  • Systems: At the highest level, systems should be composed of modules that each address a distinct aspect of the system’s overall responsibility.

The adherence to SRP brings about numerous advantages, including more organized and manageable code, reduced fragility to changes, easier refactoring opportunities, more descriptive naming, and overall, a more maintainable and testable codebase.

Practical Implementation: Lessons from a Food Delivery App

To illustrate the practical application of these principles, consider a food delivery application developed in Swift. The design evolution of this application highlights the pitfalls of neglecting SRP and the benefits of embracing modular design.

Initial Design Flaws:

In an initial implementation, a Foods class may encompass both menu items (such as food_id, food_name, food_price, food_img_name) and unrelated user details (such as user_name, street_name, city_name). This design violates the SRP, as the class is burdened with multiple responsibilities, leading to a bloated and less coherent structure. Such a class is difficult to maintain and prone to errors, as changes in user-related features could inadvertently affect menu functionalities.

Let’s consider an example in Swift. — “food delivery app” :

In the food delivery application, food_id , `food_name, food_priceand food_img_name, are described in the Foods class. Apart from these, there are user_name, street_name , city_nameaddress and user information. Trying to explain the address and user data in the section where the food-related data is given caused it to appear in an irrelevant situation as it is against the SRP.

Although address and user information is needed in the delivery application designed here, including the menu is meaningless for the code block and is not compatible with SRP.

In addition, although the login, logout and changeAdressfunctions here contain the features of the user, it is wrong in terms of SRP to be included here.

Enhanced Modular Design

A refined approach would see the application’s functionality cleanly divided into focused classes. The Foods class would be strictly concerned with food-related information, while separate classes would handle user details and address information. This clear separation aligns with the SRP, ensuring each class has a single responsibility and reason to change. The benefits of this approach include improved code clarity, easier maintenance, and the ability to update or extend one aspect of the application without risking unintended side effects in others.

Each class should be modular and free of complexity. It is necessary to achieve a solid and tight structure by dividing it into small parts. The Foods class is defined here only for its related function.

Let’s look at a different example and make sense.

Creating and explaining a complex example that violates the Single Responsibility Principle (SRP) can help illustrate the importance of adhering to this principle in software design. Below, I’ll provide a Swift code example that violates SRP by mixing several responsibilities within a single class, and then refactor it to adhere to SRP, thus improving the code’s structure, maintainability, and scalability.

Violating SRP: Complex Example

Consider a Swift class named UserProfileManager that handles user details, authentication, and user preferences. This example is intentionally designed to violate SRP by combining multiple responsibilities.

class UserProfileManager {
var username: String
var email: String
var password: String
var preferences: [String: Any]

init(username: String, email: String, password: String, preferences: [String: Any]) {
self.username = username
self.email = email
self.password = password
self.preferences = preferences
}

func authenticate() -> Bool {
// Simulate authentication process
print("Authenticating user \(username)...")
return true
}

func updateEmail(newEmail: String) {
self.email = newEmail
print("Email updated to \(email).")
}

func updatePreferences(newPreferences: [String: Any]) {
self.preferences = newPreferences
print("Preferences updated.")
}

// Additional methods that handle user profile management, authentication, and preferences
}

This class violates SRP because it is responsible for:

  • Managing user details.
  • Handling authentication.
  • Managing user preferences.

Refactoring to Adhere to SRP

To adhere to SRP, we should refactor the UserProfileManager into multiple classes, each handling a single responsibility.

  1. User Details Management: Handles storing and updating user details.
  2. Authentication Management: Handles the authentication process.
  3. User Preferences Management: Manages user preferences.

Refactored Code

class UserDetails {
var username: String
var email: String
var password: String // In a real scenario, passwords should be handled more securely

init(username: String, email: String, password: String) {
self.username = username
self.email = email
self.password = password
}

func updateEmail(newEmail: String) {
self.email = newEmail
print("Email updated to \(email).")
}
}

class UserAuthentication {
let userDetails: UserDetails

init(userDetails: UserDetails) {
self.userDetails = userDetails
}

func authenticate() -> Bool {
// Simulate authentication process
print("Authenticating user \(userDetails.username)...")
return true
}
}

class UserPreferences {
var preferences: [String: Any]

init(preferences: [String: Any]) {
self.preferences = preferences
}

func updatePreferences(newPreferences: [String: Any]) {
self.preferences = newPreferences
print("Preferences updated.")
}
}

Discuss in the comment.

  • UserDetails: This class is now solely responsible for managing user details like username, email, and password. It provides a method to update the email address.
  • UserAuthentication: Dedicated to handling the authentication of the user. It requires a UserDetails instance, illustrating dependency injection and focusing solely on the authentication process.
  • UserPreferences: Manages user-specific preferences. This separation allows for preferences to be modified independently of user details or authentication status.

This refactoring demonstrates adherence to the Single Responsibility Principle by ensuring each class has only one reason to change. It simplifies maintenance, enhances code readability, and makes the system more flexible for future changes or extensions.

Follow the LinkedIn, Twitter, and Github.

--

--