So far in this series, we have considered the first four points of SOLID development:
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.
We are now ready for the last one:
D: Dependency Inversion Principle -- Code should depend on abstractions, not on concrete classes.
What does it mean to depend on an abstraction? In Part 3 of this series, we saw a class called DocumentSearcher. We decided that we wanted to log our searches, and introduced LoggingSearcher. Now if the consuming code had instantiated DocumentSearchers all over the place, we would have had to change every instance of
var searcher = new DocumentSearcher();
to
var searcher = new LoggingSearcher(wrappedSearcher, logger);
However, both DocumentSearcher and LoggingSearcher inherit from ISearcher. As long as the consuming code only mentions ISearcher, and never one of the concrete searchers, it does not have to change. As long as your various implementations of ISearcher obey the Liskov Substitution Principle, you'll know you haven't broken anything.
And that's the dependency inversion principle at work: depend on the abstraction (ISearcher) instead of the concrete (DocumentSearcher or LoggingSearcher). It's called dependency inversion because in the old, top-down hierarchy of structured programming a class determines its own dependencies as it instantiates concrete instances, but in SOLID code the class is told by its callers what those concrete classes will be, via dependency injection or some other mechanism, as we will see.
Before we get to all that, I want to bring bring out an MVC version of that application.
You can download it here: RefactoringToSOLID.zip (3.13 mb). A few notes:
- To compile the Visual Studio 2010 solution, you will need at least version 3 of ASP.NET MVC, version 4.1 of the Entity Framework and Microsoft Unity 2.0.
- To run it, you must first create a SQL Server database. Name the database anything you like and then build its tables and data using the script provided in the same directory as the solution. Finally, modify the SolidDemoEntities connection strings in the config files.
- The solution contains two versions of the WebForms app: Step0 and Step0WithLogging. Those are examples of how NOT to do it and correspond to earlier posts in this series. The ASP.NET MVC project is Solid.WebApp.
- The code and config files are set up for the grand finale of this series (Part 9). You'll see #if statements for other refactoring steps in the MVC app. Those steps are more fine-grained than what I cover in these blog posts. They are there for you to explore if you wish.
MVC and its ASP.NET incarnation are huge topics that have been explained well elsewhere (start here if you're not familiar with it) so I'm going to stick to the purpose of this series and focus on refactoring from the WebForms application to the SOLID, MVC version. Here are the refactoring steps I took:
- As explained in Part 2 and Part 3, I created the ISearcher, ISearchCriteria and IQueryLogger interfaces, and made the Searcher, SearchCriteria and QueryLogger classes inherit from them. I find it helpful to put interfaces in their own project so their consumers can use them without dragging in a lot of dependencies (especially the concrete classes!). In the sample solution, the interfaces are in the Solid.Interfaces project.
- I put the concrete classes in the Solid.Core and Solid.MyDatabase projects. So not only are these classes refactored out of the code-behind, but they are also refactored out of the UI project. This enables their reuse in other contexts.
- With that refactoring in place, the unit tests went naturally in their own project, Solid.Core.Test. (I got lazy and did not unit-test the QueryLogger. My lame justification was that any test that involved a database was an integration test, not a unit test.)
At that point, I was ready to create the MVC 3 Web application. The Visual Studio MVC template creates separate source files for the model, view and controller. For the purpose of our refactoring discussion, the model is the most interesting. Here's the source. Our discussion will begin with the Search method at the end.
using System;
using System.Collections.Generic;
using Solid.Core;
using Solid.Interfaces;
using Solid.MyDatabase;
namespace Solid.WebApp.Models
{
/// <summary>
/// The Model behind the DocumentsController and View.
/// </summary>
public class DocumentsModel
{
#region Fields
// The object that does all the work.
readonly ISearcher<Document> _searcher;
#endregion
#region Properties
/// <summary>
/// Get the Account that was searched for. Null or empty means Account was not a criterion.
/// </summary>
public string Account { get; private set; }
/// <summary>
/// Get the last name that was searched for. Null or empty means Account was not a criterion.
/// </summary>
public string LastName { get; private set; }
/// <summary>
/// Get the first name that was searched for. Null or empty means Account was not a criterion.
/// </summary>
public string FirstName { get; private set; }
/// <summary>
/// Get the criteria as a user-friendly string.
/// </summary>
public string FriendlyCriteria { get; private set; }
/// <summary>
/// Get the results of the search.
/// </summary>
public IEnumerable<Document> Results { get; private set; }
#endregion
#region Constructor
/// <summary>
/// Constructor. Sets all properties to displayable initial values.
/// </summary>
/// <param name="searcher">The searcher to use, or null to use the default searcher.</param>
public DocumentsModel(ISearcher<Document> searcher = null)
{
_searcher = searcher ?? new LoggingSearcher<Document>(new DocumentSearcher(), new QueryLogger());
Account = "";
LastName = "";
FirstName = "";
FriendlyCriteria = "";
Results = new Document[] { };
}
#endregion
#region Public Methods
/// <summary>
/// Execute a search and set all properties to reflect the criteria and results.
/// </summary>
/// <param name="account">The account to search for. Null or empty means not to search by account.</param>
/// <param name="lastName">The last name to search for. Null or empty means not to search by account.</param>
/// <param name="firstName">The first name to search for. Null or empty means not to search by account.</param>
public void Search(string account, string lastName, string firstName )
{
Account = account;
LastName = lastName;
FirstName = firstName;
var criteriaBuilder = new SearchCriteriaBuilder<Document>();
foreach (var tuple in new[]
{
// Tuples are FrientlyName / PropertyName / Value
Tuple.Create("Account", "Account", Account),
Tuple.Create("Last Name", "LastName", LastName),
Tuple.Create("First Name", "FirstName", FirstName),
})
{
if (!string.IsNullOrEmpty(tuple.Item3))
criteriaBuilder.AndStartsWith(tuple.Item1, tuple.Item2, tuple.Item3);
}
var criteria = criteriaBuilder.GetResult();
FriendlyCriteria = criteria.FriendlyString;
Results = _searcher.Search(criteria);
}
#endregion
}
}
Based on the parameters passed by the controller, Search builds the search criteria and passes them to _searcher (last line of the method). Where did _searcher come from? Notice the constructor. It takes an ISearcher<Document> as a parameter and sets _searcher to it. This is an example of dependency injection. The class depends on an interface (dependency inversion) and we supply the concrete class that fulfills the interface via the constructor (dependency injection).
There are several methods for bringing this about, and we will consider them starting in the next post.