Short post tonight, as I things have been pretty crazy lately. I've been working on several projects at once with them all approaching their final delivery dates. Something that is always a fun situation to find yourself in. But a saying I've learned to live by is...busy = employed.
But I digress, Lately I've been working on a lot of modernizations, and a lot of refactoring, and I've noticed a tendency by younger developers to make the following mistakes and when architecting solutions. In many cases they tend to try to follow best practices, but fall short when it comes to the follow through, so I thought tonight might be a good time to talk about general SOLID principles. Now its my experience that many people at least "sort of" follow SOLID. And I say sort of because people tend to follow it by reflex and might not even know that they are. So now might be a good time to talk through each part of Solid and identify what they mean for your reference.
Now its worth mentioning at this stage of the game, that you really need to consider the size, scope, and needs of your project before you go through some of this. SOLID principles are important, but there are varying degrees to which they can be implemented. During every design, you as the developer need to weigh how far you want to take these to provide value to the business. The point of solid is to provide overall value to the business through software and create better software. Not to make HUGE amounts of work that make the software never become a reality.
S = Single Responsibility Objects This first item I've found is the most easily identified part of SOLID, but its also the most ignored. The idea behind Single Responsibility objects is that your classes should server a single purpose only. And you should not be building these large complex classes but rather building a series of smaller classes each designed for a very specific job. For example, lets say you have the need to have the following functionality:
- When a new user is created, they should have a password randomly generated for them.
- Those passwords should be encrypted before being stored in the database.
- The user should have the ability to generate a random password if they forget theirs.
Now some people would look at that and say...create a user's class with a RegisterUser method, and a ResetPassword method. Possibly breaking out reusable logic for private methods within. Nice and encapsulated, and great right. There is nothing wrong with this approach. But it does not follow SOLID, and is creating a scenario that is very inflexible.
The problem is that we are mixing business rules, with functional rules, and there are actually several things happening here.
- Business Rules associated with creating a user.
- Business rules for creating a password
- functional logic for handling encryption
One problem with the above approach is that by encapsulating this in a single class you are creating a scenario where you are becoming tightly coupled to the "how", rather than focusing on it separate from the "why".
What I mean by that is, for encryption let's say you use RjndealManaged in your Users class, and you build methods for randomly generating your passwords. Seems reasonable right?
Problem is that 6 months from now, the business designed to move their user management to Active Directory and they ask you to update your application to work with it. You are not looking at completely refactoring this class to function with the new system, and you run the risk of "throwing out the baby with the bath water". All the business rules for your app haven't changed, just how it does the operations, and there is now a high likelihood that you will risk damaging the business rules in your changes.
Under SOLID, one approach to the above might have been the following:
- UserRepository: A class designed for interacting with the database to save the user details. This could for the sake of argument be a facade around Entity Framework or another data access technology.
- EncryptionProvider: A class that utilizes an "Encrypt" and "Decrypt" method to handle all encryption.
- PasswordProvider: Handles the generation of a random password for use in the registration or reset password process.
- UserProvider: A class that injects dependencies from the previous 4 to account for business logic.
This approach has you building much smaller and reusable pieces that are designed to be easily replaced. For example, if your company decides that instead of storing users in the database, they want you to talk to an LDAP provider to ADFS, you just have to build a new UserRepository that handles the interaction. This allows the other classes to remain untouched, which means that they don't require as much testing. If they decide to change the logic of generating passwords, its an update to a single purpose class that allows you to continue.
Open/Closed Principle This is another one that I find is pretty easy to understand when you look at it. But many developers abuse this one, and tend to make exceptions that can be quite costly.
The Open/Closed Principle, says that all classes should be open to extension but closed to modification. But what does that mean? The idea is this, you should never go in an modify an existing class whenever possible. If the business rules change, your first instinct should be to create a new class, and extend the functionality of the old. The idea being that you could create a new class, that inherits from the old class, and override the methods where necessary.
Honestly this principle in my opinion only works in scenarios where your dealing with Dependency Injection. Let's look back at our example from the "S" principle. Our requirement was to original build authentication that worked directly with the applications database. If we were to use the single larger class approach, then this would create a scenario where we would create a new user class, and override the appropriate methods to accommodate the new functionality.
A good example of where this could be used would be a single configuration class. Let's say with have a configuration class that reaches into a single database table, and takes in a key, and returns the configuration setting. Pretty basic overall, nothing to fancy. But we notice that our database server is getting hammer pretty hard, and that's partly because all of our configuration settings are making a lot of round trips across the network. Not exactly ideal.
Now I know lots of developers, who would say let's just update our ConfigurationProvider, to use the server cache, and that's a great idea. They would then say, let's just go update our methods used for retrieval and problem solved. This is the point you left SOLID behind.
For the Open/Closed principle, we would instead create a new class called "CacheConfigurationProvider" and make it inherit from the ConfigurationProvider. In this new provider, we override the method, and implement the cached functionality. And then tell our DI container to utilize the new class when resolving the interface.
What does this buy us? Again its a case of not throwing out the baby with the bath water. We keep the tested parts of the existing class and only make modifications where necessary. Additionally should we ever be in a scenario where we need to leverage the original functionality (say a mobile device) we aren't having to rip out code.
Additionally, we are focusing on the Single Responsibility principle as the CacheConfigurationProvider only focuses on implementing the logic to talk to the Server cache, and not the database.
I think that's good for tonight, but later this week, I'll upload another post talking about the LI parts of the SOLID. Those parts are:
- Liskov Substitution Principle
- Interface Segregation Principle
Till next time, same Byte time, same byte channel!