In a previous edition of the DNC Magazine, I covered some basics of Object Oriented Programming (OOP).
As a quick review, I discussed different types of inheritance, polymorphism, encapsulation, loose coupling, and tight cohesion. Now I want to dive deeper into good OOP techniques and begin a discussion on SOLID.
First introduced by Robert “Uncle Bob” Martin, SOLID is not new.
Uncle Bob simply took concepts that had been around for years and put them together. However, he didn’t have them in SOLID order. We can credit Michael Feathers for coming up with the SOLID acronym.
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.
Over the next five issues, I’ll cover each one of these concepts. While originally targeting OOP, many of these concepts apply to non-OOP languages as well.
This article is published from the DNC Magazine for .NET Developers and Architects. Subscribe to this magazine for FREE and download all previous, current and upcoming editions
Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) states that a class should do one thing and one thing only.
After years of working with OOP code, I’ve found that many developers violate this principle all the time.
Yes, we write classes and methods, but we tend to write one big method that does something in a procedural manner rather than having smaller classes that do one thing.
Here’s some typical code that demonstrates this.
public class CsvFileProcessor
{
public void Process(string filename)
{
TextReader tr = new StreamReader(filename);
tr.ReadToEnd();
tr.Close();
var conn = new SqlConnection("server=(local);integrated security=sspi;database=SRP");
conn.Open();
string[] lines = tr.ToString().Split(new string[] {@"\r\l"}, StringSplitOptions.RemoveEmptyEntries);
foreach( string line in lines)
{
string[] columns = line.Split(new string[] {","}, StringSplitOptions.RemoveEmptyEntries);
var command = conn.CreateCommand();
command.CommandText = "INSERT INTO People (FirstName, LastName, Email) VALUES (@FirstName, @LastName, @Email)";
command.Parameters.AddWithValue("@FirstName", columns[0]);
command.Parameters.AddWithValue("@LastName", columns[1]);
command.Parameters.AddWithValue("@Email", columns[2]);
command.ExecuteNonQuery();
}
conn.Close();
}
}
How many things is this class doing? One? Two? Three? More?
You may be tempted to say one. That is, the class processes a CSV file.
Look at this class another way. How would you unit test this?
It wouldn’t be easy.
What if you had other things like data validation and error logging? How would you unit test it then?
The truth is, this class is doing three things:
1. Reading a CSV file
2. Parsing the CSV file
3. Storing the data
Doing lots of things in a class is bad not just because it is difficult to unit test, but it increases the odds of introducing bugs.
If you change the code in the Parsing section, and you add a bug, then Reading and Storing are also broken. And, because unit tests will not exist or are very complex, it also takes longer to track down and fix the bug.
Applying Single Responsibility Principle
In order to fix this, we need to break down the code into the individual pieces.
You may be thinking you can just have three methods, one for each piece of functionality. But go back to the definition of SRP. It says that a class should have only one purpose.
So, we need three classes to do the work. Alright, we’ll actually have more as you’ll see in a moment.
The way to fix this code is through code refactoring. Initially, we’ll put each piece of functionality into its own method.
public class CsvFileProcessor
{
public void Process(string filename)
{
var csvData = ReadCsv(filename);
var parsedData = ParseCsv(csvData);
StoreCsvData(parsedData);
}
public string ReadCsv(string filename)
{
TextReader tr = new StreamReader(filename);
tr.ReadToEnd();
tr.Close();
return tr.ToString();
}
public string[] ParseCsv(string csvData)
{
return csvData.ToString().Split(new string[] { @"\r\l" }, StringSplitOptions.RemoveEmptyEntries);
}
public void StoreCsvData(string[] csvData)
{
var conn = new SqlConnection("server=(local);integrated security=sspi;database=SRP");
conn.Open();
foreach (string line in csvData)
{
string[] columns = line.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
var command = conn.CreateCommand();
command.CommandText = "INSERT INTO People (FirstName, LastName, Email) VALUES (@FirstName, @LastName, @Email)";
command.Parameters.AddWithValue("@FirstName", columns[0]);
command.Parameters.AddWithValue("@LastName", columns[1]);
command.Parameters.AddWithValue("@Email", columns[2]);
command.ExecuteNonQuery();
}
conn.Close();
}
}
As you can see, things still aren’t quite right.
We’re parsing the CSV file into rows in the ParseCsv() method, but additional parsing is happening in the StoreCsvData() method to get each row into columns.
The way to fix that is with a ContactDTO that stores the data from each row.
The next step is to add the DTO, but I’ll skip a step and also break out each method into its own class.
But I’m going to think ahead here too. What if the data doesn’t come in as CSV? What it its XML or JSON or something else?
You solve this with interfaces.
public interface IContactDataProvider
{
string Read();
}
public interface IContactParser
{
IList<ContactDTO> Parse(string contactList);
}
public interface IContactWriter
{
void Write(IList<ContactDTO> contactData);
}
public class ContactProcessor
{
public void Process(IContactDataProvider cdp, IContactParser cp, IContactWriter cw)
{
var providedData = cdp.Read();
var parsedData = cp.Parse(providedData);
cw.Write(parsedData);
}
}
public class CSVContactDataProvider : IContactDataProvider
{
private readonly string _filename;
public CSVContactDataProvider(string filename)
{
_filename = filename;
}
public string Read()
{
TextReader tr = new StreamReader(_filename);
tr.ReadToEnd();
tr.Close();
return tr.ToString();
}
}
public class CSVContactParser : IContactParser
{
public IList<ContactDTO> Parse(string csvData)
{
IList<ContactDTO> contacts = new List<ContactDTO>();
string[] lines = csvData.Split(new string[] { @"\r\l" }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
string[] columns = line.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
var contact = new ContactDTO
{
FirstName = columns[0],
LastName = columns[1],
Email = columns[2]
};
contacts.Add(contact);
}
return contacts;
}
}
public class ADOContactWriter : IContactWriter
{
public void Write(IList<ContactDTO> contacts)
{
var conn = new SqlConnection("server=(local);integrated security=sspi;database=SRP");
conn.Open();
foreach (var contact in contacts)
{
var command = conn.CreateCommand();
command.CommandText = "INSERT INTO People (FirstName, LastName, Email) VALUES (@FirstName, @LastName, @Email)";
command.Parameters.AddWithValue("@FirstName", contact.FirstName);
command.Parameters.AddWithValue("@LastName", contact.LastName);
command.Parameters.AddWithValue("@Email", contact.Email);
command.ExecuteNonQuery();
}
conn.Close();
}
}
public class ContactDTO
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
We’re using generic method names of Read, Parse, and Write because we don’t know what type of data we’ll get.
Now we can easily unit test this code.
We can also easily modify the Parse code and if we introduce a new bug, it won’t affect the Read and Write code.
Another bonus is that we’ve loosely coupled the implementation.
So, there you have it. We took what is fairly common procedural code and refactored it using the Single Responsibility Principle.
Next time you look at a class, ask yourself if you can refactor it to use SRP. Applying the S of SOLID will help your code to be green, lush, and vibrant, and you’re on your way to having a software garden.
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 in What is Software Gardening.
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.