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 The Open/Closed Principle?
Like our post on Single Responsibility, let’s start off with the Wikipedia definition of the Open/Closed Principle :
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Probably not that helpful to be honest. It’s sort of an open ended statement that could be taken to mean different things for different developers.
In looking at resources for this article, I actually found many write ups talking about how being able to substitute one implementation of an interface for another was an example of being Open/Closed, and indeed Wikipedia touches on this :
During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other.
I’m not the right person to start redefining decades of research into software design, but I personally don’t like this definition as I feel like it very closely resembles the Liskov Principle (Which we will talk about in our next article in this series). Instead I’ll describe it more loosely as :
Can I extend this code and add more functionality without modifying the original code in any way.
I’m going to take this explanation and run with it.
Open/Closed Principle In Practice
Let’s take our code example that we finished with from the Single Responsibility article. It ended up looking like this :
public class RegisterService { private readonly IUserRepository _userRepository; private readonly IEmailService _emailService; public RegisterService(IUserRepository userRepository, IEmailService emailService) { _userRepository = userRepository; _emailService = emailService; } public void RegisterUser(string username) { if (username == "admin") throw new InvalidOperationException(); _userRepository.Insert(new UserModel()); _emailService.SendEmail(new Email()) } }
Looks OK. But what happens if we want to also check other usernames and reject the registration if they fail? We might end up with a statement like so :
if (username == "admin" || username == "administrator") throw new InvalidOperationException();
Is this just continue on forever? Each time we are actually modifying the logic of our app and we are probably going to have to regression test that past use cases are still valid every time we edit this method.
So is there a way to refactor this class so that we are able to add functionality (Add blacklisted usernames for example), but not have to modify the logic every time?
How about this! We create an interface that provides a list of Blacklisted Usernames :
public interface IRegistrationServiceConfiguration { public List<string> BlacklistedUsernames { get; } }
Then we inject this into our RegisterService class and modify our register method to simply check against this list.
public class RegisterService { private readonly IUserRepository _userRepository; private readonly IEmailService _emailService; private readonly IRegistrationServiceConfiguration _registrationServiceConfiguration; public RegisterService(IUserRepository userRepository, IEmailService emailService, IRegistrationServiceConfiguration registrationServiceConfiguration) { _userRepository = userRepository; _emailService = emailService; _registrationServiceConfiguration = registrationServiceConfiguration; } public void RegisterUser(string username) { if (_registrationServiceConfiguration.BlacklistedUsernames.Contains(username)) throw new InvalidOperationException(); _userRepository.Insert(new UserModel()); _emailService.SendEmail(new Email()) } }
Better right? Not only have we removed having hardcoded strings in our code (which is obviously beneficial), but now when we extend that list out of usernames we don’t want to allow, our code actually doesn’t change at all.
This is the Open/Closed Principle.
Extending Our Example
The thing is, we’ve taken a list of hardcoded strings and made them be more configuration driven. I mean yeah, we are demonstrating the principle correctly, but we are probably also just demonstrating that we aren’t a junior developer. Let’s try and take our example that one step further to make it a bit more “real world”.
What if instead of just checking against a blacklisted set of usernames, we have a set of “pre-registration checks”. These could grow over time and we don’t want to have a huge if/else statement littered through our method. For example changing our code to look like this would be a bad idea :
public void RegisterUser(string username) { if (_registrationServiceConfiguration.BlacklistedUsernames.Contains(username)) throw new InvalidOperationException(); if (_userRepository.CheckUserExists(username)) throw new InvalidOperationException("User already exists"); _userRepository.Insert(new UserModel()); _emailService.SendEmail(new Email()) }
How many statements are we going to add like this? And each time we are subtly changing the logic for this method that requires even more regression testing.
How about this instead, let’s create an interface for registration pre checks and implement it like so :
public interface IRegistrationPreCheck { void Check(string username); } public class RegistrationPreCheckBlacklistedUsernames : IRegistrationPreCheck { private readonly IRegistrationServiceConfiguration _registrationServiceConfiguration; public RegistrationPreCheckBlacklistedUsernames(IRegistrationServiceConfiguration registrationServiceConfiguration) { _registrationServiceConfiguration = registrationServiceConfiguration; } public void Check(string username) { if (_registrationServiceConfiguration.BlacklistedUsernames.Contains(username)) throw new InvalidOperationException(); } }
Now we can modify our RegisterService class to instead take a list of pre checks and simply check all of them in our RegisterUser method :
public class RegisterService { private readonly IUserRepository _userRepository; private readonly IEmailService _emailService; private readonly IEnumerable<IRegistrationPreCheck> _registrationPreChecks; public RegisterService(IUserRepository userRepository, IEmailService emailService, IEnumerable<IRegistrationPreCheck> registrationPreChecks) { _userRepository = userRepository; _emailService = emailService; _registrationPreChecks = registrationPreChecks; } public void RegisterUser(string username) { _registrationPreChecks.ToList().ForEach(x => x.Check(username)); _userRepository.Insert(new UserModel()); _emailService.SendEmail(new Email()) } }
Now if we add another pre-check, instead of changing the logic of our RegisterUser method, we just add it to the list of pre-checks passed into the constructor and there is no modification necessary. This is probably a better example of the Open/Closed Principle that demonstrates the ability to add functionality to a class that doesn’t require changes to the existing logic.
What’s Next?
Up next is the L in SOLID. That is, The Liskov Principle.
Thanks for the interesting article!
One comment, the interface method should contain an argument:
public interface IRegistrationPreCheck
{
void Check(string username);
}
Oof! You are right! Updated.
Hi, so how is the List BlacklistedUsernames initialized in this scenario if it is just a property of an interface? That interface is never implemented only injected. Great article btw
Hi Tony,
So generally speaking what I do is I create an implementation (That’s still just a POCO). And I bind it from configuration as done in Method 2 here. https://dotnetcoretutorials.com/2016/12/26/custom-configuration-sections-asp-net-core/
It’s just a nice simple way to bind configurations sections from your appsettings.json to code.
Hey Wade,
Is the reason for the public interface IRegistrationPreCheck to abstract various checks?
If I follow the flow of things correctly I would have expected a method there called CheckUserExists?
After you implement it you only use the existing functionality of whitelisting users, so it kind of confused me.
How would you generalize the OC principle, find a way to abstract functionality so you do not mess about with a class a lot?
Thanks, great article on the matter.
Sorry, that is a little confusing.
But yes for checking if the user already exists I would simply write another implementation of IRegistrationPreCheck that then checks if the user already exists.
The reason for the interface is so that you can “add” functionality by adding another implementation of the interface, without having to modify any existing code (I guess except add it to the list of checks). But the existing checks are completely independent of each other. And the repository itself, while it does call the checks, it does not know anything about what those checks are and frankly does not care.
So to generalise the principle, it’s about being able to “add” code without having to “modify” code. So in our example we “add” a check, without having to modify any existing checks or the repository itself.
Hi Wade,
How do you then control the flow within the RegisterUser method after one or more check methods in the list of check classes fail?
I assume Insert() and SendEmail() should not be executed.
Can you update the post or give some examples?
Thanks.
Dan.