DotNetCurry Logo

Single Responsibility Principle (C#)

Posted by: Craig Berntson , on 7/3/2015, in Category Software Gardening
Views: 26963
Abstract: The Single Responsibility Principle (SRP) states that a class should do one thing and one thing only. Learn about this principle in this article.

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.

Single Responsibility Principle

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.

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Craig Berntson works for one of the largest mortgage companies in the US where he specializes in middleware development and helping teams get better. He has spoken at developer events across the US, Canada, and Europe for over 20 years and is a Grape City Community Influencer. Craig is the coauthor of 'Continuous Integration in .NET' available from Manning. He has been a Microsoft MVP since 1996. Craig lives in Salt Lake City, Utah. Email: dnc@craigberntson.com Twitter: @craigber.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!