Single Resonposility Principle in GO

Introduction

The single Resonposility Principle (SRP) states that a class, function, module, or type should have only reason to change.

The idea is to design classes or types that are focused on a single task or responsibility. This makes the code more modular, maintainable, and easier to understand.

What happens if SRP is not followed?

Violating this principle leads to classes or types with more than one responsibility, which results in code that is challenging to

  • maintain

  • test

  • extend

Such violations often lead to tightly coupled code, reduced reusability, and an increased likelihood of errors.

Adhering to SRP ensures that classes or types are focused on a single responsibility, promoting modularity, maintainability, and code clarity.

Example: SRP Violation

package main

import "fmt"

type User struct {
    Username string
    Password string
}

type UserService struct {
    users []User
}

func (us *UserService) RegisterUser(username, password string) {
    newUser := User{Username: username, Password: password}
    us.users = append(us.users, newUser)
    fmt.Println("User registered:", newUser.Username)
}

func (us *UserService) AuthenticateUser(username, password string) bool {
    for _, user := range us.users {
        if user.Username == username && user.Password == password {
            fmt.Println("User authenticated:", username)
            return true
        }
    }
    fmt.Println("Authentication failed for:", username)
    return false
}

func main() {
    userService := UserService{}
    userService.RegisterUser("Ajay Gupta", "AjayGuptaPassword")
    userService.AuthenticateUser("Ravi Verma", "RaviVermaPassword")
}

In this example, the UserService handles both user registration and user authentication. However, this violates the SRP because the type UserService has two distinct responsibilities,

  • RegisterUser

  • AuthenticateUser

Now, let's consider the implications of a change in the user registration logic. Suppose you need to modify the way user registration works, perhaps adding additional validation steps or changing the underlying storage mechanism. If the registration logic is bundled with user authentication, any change to the registration process might also affect the authentication logic. This interdependence makes the code more fragile and harder to maintain.

A very important point to notice is both methods RegisterUser and AuthenticateUser follow the SRP but the type UserService violates the SRP.

When SRP is compliant

A more SRP-compliant approach would involve separating these responsibilities into different types,

  • RegistrationService

  • AuthenticationService

package main

import "fmt"

type User struct {
    Username string
    Password string
}

type RegistrationService struct {
    users []User
}

func (rs *RegistrationService) RegisterUser(username, password string) {
    newUser := User{Username: username, Password: password}
    rs.users = append(rs.users, newUser)
    fmt.Println("User registered:", newUser.Username)
}

type AuthenticationService struct {
    users []User
}

func (as *AuthenticationService) AuthenticateUser(username, password string) bool {
    for _, user := range as.users {
        if user.Username == username && user.Password == password {
            fmt.Println("User authenticated:", username)
            return true
        }
    }
    fmt.Println("Authentication failed for:", username)
    return false
}

func main() {
    registrationService := RegistrationService{}
    registrationService.RegisterUser("Ajay Verma", "AjayVermaPassword")

    authenticationService := AuthenticationService{users: registrationService.users}
    authenticationService.AuthenticateUser("Ravi Bisnoi", "RaviBisnoiPassword")
}

Now, the responsibilities of user registration and authentication are separated into RegistrationService and AuthenticationService types, respectively, adhering to the Single Responsibility Principle.

Summary

In summary:

  1. Initial Example (SRP Violation):

    • UserService handles both user registration and authentication.

    • Changes to registration logic might affect authentication logic, and vice versa.

    • The type has multiple reasons to change, violating SRP.

  2. Refactored Example (SRP Adherence):

    • RegistrationService handles user registration.

    • AuthenticationService handles user authentication.

    • Changes in registration logic won't impact authentication logic and vice versa.

    • Each type has a single responsibility, adhering to SRP.

By adhering to the Single Responsibility Principle, you create code that is more modular, easier to understand, and less prone to unintended consequences when changes are made. This separation of concerns contributes to better maintainability and flexibility in the long run.