Last year I finished up a long series on SOLID principles.
So, what is SOLID? Well, it is five OOP principles, the first letter of each spelling out SOLID: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
These principles, when combined together, make it easy to develop maintainable and extensible software.
Here at DNC Magazine, we’ve had a couple of readers send questions about SOLID. This issue’s column is my attempt to answer the questions.
Before getting to the questions, I want to summarize SOLID principles.
Now, on to the questions.
SOLID for Beginners
The first question is from Eric, who writes,
“I am a Computer Science Graduate who has learnt C#. Can you explain me SOLID (like for beginners)”?
Great question, Eric. Coming out of school with a shiny degree, one can quickly realize they were taught to write code, but not taught to write software. Schools typically teach how to do things in Java, JavaScript, C#, Python and other languages but neglect the important things such as DevOps, Agile, SOLID, and other practices.
SOLID is often difficult to understand for new developers, but yet one of the most important concepts you can understand. Where I work at Quicken Loans, we typically ask about SOLID when interviewing hiring candidates and some of them cannot give a satisfactory answer.
The primary thing to ask yourself when writing code is, “Does this solution solve the problem?” If you can say yes, the code is correct.
However, it may not answer other questions you need to keep in mind. These are things like:
- Does the code have adequate safe guards against invalid data?
- Is the code readable by a human?
- Is it maintainable?
- Can the code be easily extended when needs change?
- Is this code secure?
- Does the code do too much? Should I break it down into smaller pieces?
Many of these questions can be answered by applying SOLID.
Let’s review SOLID.
SOLID has been around for years. The concepts were first brought together by Robert “Uncle Bob” Martin. They are a set of concepts that help us create better code.
You shouldn’t think that code will comply with every SOLID principle the moment it leaves your fingers and enters the computer via the keyboard.
Some principles are more applicable to interfaces, some to classes.
And, once the code is originally written, you will typically refactor it before you are satisfied that is as close to SOLID as you can get it.
When learning SOLID, learn one principle at a time, learn it well, then move on to the next one.
Single Responsibility Principle
Single Responsibility Principle (SRP) answers many of the above questions.
First, it can identify if code does too much. The code should do one thing and one thing only and do it well. And if code does one thing, it’s easier to understand when you read it. It’s also less likely that you’ll have to modify the code and if you do, odds are that unexpected side effects won’t creep in.
But, it isn’t always clear what one thing code should be doing.
Here’s an example. For many years, developers have been creating data classes. These would be used to Create, Read, Update, and Delete (CRUD) data.
One day, someone proposed that Create, Update, and Delete are all similar in that they change data in the database.
Read is a different animal.
It doesn’t change data, but simply tells us what the data is, therefore, following SRP, it should be in a different class. After all, they said, it’s far more likely we’ll have to change code to read the data for different purposes than to modify the data.
We may need a single row, multiple rows, summarized data, data sorted in different ways, totals, subtotals, counts, groupings, etc. This led to a concept called Command Query Responsibility Separation (CQRS).
Others counteracted this by saying that in the end, it’s all data access of some kind, so it all belongs in the same class.
Which one is correct?
The answer is, both are correct. CQRS may add unneeded complexity.
As a general rule, if you have to modify code in two or more places, either in the same class or multiple classes, to apply a single modification (fix a bug or add new functionality) that section of code MIGHT be a candidate for multiple classes, hence making it compliant with SRP.
What if it’s only a single line of code that you move to a new class? Is the additional complexity worth it? Only you and your team can decide this.
Here's a detailed tutorial on Single Responsibility Principle (SRP).
Open-Closed Principle
The Open-Closed Principle is all about adding new functionality without modifying the existing code or even assembly. The reason behind this is that every time you modify code, you run the risk of adding bugs to the existing functionality.
Think about the String class in .NET.
There are many operations that you can perform on a string. You have string.Length, string.Split, string.Substring and many others. What if there is a need to reverse all the characters in a string, so that instead of “Dot Net Curry”, you wanted, “yrruC teN, toD”?
If the string class is modified, there is a chance (albeit very small) that existing code can get accidentally changed. Instead, create an extension method that lives in a different assembly. BAM! Done.
Here's a detailed tutorial on Open-Closed Principle (OCP).
Liskov Substitution Principle
Now, let’s look at an oddly named principle, Liskov Substitution Principle (LSP), named after the person who first proposed it.
What it says is that we should be able to replace a class we’re using with a subclass, and the code keeps working.
A common example here is a logging class. Think of all the places you can write a log. It could be to a database, a file on disk, a web service, use the Windows Event Log, and others. The file on disk can take many forms. It could be plain text, XML, JSON, HTML, and many other formats.
But the code that calls the logging methods should not have to call different methods for different ways to log. It should always call a similarly named method for all of them and a specific instance of the logging class handles the details.
You’d have a different class for each way you support for writing a log.
Read more in a detailed tutorial on Liskov Substitution Principle (LSP).
Interface Segregation Principle
Have you ever looked at the different interfaces in the List class in .Net?
I have.
List inherits from eight different interfaces. Here’s the definition from MSDN.
Why does it do this? The simple reason is to comply with the fourth SOLID principle, the Interface Segregation Principle (ISP).
Think about the functionality defined in each of those interfaces? Now imagine if all that functionality was contained in a single interface. How complex would that be? How many different things would this single, combined interface do? It would be crazy! It would be impossible to properly maintain.
And then imagine if you had a class that didn’t need to implement IReadOnlyList. How would you implement that? Basically, you’d have code that didn’t do anything. Why do you have code that does nothing?
The solution is to have many specialized interfaces so that consumers depend only on what they need.
To delve further into ISP, here's a detailed tutorial on Interface Segregation Principle (ISP).
Dependency Inversion Principle
Let’s return to the logging example above. How do you tell a specific class which type of logging it should use? That’s the job of the Dependency Inversion Principle or as some call it Dependency Injection Principle.
When you instantiate the class, you pass in an instance of the class you want to use through Constructor Injection.
The following code shows how to do this.
public interface ILogger
{
void WriteLog(string message);
}
public class DatabaseLogger : ILogger
{
public void WriteLog(string message)
{
// Format message
// Build parameter
// Open connection
// Send command
// Close connection
}
}
public class TextFileLogger : ILogger
{
public void WriteLog(string message)
{
// Format message
// Open text file
// Write message
// Close text file
}
}
public class Main
{
public Main()
{
var processor = new Processor(new TextFileLogger());
}
}
public class Processor
{
private readonly ILogger _logger;
public Processor(ILogger logger)
{
_logger = logger;
}
public void RunProcessing()
{
try
{
// Do the processing
}
catch (Exception ex)
{
_logger.WriteLog(ex.Message);
}
}
}
At the top, the interface is defined followed by two classes, DatabaseLogger and TextFile logger that implement the interface.
In the Main class, an instance of the TextFileLogger is created and passed to the constructor of the Processor class. This is Dependency Injection. The Processor class depends on an instance of ILogger and instead of creating it every time, the instance is injected into the constructor. This also loosely couples the specific logger instance from the Processor class.
The instance of the logger is then used inside the Try/Catch block. If you want to change the logger type, all you need to do is change the calling program so that you instantiate DatabaseLogger and send it instead.
Further Reading - Dependency Inversion Principle (DIP).
Final thoughts on learning SOLID
And there you have a simplified explanation of SOLID. I encourage you to look at my earlier columns on this topic. All are linked to above. You may benefit from finding a senior level developer on your team and using him as a mentor. Mention to him that you’re interested in learning SOLID. That will help the learning curve and give you someone to rely on to help you in your early career.
Now, onto the second question.
To SOLID or not to SOLID? That is the Question
With apologies to William Shakespeare, let’s look at the second question.
Adam asks, “When should I use SOLID and when should I not use SOLID?”
Well, Adam, the easy answer is always use SOLID, but as I discussed above, it doesn’t always make sense. Sometimes it adds complexity.
The real answer is well, more complex.
Overly complex applications are an ugly side of software development.
I’ve seen them over the years. I’ve tried to fix them.
Sometimes the complexity is not in the code, but in the architectural directions taken in the application. Bad architecture is often impossible to fix without a complete rewrite.
While SOLID is mostly a coding concept, there is an architectural aspect. Should you split something into multiple classes? How many interfaces should you use?
This list goes on.
Sometimes, the application of SOLID comes along as you design what the class is doing. See the above discussion on List. As the designers were defining the functionality of the class, the interfaces began to take shape and they learned which ones needed to be implemented.
In my work, once I have the code working correctly, I may do additional refactoring to follow a specific Design Pattern or to make the code more self-documenting. This is also a good time to look at applying some SOLID.
Asking questions like, “Does this class do too much and if it does, will I add complexity with multiple classes?” need to be asked. You may need to discuss with your team members as it may not be obvious.
Take the DatabaseLogger implementation above. There are five steps listed in the WriteLog method. Should each of those be a separate method? Should it be relegated to a different class? Or, should it all stay in that method?
The answer to each of these questions is, “Maybe.”
You may also find that the same SOLID questions come into play when modifying a class, either by adding new functionality (most likely SOLID comes into play) or fixing a bug (maybe SOLID should be used).
There’s another situation when you may not use SOLID and that’s when you’re time restrained. Alright, I accept that every project is time constrained, but there are times you just need to get the code out and other times when you can “do it right”. If you’re seriously time constrained, adding the technical debt of not using SOLID when you should may be acceptable.
But as with all technical debt, you should have a plan to pay it off.
One final place where SOLID won’t come into play is with prototypes and throw-away code. These are both places where you don’t plan to put the code into production and it will be removed soon. Don’t spend the time worrying about SOLID here.
Now, having said all this, as you gain experience, you start to see how to use SOLID earlier in the architectural and code writing process. Not all the time. You’ll still have programming puzzles that require you to refactor to SOLID, but because of accumulated experience, you should be able to complete the refactoring more quickly and with better results.
Conclusion
As with many software development concepts, SOLID has a place in your toolbox. It has a learning curve as steep as there is, but it’s one to master, not only learning the principles but when to use them and when not to use them. And by understanding SOLID and using it properly your software can be green, lush, and growing.
About Software Gardening
Comparing software development to constructing a building says that software is solid and difficult to change. Instead, we should compare software development to gardening as a garden changes all the time. Software Gardening embraces practices and tools that help you create the best possible garden for your software, allowing it to grow and change with less effort.
Learn more about Software Gardening.
This article was technically reviewed by Yacoub Massad.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Craig Berntson is a software architect specializing in breaking up the monolith and improving developer teams and processes. He is the author of two books on software development and has been a speaker at conferences across North America and Europe. He received the Microsoft MVP award twenty-two straight years. He lives in Salt Lake City, Utah.