Refactoring to SOLID - Part 1

by Larry Spencer Friday, March 16, 2012 1:57 PM

I recently gave a 3-half-day seminar at my company on the principles of SOLID development. As an illustration, we refactored an old-style WebForms application to ASP.NET MVC 3 with SOLID practices. In case you have some legacy code to refactor -- or want to avoid writing new code that looks like legacy code -- I offer this short series of posts.

First, to review what's behind the acronym:

S: Single Responsibility Principle -- Each class should have only one reason to change.

O: Open/Closed Principle -- Modules should be open for extension but closed for modification.

L: Liskov Substitution Principle -- A derived class should be able to be used in place of its base class, without surprises.

I: Interface Segregation Principle -- An interface that includes unrelated operations should be broken down into smaller, cohesive interfaces.

D: Dependency Inversion Principle -- Code should depend on abstractions.

Now let's get started. We will be working with a Web application that searches a database for documents that match very simple criteria: first name, last name and account number must start with the characters the user enters. Not wasting any time to make the application look pretty, here is what the Web page looks like after the Search button is pressed.

 

In this before-refactoring version, I have put all the logic in the code-behind for the Search button:

 

protected void BtnSearch_Click(object sender, EventArgs e)
{
    var sbCriteria = new StringBuilder();
    using (var db = new SolidDemoEntities())
    {
        var query = db.Documents.AsQueryable();
        if (!String.IsNullOrWhiteSpace(TxbAccount.Text))
        {
            query = query.Where(d => d.Account.StartsWith(TxbAccount.Text));
            sbCriteria.AppendFormat("Account starts with '{0}' AND ", TxbAccount.Text);
        }

        if (!String.IsNullOrWhiteSpace(TxbLastName.Text))
        {
            query = query.Where(d => d.LastName.StartsWith(TxbLastName.Text));
            sbCriteria.AppendFormat("Last Name starts with '{0}' AND ", TxbLastName.Text);
        }

        if (!String.IsNullOrWhiteSpace(TxbFirstName.Text))
        {
            query = query.Where(d => d.FirstName.StartsWith(TxbFirstName.Text));
            sbCriteria.AppendFormat("First Name starts with '{0}' AND ", TxbFirstName.Text);
        }

        if (sbCriteria.Length > 5)
            sbCriteria.Remove(sbCriteria.Length - 5, 5);
        else
            sbCriteria.Append("-- all records --");

        LblCriteria.Text = sbCriteria.ToString();

        LvwResults.DataSource = query.ToArray();
        LvwResults.DataBind();
    }
}

 

 

Although this style of coding used to be common (and is still common, in some quarters), it is as gross a violation of the Single Responsibility Principle as you're ever going to find. Consider all the responsibilities piled onto one poor button:

  • Parse the user's input.
  • Manage a connection to a database.
  • Build a query to the database -- both as a string and as a LINQ Expression.
  • Execute the query.
  • Bind the results to the form.
  • ...and there's not even any error-handling!

Why is this so bad? All the code is in one place so it's clear, right? Well, it's not so bad as long as the application is this small. However, imagine an application with dozens of Web pages, each with multiple actions. Then some of the drawbacks would become apparent.

  • It's not possible to unit-test the application. It only works when running in the ASP.NET context, plus it expects a database to be present.
  • None of the code can be reused. Wouldn't it be nice if we could reuse the nifty Expression-building aspect?

Worse, suppose we want to add more features (logging the searches to an audit trail, for example). The coding style has been established and chances are the next programmer is just going to glom the new features in the same method.

There is a better way, and I will explore it in subsequent posts. Here's an index to the rest of the series.

Part 2: The Single Responsibility Principle

Part 3: The Open/Closed Principle

Part 4: The Liskov Substitution Principle

Part 5: The Interface Segregation Principle

Part 6: The Dependency Inversion Principle and MVC

Part 7: Dependency Injection in ASP.NET MVC (Poor Man's Version)

Part 8: Dependency Injection in ASP.NET MVC (Unity Version)

Part 9: Using Unity Interception to Implement the Open/Closed Principle

Tags: , ,

All | Composibility | General | Talks

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.