In recent work with Sitecore, Solr has been recently introduced to some development I have been working on. The work required writing several instances in which we needed to query Solr for some information. At this time, I wanted some way of profiling and logging how these methods were operating, but I didn't want to bog down my service class with dependencies on logging and profiling.

I have also been reading through Adaptive Code via C# which has been a fantastic book on design patterns and SOLID. Here I am going to apply a couple patterns to keep my code more adaptable and maintainable over time.

Design Patterns I will try to put into practice are:

  • Decorator Pattern
  • Adapter Pattern
  • Factory Design Pattern

Let's get started with an example. Below is a simple class that searches Albums based on keyword:

public class MusicSearchService : IMusicSearchService
{
    private IProviderSearchContext _search;
    
    public MusicSearchService(IProviderSearchContext search)
    {
        _search = search;
    }
    
    public IEnumerable<Albums> SearchAlbums(string keyword)
    {
        IQueryable<SearchResultItem> albums = _search.GetQueryable<SearchResultItem>()
            .Where(x => x.Content.Like("*" + keyword + "*"));
            
        foreach(SearchResultItem item in albums)
        {
            yield return new Album() {
                Title = item.Name
            };
        }
    }
}

I have added an interface for the service class because interfaces are great to hand to a client and not be specific to some concrete class. The client doesn't need to know which class implementation I hand to it.

public class IMusicSearchService
{
    IEnumerable<Albums> SearchAlbums(string keyword);
}
Decorator Pattern

In order to not introduce profiling logic to my SearchAlbums method, I am going to introduce the Decorator Pattern. The Decorator Pattern has a couple of requirements to make this work:

  1. MusicSearchService needs to become an abstract class while making the SearchAlbums method an abstract method.
  2. ConcreteMusicSearchService needs to be created to override the SearchAlbums method which we will move the search logic to.
  3. MusicSearchServiceDecorator also will need to be created to allow us to implementing some activity such as profiling away from the search logic.

Below is what this looks like:

MusicSearchService

public class MusicSearchService : IMusicSearchService
{
    public abstract IEnumerable<Albums> SearchAlbums(string keyword);
}

ConcreteMusicSearchService

public class ConcreteMusicSearchService : MusicSearchService, IMusicSearchService
{
    private IProviderSearchContext _search;
    
    public ConcreteMusicSearchService(IProviderSearchContext search)
    {
        _search = search;
    }
    
    public override IEnumerable<Albums> SearchAlbums(string keyword)
    {
        IQueryable<SearchResultItem> albums = _search.GetQueryable<SearchResultItem>()
            .Where(x => x.Content.Like("*" + keyword + "*"));
            
        foreach(SearchResultItem item in albums)
        {
            yield return new Album() {
                Title = item.Name
            };
        }
    }
}

MusicSearchServiceDecorator

public class MusicSearchServiceDecorator : MusicSearchService, IMusicSearchService
{
    private IMusicSearchService _decoratedService;
    
    public MusicSearchServiceDecorator(IMusicSearchService decoratedService)
    {
        _decoratedService = decoratedService;
    }
    
    public override IEnumerable<Albums> SearchAlbums(string keyword)
    {
    	return _decoratedService.SearchAlbums(keyword);
    }
}

Normally, I would have been more specific in naming these classes, but I wanted to what is the decorator and which is the concrete class. Since I was creating a decorator for profiling, I would have named the class ProfilingMusicSearchService. Also, I would have named the abstract class MusicSearchServiceBase in I can treat it as a base class.

Adapter Pattern

As part of profiling the search logic for albums, we want to utilize the stopwatch to measure the execution time. However, I may want to create an adapter class to wrap around the stopwatch implementation I want to use. In the future, there may be other implementations of stopwatch I may decide to use down the road. Or, as the Adapter Pattern suggests, I may have an implementation that may be incompatible to use and I may want to wrap around this incompatibility.

We will create a StopwatchAdapter class to implement the stopwatch:

public class StopwatchAdapter : IStopwatch
{
    private readonly Stopwatch _stopwatch;
    public StopwatchAdapter(Stopwatch stopwatch)
    {
        _stopwatch = stopwatch;
    }

    public void Start()
    {
        _stopwatch.Start();
    }

    public long Stop()
    {
        _stopwatch.Stop();
        long elaspedMilliseconds = _stopwatch.ElapsedMilliseconds;
        _stopwatch.Reset();
        return elaspedMilliseconds;
    }
}

We will also create an IStopwatch interface to inject into the decorator class.

public interface IStopwatch
{
    void Start();
    long Stop();
}

Let's update our decorator class to inject the stopwatch implementation:

public class MusicSearchServiceDecorator : MusicSearchService, IMusicSearchService
{
    private IMusicSearchService _decoratedService;
    private IStopwatch _stopwatch;
    
    public MusicSearchServiceDecorator(IMusicSearchService decoratedService, IStopwatch stopwatch)
    {
        _decoratedService = decoratedService;
        _stopwatch = stopwatch;
    }
    
    public override IEnumerable<Albums> SearchAlbums(string keyword)
    {
    	_stopwatch.Start()
    	var results = _decoratedService.SearchAlbums(keyword);
        long elapsedTime = _stopwatch.Stop();
        return results;
    }
}

Finally, what we can do next is implement a Logger Adaptive Class the same way and log the information we want from profiling.

At this point, everything has been written to be fairly testable. I can mock these classes and create some unit tests if I needed. I have also separated my profiling away from my Albums search implementation and my SearchAlbums has only one reason to change.

Factory Design Pattern

All that is left is to when to turn on profiling. There are several ways I could approach this such as utilizing some dependency injection with my service classes or let a factory class to my Music Albums functionality determine when to start profiling.

Initially, we can start with a factory class as follows:

public static class MusicAlbumsFactory()
{
    public static IMusicSearchService GetMusicService(IProviderSearchService search)
    {
    	return new ConcreteMusicSearchService(search);
    }
}

Let's add some logic to determine when to profile. Here I added a grab a setting from Sitecore settings. This can be done independently by configuration or from a Sitecore content item. I am going to allow this to be injected into the factory class's GetMusicService:

public static class MusicAlbumsFactory()
{
    public static IMusicSearchService GetMusicService(IProviderSearchService search, ISearchSetting setting)
    {
        IMusicSearchService service = new ConcreteMusicSearchService(search);
    	if(setting.IsProfiling)
        {
            IStopwatch stopwatch = new StopwatchAdapter(new System.Diagnostics.Stopwatch());
            return new MusicSearchServiceDecorator(service, stopwatch);
        }
    	return new ConcreteMusicSearchService(search);
    }
}

Here is what I love about interfaces, the client doesn't need to know what I hand it as long it can expect what the interface tells it. Here, I've either handed it the concrete class or the decorator class depending when I want to profile.

Much credit goes to Gary McLean Hall the author of Adaptive Code via C#. You can see another example of this in action at Profiling Decorate Example on Github.

I hope this was practical. As I have been learning the concepts of SOLID, these design patterns have given me another tool to create some clean and maintainable code.