Refactoring to SOLID - Part 2: The Single Responsibility Principle

by Larry Spencer Monday, March 19, 2012 7:37 PM

In the last post, we saw an old-style Web application where all the business logic was in the code-behind for a button. In this post, we will begin the refactoring process. First, here is the old version.

 

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();
    }
}

 

Our beleaguered Search button does far too many jobs. This violates the Single Responsibility Principle -- the S of SOLID -- and is bad for the reasons we saw last time. Merely breaking up the BtnSearch_Click method into component methods does not fix the problem because it's the class that is supposed to have a single responsibility, not just the method.

So what should we break out into a separate class? An immediately obvious candidate is the logic that builds the search criteria. A novice might make a SearchCriteria class and be done with it, but there is an even better approach. Whenever I hear the word "build," I wonder whether the Builder Pattern would be a good fit. In this case, it is. We want to create an object (the search criteria) piece by piece. Once we have added all the pieces, we use the result and no longer care about the building process. That's the Builder Pattern a nutshell.

We will create two classes: SearchCriteriaBuilder and SearchCriteria. If you think of this pattern in terms of the Single Responsibility Principle, SearchCriteriaBuilder has the responsibility of assembling the search criteria, but there is no need to clutter up the SearchCriteria class with the code to build itself. Once you have search criteria, all of that building code is irrelevant.

For good measure, we will add an interface for each class. By abstracting the builder behind an interface, we make it easier to substitute other forms of builders in the future. Likewise for the search criteria.

All four classes and interfaces are generic on T, which is the type of object we are searching for (a Document in the present application).

SearchCriteriaBuilder lets you add criteria with the AndStartsWith method. Later, we might add other operations such as OrStartsWith or AndIsExactlyEqualTo. For now, AndStartsWith illustrates the point.

SearchCriteria's single responsibility is to present the criteria to the world. The criteria may take the form of a "friendly" (comprehensible to a human) string or an Expression suitable for LINQ to Entities.

Here is the code. By the way, note that the interfaces are implemented explicitly, for the reasons I've outlined here.

ISearchCriteriaBuilder<T>

 

namespace Solid.Interfaces
{
    /// <summary>
    /// Builds search criteria. For now, only simple 'AND' searches are supported.
    /// </summary>
    /// <typeparam name="T">The type of object being searched.</typeparam>
    public interface ISearchCriteriaBuilder<T>
    {
        /// <summary>
        /// AND-in a criterion that the property starts with a value.
        /// </summary>
        /// <param name="friendlyName">A name to display to the user.</param>
        /// <param name="propertyName">The name of the property being queried. It must be a string property.</param>
        /// <param name="value">The property's value must start with this string in order to make a match.</param>
        /// <returns>This object, for a fluent interface.</returns>
        ISearchCriteriaBuilder<T> AndStartsWith(string friendlyName, string propertyName, string value);
        
        /// <summary>
        /// Get a SearchCriteria object that reflects the criteria added so far.
        /// </summary>
        /// <returns>A SearchCriteria object.</returns>
        ISearchCriteria<T> GetResult();        
    }
}

 

 

SearchCriteriaBuilder<T>

 

using System;
using System.Linq.Expressions;
using System.Text;
using Solid.Interfaces;

namespace Solid.Core
{
    /// <summary>
    /// Builds search criteria. For now, only simple 'AND' searches are supported.
    /// </summary>
    /// <typeparam name="T">The type of object being searched.</typeparam>
    public class SearchCriteriaBuilder<T> : ISearchCriteriaBuilder<T>
    {
        #region Fields
        StringBuilder _friendlyString = new StringBuilder();

        // All the criteria that we build up for after the => of a lambda on type T.
        Expression _lambdaCriteria = null;

        // The parameter to feed
        ParameterExpression _parameter = Expression.Parameter(typeof(T));
        #endregion

        #region Constructor
        /// <summary>
        /// Constructor.
        /// </summary>
        public SearchCriteriaBuilder()
        {
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// AND-in a criterion that the property starts with a value.
        /// </summary>
        /// <param name="friendlyName">A name to display to the user.</param>
        /// <param name="propertyName">The name of the property being queried. It must be a string property.</param>
        /// <param name="value">The property's value must start with this string in order to make a match.</param>
        /// <returns>This object, for a fluent interface.</returns>
        ISearchCriteriaBuilder<T> ISearchCriteriaBuilder<T>.AndStartsWith(string friendlyName, string propertyName, string value)
        {
            var friendly = String.Format("({0} starts with '{1}')", friendlyName, value);

            // obj.PropertyName
            var propName = Expression.Property(_parameter, propertyName);

            // The value
            var constValue = Expression.Constant(value);

            // obj.PropertyName.StartsWith(value)
            var startsWith = Expression.Call(
                propName,
                typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),
                constValue);

            if (_lambdaCriteria == null) // First time
            {
                _lambdaCriteria = startsWith;
            }
            else // Not first time
            {
                _friendlyString.Append(" AND ");
                _lambdaCriteria = Expression.AndAlso(_lambdaCriteria, startsWith);
            }
            _friendlyString.Append(friendly);
            return this;
        }

        /// <summary>
        /// Get a SearchCriteria object that reflects the criteria added so far.
        /// </summary>
        /// <returns>A SearchCriteria object.</returns>
        ISearchCriteria<T> ISearchCriteriaBuilder<T>.GetResult()
        {
            if (_lambdaCriteria == null)
            {
                return new SearchCriteria<T>(Properties.Resources.CriteriaAllRecords,
                    Expression.Lambda<Func<T, bool>>(Expression.Constant(true), _parameter));
            }
            else
            {
                return new SearchCriteria<T>(
                    _friendlyString.ToString(),
                    Expression.Lambda<Func<T, bool>>(_lambdaCriteria, _parameter));
            }
        }
        #endregion
    }
}

 

 

ISearchCriteria<T>

 

using System;
using System.Linq.Expressions;

namespace Solid.Interfaces
{
    /// <summary>
    /// Represents the criteria for searching for objects of type T.
    /// </summary>
    /// <typeparam name="T">The type of objects being searched.</typeparam>
    public interface ISearchCriteria<T>
    {
        /// <summary>
        /// Get a friendly representation of the criteria.
        /// </summary>
        string FriendlyString { get; }

        /// <summary>
        /// Get the criteria as a LINQ Expression.
        /// </summary>
        Expression<Func<T, bool>> Expression { get; }
    }
}

 

 

SearchCriteria<T>

 

using System;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using Solid.Interfaces;

namespace Solid.Core
{
    /// <summary>
    /// Represents criteria for searching a repository of objects of type T.
    /// </summary>
    public class SearchCriteria<T> : ISearchCriteria<T>
    {
        /// <summary>
        /// Get a friendly representation of the criteria.
        /// </summary>
        public string FriendlyString { get; private set; }

        /// <summary>
        /// Get the criteria as a LINQ Expression.
        /// </summary>
        public Expression<Func<T, bool>> Expression { get; private set; }
       
        /// <summary>
        /// Constructor. It is internal because it is intended that the SearchCriteriaBuilder,
        /// in this assemby, be used to construct SearchCriteria.
        /// </summary>
        /// <param name="friendlyString">A user-friendly way of expressing the search criteria.</param>
        /// <param name="expression">A LINQ Expression that contains the search criteria.</param>
        internal SearchCriteria(string friendlyString, Expression<Func<T, bool>> expression)
        {
            Contract.Requires(friendlyString != null);
            Contract.Requires(expression != null);
            
            FriendlyString = friendlyString;
            Expression = expression;
        }
    }
}

 

For faithfully adhering to the Single Responsibility Principle, we have been rewarded with a set of classes and interfaces that are eminently reusable and -- just as important -- easily testable. That's the 'S' of SOLID.

Next time, we'll move on to the 'O' and see what it means for a class to be open for extension but closed for modification.

Tags: , ,

All | Composibility | Dependency Injection | General | Talks

Pingbacks and trackbacks (2)+

Add comment