This article is part of a series on the SOLID design principles. You can start here or jump around using the links below!
S – Single Responsibility
O – Open/Closed Principle
L – Liskov Substitution Principle
I – Interface Segregation Principle
D – Dependency Inversion
What Is SOLID?
SOLID is a set of 5 design principles in software engineering intended to make software development more flexible, easier to maintain, and easier to understand. While sometimes seen as a “read up on this before my job interview” type of topic, SOLID actually has some great fundamentals for software design that I think is applicable at all levels. For example, I commonly tell my Intermediate developers that when doing a code review, don’t think about “How would I implement this?” but instead “Does this follow SOLID principles?”. Not that things are ever as black and white as that, but it forms a good starting point for looking at software design objectively.
Like all “patterns” and “principle” type articles. Nothing should be taken as being “do this or else”, but hopefully throughout this series we can take a look at the SOLID principles at a high level, and you can then take that and see how it might apply to your day to day programming life. First up is the “Single Responsibility Principle”.
What Is Single Responsibility?
I remember reading a definition of Single Responsibility as being :
A class should have only one reason to change
And all I could think was… And what is that reason? Was the quote cut off? Was there more to this?
And I don’t think I’m alone. I feel like this definition is a somewhat backwards way of saying what we probably all intuitively think of when we see “Single Responsibility”. But once we explore the full picture, it’s actually quite a succinct way of describing the principle. Let’s dive right in.
Single Responsibility In Practice
If we can, let’s ignore the above definition for a moment and look at how the single responsibility principle would actually work in practice.
Say I have a class that registers a new user on my website. The service might look a bit like so :
public class RegisterService { public void RegisterUser(string username) { if (username == "admin") throw new InvalidOperationException(); SqlConnection connection = new SqlConnection(); connection.Open(); SqlCommand command = new SqlCommand("INSERT INTO [...]");//Insert user into database. SmtpClient client = new SmtpClient("smtp.myhost.com"); client.Send(new MailMessage()); //Send a welcome email. } }
Pretty simple :
- Check to see if the user is an admin, if it is throw an exception.
- Open a connection to the database and insert that user into it.
- Send a welcome email.
How does this violate the single responsibility principle? Well while theoretically this is doing “one” thing in that it registers a user, it’s also handling various parts of that process at a low level.
- It’s controlling how we connect to a database, opening a database connection, and the SQL statement that goes along with it.
- It’s controlling how the email is sent. Specifically that we are using a direct connection to an SMTP server and not using a third party API or similar to send an email.
It can become a bit of a “splitting hairs” type argument and I’ve intentionally left the logic statement for checking the username in there as a bit of a red herring. I mean theoretically you could get to the point where you go One Class -> One Method -> One Line. So you have to use your judgement and in my opinion, the admin check there is fine (But feel free to disagree!).
Another way to look at it is the saying “Are we doing one thing only, and doing that one thing well”. Well we are directing the flow of registering a user well, but we are also doing a bunch of database/SMTP work mixed in with it that frankly we probably aren’t doing “well”.
A Class Should Have Only One Reason To Change
Let’s get back to our original definition.
A class should have only one reason to change
If we take the above code, what things could come up that would cause us to have to rewrite/change this code?
- The flow of registering a user changes
- The restricted usernames change
- The database schema/connection details change (Connection String, Table Schema, Statement changes to a Stored Proc etc)
- We stop using a SQL database all together for storing users and instead use a hosted solution (Auth0, Azure B2C etc)
- Our SMTP/Email details change (From Email, Subject, Email contents, SMTP server etc)
- We use a third party API to send emails (Sendgrid for example)
So as we can see, there are things that could change that have absolutely nothing about registering a user. If we move to using a different method of sending emails, we are going to edit the “RegisterUser” method? Doesn’t make sense right!
While the statement of having only “one” reason to change might be extreme, we should work towards abstracting code so that changes unrelated to our core responsibility of registering users affects minimal change to our code. The only reason why our class should change is because the way in which users register changes. But changes to how we send emails or which database we decide to use shouldn’t change the RegisterService at all.
With that in mind, what we would typically end up with is something like so :
public void RegisterUser(string username) { if (username == "admin") throw new InvalidOperationException(); _userRepository.Insert(...); _emailService.Send(...); }
You probably already have code like this so it’s not going to be ground breaking stuff. But having split out code into separate classes, we are now on our way to abiding by the statement “A class should only have one reason to change”.
What Does Single Responsibility Give Us?
It’s all well and good throwing out names of patterns and principles, but what is the actual effect of Single Responsibility?
- Code being broken apart with each class having a single purpose makes code easier to maintain, easier to read, and easier to modify.
- Unit testing code suddenly becomes easier when a class is trying to only do one thing.
- A side effect of this means that code re-useability jumps up as you are splitting code into succinct blocks that other classes can make use of.
- The complexity of a single class often goes drastically down because you only have to worry about “one” piece of logic (For example how a user is registered, rather than also worrying about how an email is sent).
Simply put, splitting code up into smaller chunks where each piece just focuses on the one thing it’s supposed to be doing makes a developers life a hell of a lot easier.
What’s Next?
Up next is the O in SOLID. That is, The Open/Closed Principle.
Great article
still violating SRP because your method still have more than one responsibility
1. checking for user and thrown exception
2.register user
if your requirements change for user allowed for register method, so you have one reason to change it. futhermore, this code is also violates Open-Closed principle because of adding new allowed user for register method cause changing of source code
It certainly does violate the Open/Closed principle which is why the next article in the series looks at exactly that! https://dotnetcoretutorials.com/2019/10/18/solid-in-c-open-closed-principle/