If you've had an education in computer science or programming, then you'll likely have seen design patterns before. In practice however, it may be more difficult to recognize where design patterns are appropriate. In this series of posts, I'd like to put several design patterns in the spotlight and give some practical examples on how you can use them in your Umbraco websites.
Recap
Design patterns, also known as: programming patterns, are "common solutions to common problems". The aim of design patterns is to support you as a developer when dealing with complexity. Not only do they stimulate healthy programming habits, they're also an effective communication device between developers.
Strategy pattern
I'm about 80% confident that the strategy is my favourite design pattern. My Url Tracker plugin already has at least 8 of them! The strategy pattern has helped me a lot to manage the diversity that Umbraco has to offer.
You'll want to use the strategy pattern if you need to do the same thing in different ways. That would look something like this:
Using this pattern, the consuming code is no longer concerned with how something is done, as long as it is done.
Umbraco provides some tools that make it easier for you to use the strategy pattern, in the form of collection builders. Let's have a look at how that works:
Strategy pattern in practice
In this example, I'll show some code that I've seen many times in practice and we'll refactor it so that it uses a strategy pattern. If you've done anything with the examine index, you may be familiar with code like this:
public class IndexComponent : IComponent
{
private readonly IExamineManager _examineManager;
private readonly IUmbracoContextFactory _umbracoContextFactory;
public IndexComponent(IExamineManager examineManager,
IUmbracoContextFactory umbracoContextFactory)
{
_examineManager = examineManager;
_umbracoContextFactory = umbracoContextFactory;
}
public void Initialize()
{
if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out var index)) return;
index.TransformingIndexValues += OnTransformingIndexValues;
}
private void OnTransformingIndexValues(object? sender, IndexingItemEventArgs e)
{
if (e.ValueSet.Category != IndexTypes.Content) return;
var updatedValues = e.ValueSet.Values.ToDictionary(x => x.Key, x => (IEnumerable<object>)x.Value);
using var cref = _umbracoContextFactory.EnsureUmbracoContext();
var content = cref.UmbracoContext.Content?.GetById(int.Parse(e.ValueSet.Id));
if (content is null) return;
if (content is ContentPage contentPage)
{
var list = new List<object>();
list.Add(contentPage.PublicationDate.Ticks);
updatedValues["sortablePublicationDate"] = list;
}
e.SetValues(updatedValues);
}
public void Terminate()
{ }
}
Although this code works, it is not very scalable. What if NewsPage
also has a publication date? Or BlogPage
? What if I want to add more values to the index? On a decently large website, this class is going to get out of control.
Let's introduce a strategy pattern:
public interface IIndexStrategy
{
bool CanHandle(IPublishedContent content);
Dictionary<string, IEnumerable<object>> Enrich(Dictionary<string, IEnumerable<object>> indexValues, IPublishedContent content);
}
Let's also introduce a base implementation, based on common operations:
public abstract class IndexStrategyBase<T>
: IIndexStrategy
where T : IPublishedContent
{
// Any implementation based on this base class will handle indexes for content that implements 'T'
public virtual bool CanHandle(IPublishedContent content)
=> content is T;
public Dictionary<string, IEnumerable<object>> Enrich(Dictionary<string, IEnumerable<object>> indexValues, IPublishedContent content)
=> Enrich(indexValues, (T)content);
protected abstract Dictionary<string, IEnumerable<object>> Enrich(Dictionary<string, IEnumerable<object>> indexValues, T content);
}
Now we can use the base class to make some specific implementations:
// An implementation may use a specific document type
public class ContentPageIndexStrategy
: IndexStrategyBase<ContentPage>
{
protected override Dictionary<string, IEnumerable<object>> Enrich(Dictionary<string, IEnumerable<object>> indexValues, ContentPage content)
{
var list = new List<object>();
list.Add(content.PublicationDate.Ticks);
indexValues["sortablePublicationDate"] = list;
return indexValues;
}
}
// An implementation may also use a composition of a document type
public class SeoCompositionIndexStrategy
: IndexStrategyBase<ISeoComposition>
{
protected override Dictionary<string, IEnumerable<object>> Enrich(Dictionary<string, IEnumerable<object>> indexValues, ISeoComposition content)
{
var list = new List<object>();
list.Add(content.ShouldIndex && !string.IsNullOrWhiteSpace(content.SeoTitle));
indexValues["seoIsConfigured"] = list;
return indexValues;
}
}
Next up is a collection builder to bundle our strategy together:
public class IndexStrategyCollectionBuilder
: OrderedCollectionBuilderBase<IndexStrategyCollectionBuilder, IndexStrategyCollection, IIndexStrategy>
{
protected override IndexStrategyCollectionBuilder This => this;
}
public interface IIndexStrategyCollection
{
Dictionary<string, IEnumerable<object>> Enrich(Dictionary<string, IEnumerable<object>> indexValues, int contentId);
}
public class IndexStrategyCollection
: BuilderCollectionBase<IIndexStrategy>, IIndexStrategyCollection
{
private readonly IUmbracoContextFactory _umbracoContextFactory;
public IndexStrategyCollection(Func<IEnumerable<IIndexStrategy>> items, IUmbracoContextFactory umbracoContextFactory)
: base(items)
{
_umbracoContextFactory = umbracoContextFactory;
}
public Dictionary<string, IEnumerable<object>> Enrich(Dictionary<string, IEnumerable<object>> indexValues, int contentId)
{
using var cref = _umbracoContextFactory.EnsureUmbracoContext();
var content = cref.UmbracoContext.Content?.GetById(contentId);
if (content is null) return indexValues;
foreach(var strategy in this.Where(s => s.CanHandle(content)))
indexValues = strategy.Enrich(indexValues, content);
return indexValues;
}
}
Finally, we need to register the strategy in Umbraco's dependency injection container:
// Create a convenience extension to make it easier to use the strategy
public static class UmbracoContainerExtensions
{
public static IndexStrategyCollectionBuilder IndexStrategies(this IUmbracoBuilder builder)
{
builder.Services.AddUnique<IIndexStrategyCollection>(sp => sp.GetRequiredService<IndexStrategyCollection>());
return builder.WithCollectionBuilder<IndexStrategyCollectionBuilder>();
}
}
// Create a composer to compose the strategy
public class IndexComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.IndexStrategies()
.Append<ContentPageIndexStrategy>()
.Append<SeoCompositionIndexStrategy>();
}
}
Now to use the strategy, we need to change the original code to this:
public class IndexComponent : IComponent
{
private readonly IExamineManager _examineManager;
private readonly IIndexStrategyCollection _strategyCollection;
public IndexComponent(IExamineManager examineManager,
IIndexStrategyCollection strategyCollection)
{
_examineManager = examineManager;
_strategyCollection = strategyCollection;
}
public void Initialize()
{
if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out var index)) return;
index.TransformingIndexValues += OnTransformingIndexValues;
}
private void OnTransformingIndexValues(object? sender, IndexingItemEventArgs e)
{
if (e.ValueSet.Category != IndexTypes.Content) return;
var updatedValues = e.ValueSet.Values.ToDictionary(x => x.Key, x => (IEnumerable<object>)x.Value);
updatedValues = _strategyCollection.Enrich(updatedValues, int.Parse(e.ValueSet.Id));
e.SetValues(updatedValues);
}
public void Terminate()
{ }
}
The benefits
We've made significant changes to the original code, so let's have a look at what we gained:
- Testability: Every class now has a single responsibility and none of them have any hard dependencies. It's not only easier to test these classes, but also to see which cases you need to test.
- Readability: It's much easier to read what each object does. We no longer have to struggle with an ever-growing class that may reach several hundreds of lines.
- Scalability: Although the process of adding more rules is more or less the same, we can be sure that none of the individual parts grow out of control. We can also be more confident that a change to one part of the logic doesn't accidentally affect another part of the logic.
- Extendability: Thanks to Umbraco's collection builders, it is now possible to have your index logic extended by plugins or application parts. On top of that, the collection builders allow you to manage the order in which the various strategies appear in your collection, which can be useful in certain cases.
This is just one example of many. Some other potential uses for the strategy pattern include, but are not limited to:
- ✅ Serializing content into different viewmodels in api endpoints
- ✅ Processing block list elements for various purposes, like word-counters.
- ✅ Searching content in different ways, based on query parameters.
When you have a hammer...
What's important to understand is that design patterns are not rules, but guidelines. That is: the pattern should fit the problem, not the other way around. Some bad examples of strategy pattern are:
- ❌ Combining multiple features into a single api endpoint, like updating or adding entities based on a query parameter.
- ❌ Eliminating branching with limited options, for example: signing up on a form with or without subscribing to a newsletter.
Conclusion
Umbraco gives us the tools to greatly increase the quality of our code with the strategy pattern using collection builders. The strategy pattern is a great solution if you have multiple ways to do the same thing.
Have you ever implemented a strategy pattern? Do you have any questions about the pattern? What other patterns would you like me to address? Let me know with a comment!
Top comments (0)