Refactoring to SOLID - Part 3: The Open/Closed Principle

by Larry Spencer Wednesday, April 4, 2012 7:58 AM

In this series, we have been refactoring an old-style Web application to a new one based on SOLID principles. Our sample application searches for documents in a repository.

In the previous post  we saw that the code-behind for the Search button had far too much to do, violating the Single Responsibility Principle. We refactored the building of search criteria into separate classes that followed the Builder Pattern. This allows us to put the search itself in a very simple class, which is below. I've made it inherit from a generic ISearcher<T> interface, for reasons that will be clear later.

 

 

DocumentSearcher

 

public class DocumentSearcher : ISearcher<Document>
{
    /// <summary>
    /// Search the repository and return the matches.
    /// </summary>
    /// <param name="criteria">The criteria for the search. Null means to return all documents.</param>
    /// <returns>An IEnumerable of matching objects.</returns>
    public IEnumerable<Document> Search(ISearchCriteria<Document> criteria)
    {
        using (var db = new SolidDemoEntities())
        {
            var query = db.Documents.AsQueryable();
            if (criteria != null)
                query = query.Where(criteria.Expression);
            return query.ToArray();
        }
    }
}
 

 

Now suppose we have a new requirement to log all the searches. Our first instinct might be to add logging to DocumentSearcher.Search, perhaps with an optional bool parameter that says whether to actually log or not. That would be a bad instinct, for several reasons:

  • The DocumentSearcher class would have the additional, unrelated reponsibility of logging. This would violate the Single Responsibility Principle that we just worked so hard to implement.
  • The code for logging would not be reusable elsewhere.
  • If we ever decide not to log, our method has "junk DNA" -- irrelevant, useless code.

A better approach is to wrap the DocumentSearcher in a new class called LoggingSearcher. This is the Decorator Pattern: to wrap an existing class in a class that extends its functionality. It exemplifies the 'O' of SOLID, which says that a class should be open for extension, but closed to modification. Once our DocumentSearcher is working, we should not modify it, except for bug fixes.

While we're at it, let's generalize the logging capability to an IQueryLogger interface. Implementations of this interface might include logging to a database, logging to a file, etc.

Here's the idea in UML.

 

Search Classes

 

...and here it is in code. You'll see that the constructor takes the ISearcher<T> that the class wraps, plus an IQueryLogger that does the actual logging. This is the Dependency Injection pattern, which we'll cover in some detail later (when I'll also explain the Dependency attribute). For now, the point is that we considered our DocumentSearcher class to be closed for modification, but open for extension. 

Also notice that LoggingSearcher inherits from ISearcher<T>, just as DocumentSearcher did. This sets us up to follow the Liskov Substitution Principle -- the 'L'  of SOLID, which will be the subject of the next post.

 

 

LoggingSearcher

 

/// <summary>
/// Decorates a wrapped ISearcher with logging.
/// </summary>
/// <typeparam name="T"></typeparam>
public class LoggingSearcher<T> : ISearcher<T>
{
    #region Fields
    ISearcher<T> _wrappedSearcher;
    IQueryLogger _logger;
    #endregion

    #region Properties
    public const string SearcherDependency  = "LoggingSearcher_WrappedSearcher";
    #endregion

    #region Constructor
    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="wrappedSearcher">The underlying searcher.</param>
    /// <param name="logger">The logger, or null if logging is not desired.</param>
    public LoggingSearcher(
        [Dependency(SearcherDependency)] ISearcher<T> wrappedSearcher, 
        IQueryLogger logger)
    {
        Contract.Requires(wrappedSearcher != null);

        _wrappedSearcher = wrappedSearcher;
        _logger = logger;
    }
    #endregion

    #region ISearcher<T> Implementation
    /// <summary>
    /// Search the repository and return the matches.
    /// </summary>
    /// <param name="criteria">The criteria for the search.
    /// Null means to return all records.</param>
    /// <returns>An IEnumerable of matching objects.</returns>
    IEnumerable<T> ISearcher<T>.Search(ISearchCriteria<T> criteria)
    {
        if (_logger != null)
            _logger.Log(WindowsIdentity.GetCurrent().User, criteria.FriendlyString);
        return _wrappedSearcher.Search(criteria);
    }
    #endregion
}

 

 

ISearcher

 

/// <summary>
/// Describes a class that can search through a repository of objects of type T.
/// </summary>
public interface ISearcher<T>
{
    /// <summary>
    /// Search the repository and return the matches.
    /// </summary>
    /// <param name="criteria">The criteria for the search.
    /// Null means to return all records.</param>
    /// <returns>An IEnumerable of matching objects.</returns>
    IEnumerable<T> Search(ISearchCriteria<T> criteria);
}

 

 

IQueryLogger

 

/// <summary>
/// Interface for classes that can log queries.
/// </summary>
public interface IQueryLogger 
{
    /// <summary>
    /// Log a query with the given criteria by the given user.
    /// Use the current UTC time.
    /// </summary>
    /// <param name="sid">The user's SID.</param>
    /// <param name="criteria">The criteria that are being logged.</param>
    void Log(SecurityIdentifier sid, string criteria);
}

 

 

QueryLogger

 

/// <summary>
/// Logs search criteria (in string form) to an
/// EntityFramework database.
/// </summary>
public sealed class QueryLogger : IQueryLogger
{
    /// <summary>
    /// Log a query with the given criteria by the given user.
    /// Use the current UTC time.
    /// </summary>
    /// <param name="sid">The user's SID.</param>
    /// <param name="criteria">The criteria that are being logged.</param>
    public void Log(SecurityIdentifier sid, string criteria)
    {
        using (var entities = new SolidDemoEntities())
        {
            entities.QueryLogs.Add(new QueryLog()
                {
                    Criteria = criteria,
                    Sid = sid.ToString(),
                    TimeUtc = DateTime.UtcNow
                });
            entities.SaveChanges();
        }
    }
}

Tags: , ,

All | Composibility | Dependency Injection | 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.