Understanding SOLID: 
Single responsibility principle

Understanding SOLID: Single responsibility principle

Meet John, again! 👋🏼 , John was asked to implement a login screen, and the TL asked him to create a Login screen with email validation, providing him with some validation rules:

  • Email should end with @gmail.
  • Email should not include special characters.

John went directly to implementation🚀, let’s see what he have for us 🧐:

class LoginScreen(val loginViewModel: LoginViewModel): Fragment() {
        /* UI initialization code */
    fun login() {
        loginViewModel.login(userEmailTextView.text, userPasswordTextView.text)
    }
}

class LoginViewModel(private val loginService: LoginService): ViewModel() {
    fun login(userEmail: String, userPassword: String) {
        if (!userEmail.contains("@gmail") || userEmail.contains(Regex("[!#$%^&*(){}]"))) {
            Log.e("$userEmail is invalide email address")
            throw IllegalArgumentException("$userEmail is invalid")
        }
        Log.i("User is logging with $userEmail")
        loginService.login(userEmail, userPassword)
    }
}

Let’s understand what John has done,

1- He created a screen depending on LoginViewModel

2- Then he creates a LoginViewModel with login() function

3- Thelogin() function has 3 tasks to do, first validating the email address with some validation rules, second if valid, will delegate to LoginService to launch the login request, and the third log the login flow.

What is Wrong with that?

What if we need to change the validation rules? what if we need to change the way we log the login flow? is login() function responsible for validation and logging?

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

Any class has one or more responsibilities, whenever a new change is requested, you will open this class and modify it as required, so the responsibility is a reason to open a class and change it to a new responsibility or modify it, and that’s what it mean a responsibility is a reason to change.

in John’s case, the login() has three responsibilities (Logging, Email validation, Login),

  • if we need to change the way we log things, John has to go to login() and change it according to the request.
  • in case the business has its own domain and decides to validate the email address to match his domain (@mybusiness.com), John has to go and change the validation rule.

As we don’t need to change code over and over again, we should minimize the number of responsibilities for each component (class, function, module), to minimize the reasons to change components over time.

Let’s refactor 🛠️

First, let’s Separate concerns, we can split the responsibilities into multiple collaborative classes, each with its responsibility.

class LoginViewModel(
    private val loginService: LoginService,       // responsible for login
    private val validator: ValidatorInterface,    // responsible for validation
    private val logger: LoggerInterface           // resonsible for logging
) : ViewModel() {

    fun login(userEmail: String, userPassword: String) {
        validator.validateEmail(userEmail)
        logger.log("User is logging with $userEmail")
        loginService.login(userEmail, userPassword)
    }
}

// different directory
class FileLogger: LoggerInterface {
    fun log(message: String) {
        // log into file
    }
}

class CommonValidator(private val logger:LoggerInterface): ValidatorInterface {
    fun validateEmail(email: String): Boolean {
        if (email.isBlank() || email.contains(Regex("[!@#$%^&*(){}]"))) {
            logger.log("$email is not valid")
            return false
        }
        return true
    }
}

Here we separate the responsibility of logging and validation into a Collaborative classes, one for logging and one for common validations, hence, each will have one Reason to change.

What is the impact of applying SRP

  • Easy to understand: the project becomes easier to understand, and each component can demonstrate its purpose and its responsibility.
  • Flexibility: if we separate each component concern, then we can create components that are flexible enough, for example, we can create multiple instances of the LoggerInterface each with a different way of logging, and still the LoginViewModel does NOT know the difference.

      class LocalLogger: LoggerInterface {..}
      class ExternalLogger: LoggerInterface {...}
    
  • Reusability: when we separate the email validation logic into another class, we can pass some sort of validation rules and we can reuse the validation function in multiple scenarios as we have done by accepting an instance of LoggerInterface in CommonValidator class, and we can refactor CommonValidator to make it more reusable:

    class CommonValidator: ValidatorInterface {
     override fun validateEmail(email: String, validationRules: List<ValidationRule>): Boolean {
         validationRules.forEach { 
             if (!it.applyRule(email)) return false
         }
         return true
      }
    }
    

    instead of statically putting conditions for validation, we can pass the exact validation rule through a function parameter, or class constructor, and it was easy to enhance it because the validation is separated into its own component, we can configure and enhance it as we want 👍🏼.

Single Responsibility violation:

The principle is easy to understand, but easy to break, as it’s easy to add a new function instead of creating a new class , after some time, you will have a huge class, hard to maintain, and not reusable, I can assume while you reading this article, you think about a FAT class you want to refactor it, let’s see how we can catch SRP violation:

  • Too many instance variables inside a class.
  • Too many public methods.
  • Each method uses a different instance variable
  • Specific tasks are delegated to private methods

when you discover these symptoms try to extract them into “collaborator” classes, then delegate some of the class responsibilities to them.

Conclusion

The single responsibility principle is about improving the code quality and making the project easier to maintain, based on the business request, each component should define its purpose and its responsibility, by following SRP you will decouple your component as a side effect, by separating concerns you don’t have many dependencies in your component, then you have decoupled components.

Peace ✌🏼