Single Responsibility Principle (SRP) is the first one of the 5 famous principles that make up SOLID, a classic guideline to write clean and maintainable code, worshipped by a plethora of programmers worldwide. There exists various articles and workshops out there to explain the principle. Tech celebs support it, talk about it in social platforms. Many articles, however, does a poor job explaining the principle, resulting in a shallow and rigid understanding of the principle among developers. Uncle Bob, who’s believed to be the first to come up with a name for this principle, did a great job explaining it in his article

Today we’ll be examining SRP again. More specificically, we’re looking at a piece of code that less experienced developers may deem it a violation of SRP, but it’s not. The example is written in Go, but there’s no need to have prior Go knowledge as it’s pretty self-explanatory.

We’re developing a promotion component, and one of the features is to reward our users. The reason for rewarding can be thanks to user’s loyalty, or to gain more acquisition, but for the sake of simplicity we will assume it’s not in the scope of the promotion component. We can also assume that there’s another component where User type is defined, like this

// user component
type User struct {
	// Point is granted based on user's activity
	Point int
}

func (u User) IsGuest() bool {
	// determine if user is logged in or not 
}

In the promotion component, a system to rank user by their tiers, based on their Point, is implemented

// promotion component
type tier int

const (
	bronze tier = iota
	silver
	gold
	platinum
)

func userTier(u User) tier {
	if u.IsGuest() {
		return bronze
	}

	if u.Point > 1000 {
		return platinum
	}

	if u.Point > 500 {
		return gold
	}
	
	if u.Point > 100 {
		return silver
	}

	return bronze
}

func Reward(u User) string {
	t := userTier(u)

	switch t {
	case bronze:
		return "coke"
	case silver:
		return "beer"
	case gold:
		return "quesadilla"
	case platinum:
		return "pizza"
	default:
		return "biscuit"
	}
}

We define type tier to denote the different tiers to categorize a user. Then we define 4 constants to illustrate the 4 tiers that we can support. We also implement the function userTier to return the correct tier based on how many Pointthe user has. Finally, our component exposes its public interface: function Reward. Note that I’m using string type, again, for the sake of simplicty. In a real world code base, prize will probably have its own type with much more complexity.

A developer could read the code above, thinking that userTier violates SRP. One argument is that the function has some logic related to user being logged in or not, and some other logics related to user’s number of points, which is a misconception of SRP. Many developers think SRP means “a function/module should do one thing”, but they don’t know how that thing is defined, and thus relying on their instinct instead of being methodical and pragmatic. Perhaps we should take a step back, and examine the definition of SRP, and dig deeper into Uncle Bob’s explanation. It may shed some light on this.

The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change

It’s one reason to change, not one thing. So what is a reason to change? According to Uncle Bob, there is a coupling between “reason to change” and “responsibility”. The question that must be answered here is: who is the component responsible to, or who must the design of the component respond to.

And this gets to the crux of the Single Responsibility Principle. This principle is about people.

In the promotion component, userTier must respond to the tier algorithm designer. If the designer wants to change the default tier of a guest user, userTier must be changed. If the designer wants to have a different distribution of user points among those tiers, userTier must be changed. If the designer wants another tier added, userTier must be changed. If the mapping between rewards and user tiers change, userTier is not affected. If the people handling the user component changes their algorithm to validate a guest user, based on a bearer token instead of username and password for instance, then userTier remains intact. userTier, in the context of the promotion component, has a very well-defined responsibility, and reacts to the changes of only 1 person: the tier algorithm designer. And this also brings us to another perspective of SRP.

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

All changes made in the user tier algorithm will be reflected in userTier, making the user tier a cohesive piece of business logic, and de-couple others from it.

To sum up, by examining carefully what SRP really means, and by understanding the context of promotion component, we are able to demystify a seemingly violation of SRP. We also come up with 2 heuristics to determine SRP in a larger context:

  • What/Who does our component/function respond to?
  • Does our component/function changes come from the same source?